/**
 * xeokit-sdk v2.6.78
 *  Commit: 17c76751873b26ec38b03ae632f1d2003dc7a98a
 *  Built: 2025-06-18T11:33:48.212Z
 */

/** @private */
let Map$1$1 = class Map$1 {

    constructor(items, baseId) {
        this.items = items || [];
        this._lastUniqueId = (baseId || 0) + 1;
    }

    /**
     * Usage:
     *
     * id = myMap.addItem("foo") // ID internally generated
     * id = myMap.addItem("foo", "bar") // ID is "foo"
     */
    addItem() {
        let item;
        if (arguments.length === 2) {
            const id = arguments[0];
            item = arguments[1];
            if (this.items[id]) { // Won't happen if given ID is string
                throw "ID clash: '" + id + "'";
            }
            this.items[id] = item;
            return id;

        } else {
            item = arguments[0] || {};
            while (true) {
                const findId = this._lastUniqueId++;
                if (!this.items[findId]) {
                    this.items[findId] = item;
                    return findId;
                }
            }
        }
    }

    removeItem(id) {
        const item = this.items[id];
        delete this.items[id];
        return item;
    }
};

const idMap = new Map$1$1();

/**
 * Internal data class that represents the state of a menu or a submenu.
 * @private
 */
class Menu {
    constructor(id) {
        this.id = id;
        this.parentItem = null; // Set to an Item when this Menu is a submenu
        this.groups = [];
        this.menuElement = null;
        this.shown = false;
        this.mouseOver = 0;
    }
}

/**
 * Internal data class that represents a group of Items in a Menu.
 * @private
 */
class Group {
    constructor() {
        this.items = [];
    }
}

/**
 * Internal data class that represents the state of a menu item.
 * @private
 */
class Item {
    constructor(id, getTitle, doAction, getEnabled, getShown) {
        this.id = id;
        this.getTitle = getTitle;
        this.doAction = doAction;
        this.getEnabled = getEnabled;
        this.getShown = getShown;
        this.itemElement = null;
        this.subMenu = null;
        this.enabled = true;
    }
}

/**
 * @desc A customizable HTML context menu.
 *
 * [<img src="http://xeokit.io/img/docs/ContextMenu/ContextMenu.gif">](https://xeokit.github.io/xeokit-sdk/examples/index.html#ContextMenu_Canvas_TreeViewPlugin_Custom)
 *
 * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#ContextMenu_Canvas_TreeViewPlugin_Custom)]
 *
 * ## Overview
 *
 * * A pure JavaScript, lightweight context menu
 * * Dynamically configure menu items
 * * Dynamically enable or disable items
 * * Dynamically show or hide items
 * * Supports cascading sub-menus
 * * Configure custom style with CSS (see examples above)
 *
 * ## Usage
 *
 * In the example below we'll create a ````ContextMenu```` that pops up whenever we right-click on an {@link Entity} within
 * our {@link Scene}.
 *
 * First, we'll create the ````ContextMenu````, configuring it with a list of menu items.
 *
 * Each item has:
 *
 * * a ````title```` for the item,
 * * a ````doAction()```` callback to fire when the item's title is clicked,
 * * an optional ````getShown()```` callback that indicates if the item should shown in the menu or not, and
 * * an optional ````getEnabled()```` callback that indicates if the item should be shown enabled in the menu or not.
 *
 * <br>
 *
 * The ````getShown()```` and ````getEnabled()```` callbacks are invoked whenever the menu is shown.
 *
 * When an item's ````getShown()```` callback
 * returns ````true````, then the item is shown. When it returns ````false````, then the item is hidden. An item without
 * a ````getShown()```` callback is always shown.
 *
 * When an item's ````getEnabled()```` callback returns ````true````, then the item is enabled and clickable (as long as it's also shown). When it
 * returns ````false````, then the item is disabled and cannot be clicked. An item without a ````getEnabled()````
 * callback is always enabled and clickable.
 *
 * Note how the ````doAction()````,  ````getShown()```` and ````getEnabled()```` callbacks accept a ````context````
 * object. That must be set on the ````ContextMenu```` before we're able to we show it. The context object can be anything. In this example,
 * we'll use the context object to provide the callbacks with the Entity that we right-clicked.
 *
 * We'll also initially enable the ````ContextMenu````.
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#ContextMenu_Canvas_Custom)]
 *
 * ````javascript
 * const canvasContextMenu = new ContextMenu({
 *
 *    enabled: true,
 *
 *    items: [
 *       [
 *          {
 *             title: "Hide Object",
 *             getEnabled: (context) => {
 *                 return context.entity.visible; // Can't hide entity if already hidden
 *             },
 *             doAction: function (context) {
 *                 context.entity.visible = false;
 *             }
 *          }
 *       ],
 *       [
 *          {
 *             title: "Select Object",
 *             getEnabled: (context) => {
 *                 return (!context.entity.selected); // Can't select an entity that's already selected
 *             },
 *             doAction: function (context) {
 *                 context.entity.selected = true;
 *             }
 *          }
 *       ],
 *       [
 *          {
 *             title: "X-Ray Object",
 *             getEnabled: (context) => {
 *                 return (!context.entity.xrayed); // Can't X-ray an entity that's already X-rayed
 *             },
 *             doAction: (context) => {
 *                 context.entity.xrayed = true;
 *             }
 *          }
 *       ]
 *    ]
 * });
 * ````
 *
 * Next, we'll make the ````ContextMenu```` appear whenever we right-click on an Entity. Whenever we right-click
 * on the canvas, we'll attempt to pick the Entity at those mouse coordinates. If we succeed, we'll feed the
 * Entity into ````ContextMenu```` via the context object, then show the ````ContextMenu````.
 *
 * From there, each ````ContextMenu```` item's ````getEnabled()```` callback will be invoked (if provided), to determine if the item should
 * be enabled. If we click an item, its ````doAction()```` callback will be invoked with our context object.
 *
 * Remember that we must set the context on our ````ContextMenu```` before we show it, otherwise it will log an error to the console,
 * and ignore our attempt to show it.
 *
 * ````javascript*
 * viewer.scene.canvas.canvas.oncontextmenu = (e) => { // Right-clicked on the canvas
 *
 *     if (!objectContextMenu.enabled) {
 *         return;
 *     }
 *
 *     var hit = viewer.scene.pick({ // Try to pick an Entity at the coordinates
 *         canvasPos: [e.pageX, e.pageY]
 *     });
 *
 *     if (hit) { // Picked an Entity
 *
 *         objectContextMenu.context = { // Feed entity to ContextMenu
 *             entity: hit.entity
 *         };
 *
 *         objectContextMenu.show(e.pageX, e.pageY); // Show the ContextMenu
 *     }
 *
 *     e.preventDefault();
 * });
 * ````
 *
 * Note how we only show the ````ContextMenu```` if it's enabled. We can use that mechanism to switch between multiple
 * ````ContextMenu```` instances depending on what we clicked.
 *
 * ## Dynamic Item Titles
 *
 * To make an item dynamically regenerate its title text whenever we show the ````ContextMenu````, provide its title with a
 * ````getTitle()```` callback. The callback will fire each time you show ````ContextMenu````, which will dynamically
 * set the item title text.
 *
 * In the example below, we'll create a simple ````ContextMenu```` that allows us to toggle the selection of an object
 * via its first item, which changes text depending on whether we are selecting or deselecting the object.
 *
 * [[Run an example](https://xeokit.github.io/xeokit-sdk/examples/index.html#ContextMenu_dynamicItemTitles)]
 *
 * ````javascript
 * const canvasContextMenu = new ContextMenu({
 *
 *    enabled: true,
 *
 *    items: [
 *       [
 *          {
 *              getTitle: (context) => {
 *                  return (!context.entity.selected) ? "Select" : "Undo Select";
 *              },
 *              doAction: function (context) {
 *                  context.entity.selected = !context.entity.selected;
 *              }
 *          },
 *          {
 *              title: "Clear Selection",
 *              getEnabled: function (context) {
 *                  return (context.viewer.scene.numSelectedObjects > 0);
 *              },
 *              doAction: function (context) {
 *                  context.viewer.scene.setObjectsSelected(context.viewer.scene.selectedObjectIds, false);
 *              }
 *          }
 *       ]
 *    ]
 * });
 * ````
 *
 * ## Sub-menus
 *
 * Each menu item can optionally have a sub-menu, which will appear when we hover over the item.
 *
 * In the example below, we'll create a much simpler ````ContextMenu```` that has only one item, called "Effects", which
 * will open a cascading sub-menu whenever we hover over that item.
 *
 * Note that our "Effects" item has no ````doAction```` callback, because an item with a sub-menu performs no
 * action of its own.
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#ContextMenu_subMenus)]
 *
 * ````javascript
 * const canvasContextMenu = new ContextMenu({
 *     items: [ // Top level items
 *         [
 *             {
 *                 getTitle: (context) => {
 *                     return "Effects";
 *                 },
 *
 *                 items: [ // Sub-menu
 *                     [
 *                         {
 *                             getTitle: (context) => {
 *                                 return (!context.entity.visible) ? "Show" : "Hide";
 *                             },
 *                             doAction: function (context) {
 *                                 context.entity.visible = !context.entity.visible;
 *                             }
 *                         },
 *                         {
 *                             getTitle: (context) => {
 *                                 return (!context.entity.selected) ? "Select" : "Undo Select";
 *                             },
 *                             doAction: function (context) {
 *                                 context.entity.selected = !context.entity.selected;
 *                             }
 *                         },
 *                         {
 *                             getTitle: (context) => {
 *                                 return (!context.entity.highlighted) ? "Highlight" : "Undo Highlight";
 *                             },
 *                             doAction: function (context) {
 *                                 context.entity.highlighted = !context.entity.highlighted;
 *                             }
 *                         }
 *                     ]
 *                 ]
 *             }
 *          ]
 *      ]
 * });
 * ````
 */
class ContextMenu {

    /**
     * Creates a ````ContextMenu````.
     *
     * The ````ContextMenu```` will be initially hidden.
     *
     * @param {Object} [cfg] ````ContextMenu```` configuration.
     * @param {Object} [cfg.items] The context menu items. These can also be dynamically set on {@link ContextMenu#items}. See the class documentation for an example.
     * @param {Object} [cfg.context] The context, which is passed into the item callbacks. This can also be dynamically set on {@link ContextMenu#context}. This must be set before calling {@link ContextMenu#show}.
     * @param {Boolean} [cfg.enabled=true] Whether this ````ContextMenu```` is initially enabled. {@link ContextMenu#show} does nothing while this is ````false````.
     * @param {Boolean} [cfg.hideOnMouseDown=true] Whether this ````ContextMenu```` automatically hides whenever we mouse-down or tap anywhere in the page.
     * @param {Boolean} [cfg.hideOnAction=true] Whether this ````ContextMenu```` automatically hides after we select a menu item. Se false if we want the menu to remain shown and show any updates to its item titles, after we've selected an item.
     * @param {Node | undefined} [cfg.parentNode] Optional reference to an existing DOM Node (e.g. ShadowRoot), to which the menu's HTML element will be appended, defaults to ````document.body````.
     */
    constructor(cfg = {}) {

        this._id = idMap.addItem();
        this._context = null;
        this._enabled = false;  // True when the ContextMenu is enabled
        this._itemsCfg = [];    // Items as given as configs
        this._rootMenu = null;  // The root Menu in the tree
        this._menuList = [];    // List of Menus
        this._menuMap = {};     // Menus mapped to their IDs
        this._itemList = [];    // List of Items
        this._itemMap = {};     // Items mapped to their IDs
        this._shown = false;    // True when the ContextMenu is visible
        this._nextId = 0;
        this._parentNode = cfg.parentNode || document.body;
        this._offsetParent = (this._parentNode instanceof ShadowRoot) ? this._parentNode.host : this._parentNode;
        
        /**
         * Subscriptions to events fired at this ContextMenu.
         * @private
         */
        this._eventSubs = {};

        if (cfg.hideOnMouseDown !== false) {
            this._parentNode.addEventListener("mousedown", (event) => {
                if (!event.target.classList.contains("xeokit-context-menu-item")) {
                    this.hide();
                }
            });
            this._parentNode.addEventListener("touchstart", this._canvasTouchStartHandler = (event) => {
                if (!event.target.classList.contains("xeokit-context-menu-item")) {
                    this.hide();
                }
            });
        }

        if (cfg.items) {
            this.items = cfg.items;
        }

        this._hideOnAction = (cfg.hideOnAction !== false);

        this.context = cfg.context;
        this.enabled = cfg.enabled !== false;
        this.hide();
    }


    /**
     Subscribes to an event fired at this ````ContextMenu````.

     @param {String} event The event
     @param {Function} callback Callback fired on the event
     */
    on(event, callback) {
        let subs = this._eventSubs[event];
        if (!subs) {
            subs = [];
            this._eventSubs[event] = subs;
        }
        subs.push(callback);
    }

    /**
     Fires an event at this ````ContextMenu````.

     @param {String} event The event type name
     @param {Object} value The event parameters
     */
    fire(event, value) {
        const subs = this._eventSubs[event];
        if (subs) {
            for (let i = 0, len = subs.length; i < len; i++) {
                subs[i](value);
            }
        }
    }

    /**
     * Sets the ````ContextMenu```` items.
     *
     * These can be updated dynamically at any time.
     *
     * See class documentation for an example.
     *
     * @type {Object[]}
     */
    set items(itemsCfg) {
        this._clear();
        this._itemsCfg = itemsCfg || [];
        this._parseItems(itemsCfg);
        this._createUI();
    }

    /**
     * Gets the ````ContextMenu```` items.
     *
     * @type {Object[]}
     */
    get items() {
        return this._itemsCfg;
    }

    /**
     * Sets whether this ````ContextMenu```` is enabled.
     *
     * Hides the menu when disabling.
     *
     * @type {Boolean}
     */
    set enabled(enabled) {
        enabled = (!!enabled);
        if (enabled === this._enabled) {
            return;
        }
        this._enabled = enabled;
        if (!this._enabled) {
            this.hide();
        }
    }

    /**
     * Gets whether this ````ContextMenu```` is enabled.
     *
     * {@link ContextMenu#show} does nothing while this is ````false````.
     *
     * @type {Boolean}
     */
    get enabled() {
        return this._enabled;
    }

    /**
     * Sets the ````ContextMenu```` context.
     *
     * The context can be any object that you need to be provides to the callbacks configured on {@link ContextMenu#items}.
     *
     * This must be set before calling {@link ContextMenu#show}.
     *
     * @type {Object}
     */
    set context(context) {
        this._context = context;
    }

    /**
     * Gets the ````ContextMenu```` context.
     *
     * @type {Object}
     */
    get context() {
        return this._context;
    }

    /**
     * Shows this ````ContextMenu```` at the given page coordinates.
     *
     * Does nothing when {@link ContextMenu#enabled} is ````false````.
     *
     * Logs error to console and does nothing if {@link ContextMenu#context} has not been set.
     *
     * Fires a "shown" event when shown.
     *
     * @param {Number} pageX Page X-coordinate.
     * @param {Number} pageY Page Y-coordinate.
     */
    show(pageX, pageY) {
        if (!this._context) {
            console.error("ContextMenu cannot be shown without a context - set context first");
            return;
        }
        if (!this._enabled) {
            return;
        }
        if (this._shown) {
            return;
        }
        this._hideAllMenus();
        this._updateItemsTitles();
        this._updateItemsEnabledStatus();
        this._showMenu(this._rootMenu.id, pageX, pageY);
        this._updateSubMenuInfo();
        this._shown = true;
        this.fire("shown", {});
    }

    /**
     * Gets whether this ````ContextMenu```` is currently shown or not.
     *
     * @returns {Boolean} Whether this ````ContextMenu```` is shown.
     */
    get shown() {
        return this._shown;
    }

    /**
     * Hides this ````ContextMenu````.
     *
     * Fires a "hidden" event when hidden.
     */
    hide() {
        if (!this._enabled) {
            return;
        }
        if (!this._shown) {
            return;
        }
        this._hideAllMenus();
        this._shown = false;
        this.fire("hidden", {});
    }

    /**
     * Destroys this ````ContextMenu````.
     */
    destroy() {
        this._context = null;
        this._clear();
        if (this._id !== null) {
            idMap.removeItem(this._id);
            this._id = null;
        }
    }

    _clear() { // Destroys DOM elements, clears menu data
        for (let i = 0, len = this._menuList.length; i < len; i++) {
            const menu = this._menuList[i];
            const menuElement = menu.menuElement;
            menuElement.remove();
        }
        this._itemsCfg = [];
        this._rootMenu = null;
        this._menuList = [];
        this._menuMap = {};
        this._itemList = [];
        this._itemMap = {};
    }

    _parseItems(itemsCfg) { // Parses "items" config into menu data

        const visitItems = (itemsCfg) => {

            const menuId = this._getNextId();
            const menu = new Menu(menuId);

            for (let i = 0, len = itemsCfg.length; i < len; i++) {

                const itemsGroupCfg = itemsCfg[i];

                const group = new Group();

                menu.groups.push(group);

                for (let j = 0, lenj = itemsGroupCfg.length; j < lenj; j++) {

                    const itemCfg = itemsGroupCfg[j];
                    const subItemsCfg = itemCfg.items;
                    const hasSubItems = (subItemsCfg && (subItemsCfg.length > 0));
                    const itemId = this._getNextId();

                    const getTitle = itemCfg.getTitle || (() => {
                        return (itemCfg.title || "");
                    });

                    const doAction = itemCfg.doAction || itemCfg.callback || (() => {
                    });

                    const getEnabled = itemCfg.getEnabled || (() => {
                        return true;
                    });

                    const getShown = itemCfg.getShown || (() => {
                        return true;
                    });

                    const item = new Item(itemId, getTitle, doAction, getEnabled, getShown);

                    item.parentMenu = menu;

                    group.items.push(item);

                    if (hasSubItems) {
                        const subMenu = visitItems(subItemsCfg);
                        item.subMenu = subMenu;
                        subMenu.parentItem = item;
                    }

                    this._itemList.push(item);
                    this._itemMap[item.id] = item;
                }
            }

            this._menuList.push(menu);
            this._menuMap[menu.id] = menu;

            return menu;
        };

        this._rootMenu = visitItems(itemsCfg);
    }

    _getNextId() { // Returns a unique ID
        return "ContextMenu_" + this._id + "_" + this._nextId++; // Start ID with alpha chars to make a valid DOM element selector
    }

    _createUI() { // Builds DOM elements for the entire menu tree

        const visitMenu = (menu) => {

            this._createMenuUI(menu);

            const groups = menu.groups;
            for (let i = 0, len = groups.length; i < len; i++) {
                const group = groups[i];
                const groupItems = group.items;
                for (let j = 0, lenj = groupItems.length; j < lenj; j++) {
                    const item = groupItems[j];
                    const subMenu = item.subMenu;
                    if (subMenu) {
                        visitMenu(subMenu);
                    }
                }
            }
        };

        visitMenu(this._rootMenu);
    }

    _createMenuUI(menu) { // Builds DOM elements for a menu

        const groups = menu.groups;
        const html = [];

        const menuElement= document.createElement("div");
        menuElement.classList.add("xeokit-context-menu", menu.id);
        menuElement.style.zIndex = 300000;
        menuElement.style.position = "absolute";

        html.push('<ul>');

        if (groups) {

            for (let i = 0, len = groups.length; i < len; i++) {

                const group = groups[i];
                const groupIdx = i;
                const groupLen = len;
                const groupItems = group.items;

                if (groupItems) {

                    for (let j = 0, lenj = groupItems.length; j < lenj; j++) {

                        const item = groupItems[j];
                        const itemSubMenu = item.subMenu;
                        const actionTitle = item.title || "";

                        if (itemSubMenu) {

                            html.push(
                                '<li id="' + item.id + '" class="xeokit-context-menu-item xeokit-context-menu-submenu">' +
                                actionTitle +
                                '</li>');
                            if (!((groupIdx === groupLen - 1) || (j < lenj - 1))) {
                                html.push(
                                    '<li id="' + item.id + '" class="xeokit-context-menu-item-separator"></li>'
                                );
                            }

                        } else {

                            html.push(
                                '<li id="' + item.id + '" class="xeokit-context-menu-item">' +
                                actionTitle +
                                '</li>');
                            if (!((groupIdx === groupLen - 1) || (j < lenj - 1))) {
                                html.push(
                                    '<li id="' + item.id + '" class="xeokit-context-menu-item-separator"></li>'
                                );
                            }
                        }
                    }
                }
            }
        }

        html.push('</ul>');

        const htmlString = html.join("");
        menuElement.innerHTML = htmlString;
        this._parentNode.appendChild(menuElement);

        menu.menuElement = menuElement;

        menuElement.style["border-radius"] = 4 + "px";
        menuElement.style.display = 'none';
        menuElement.style["z-index"] = 300000;
        menuElement.style.background = "white";
        menuElement.style.border = "1px solid black";
        menuElement.style["box-shadow"] = "0 4px 5px 0 gray";
        menuElement.oncontextmenu = (e) => {
            e.preventDefault();
        };

        // Bind event handlers

        const self = this;

        let lastSubMenu = null;

        if (groups) {

            for (let i = 0, len = groups.length; i < len; i++) {

                const group = groups[i];
                const groupItems = group.items;

                if (groupItems) {

                    for (let j = 0, lenj = groupItems.length; j < lenj; j++) {

                        const item = groupItems[j];
                        const itemSubMenu = item.subMenu;

                        item.itemElement = this._parentNode.querySelector(`#${item.id}`);

                        if (!item.itemElement) {
                            console.error("ContextMenu item element not found: " + item.id);
                            continue;
                        }

                        item.itemElement.addEventListener("mouseenter", (event) => {
                            event.preventDefault();

                            const subMenu = item.subMenu;
                            if (!subMenu) {
                                if (lastSubMenu) {
                                    self._hideMenu(lastSubMenu.id);
                                    lastSubMenu = null;
                                }
                                return;
                            }
                            if (lastSubMenu && (lastSubMenu.id !== subMenu.id)) {
                                self._hideMenu(lastSubMenu.id);
                                lastSubMenu = null;
                            }

                            if (item.enabled === false) {
                                return;
                            }

                            const itemElement = item.itemElement;
                            const subMenuElement = subMenu.menuElement;

                            const itemRect = itemElement.getBoundingClientRect();
                            subMenuElement.getBoundingClientRect();
                            const offsetRect = self._offsetParent.getBoundingClientRect();

                            const subMenuWidth = 200; // TODO
                            const showOnRight = (itemRect.right + subMenuWidth) < offsetRect.right;
                            const showOnLeft = (itemRect.left - subMenuWidth) > offsetRect.left;
                        
                            if(showOnRight)
                                self._showMenu(subMenu.id, itemRect.right + window.scrollX - 5, itemRect.top + window.scrollY - 1);
                            else if (showOnLeft)
                                self._showMenu(subMenu.id, itemRect.left - subMenuWidth + window.scrollX, itemRect.top + window.scrollY - 1);
                            else {
                                const spaceOnLeft = itemRect.left - offsetRect.left, spaceOnRight = offsetRect.right - itemRect.right;
                                if(spaceOnRight > spaceOnLeft) 
                                    self._showMenu(subMenu.id, itemRect.right - 5 - (subMenuWidth - spaceOnRight), itemRect.top + window.scrollY - 1);
                                else 
                                    self._showMenu(subMenu.id, itemRect.left - spaceOnLeft, itemRect.top + window.scrollY - 1);
                            }

                            lastSubMenu = subMenu;
                        });

                        if (!itemSubMenu) {

                            // Item without sub-menu
                            // clicking item fires the item's action callback

                            item.itemElement.addEventListener("click", (event) => {
                                event.preventDefault();
                                if (!self._context) {
                                    return;
                                }
                                if (item.enabled === false) {
                                    return;
                                }
                                if (item.doAction) {
                                    item.doAction(self._context);
                                }
                                if (this._hideOnAction) {
                                    self.hide();
                                } else {
                                    self._updateItemsTitles();
                                    self._updateItemsEnabledStatus();
                                }
                            });
                            item.itemElement.addEventListener("mouseup", (event) => {
                                if (event.which !== 3) {
                                    return;
                                }
                                event.preventDefault();
                                if (!self._context) {
                                    return;
                                }
                                if (item.enabled === false) {
                                    return;
                                }
                                if (item.doAction) {
                                    item.doAction(self._context);
                                }
                                if (this._hideOnAction) {
                                    self.hide();
                                } else {
                                    self._updateItemsTitles();
                                    self._updateItemsEnabledStatus();
                                }
                            });
                            item.itemElement.addEventListener("mouseenter", (event) => {
                                event.preventDefault();
                                if (item.enabled === false) {
                                    return;
                                }
                                if (item.doHover) {
                                    item.doHover(self._context);
                                }
                            });

                        }
                    }
                }
            }
        }
    }

    _updateItemsTitles() { // Dynamically updates the title of each Item to the result of Item#getTitle()
        if (!this._context) {
            return;
        }
        for (let i = 0, len = this._itemList.length; i < len; i++) {
            const item = this._itemList[i];
            const itemElement = item.itemElement;
            if (!itemElement) {
                continue;
            }
            const getShown = item.getShown;
            if (!getShown || !getShown(this._context)) {
                continue;
            }
            const title = item.getTitle(this._context);
            if (item.subMenu) {
                itemElement.innerText = title;
            } else {
                itemElement.innerText = title;
            }
        }
    }

    _updateItemsEnabledStatus() { // Enables or disables each Item, depending on the result of Item#getEnabled()
        if (!this._context) {
            return;
        }
        for (let i = 0, len = this._itemList.length; i < len; i++) {
            const item = this._itemList[i];
            const itemElement = item.itemElement;
            if (!itemElement) {
                continue;
            }
            const getEnabled = item.getEnabled;
            if (!getEnabled) {
                continue;
            }
            const getShown = item.getShown;
            if (!getShown) {
                continue;
            }
            const shown = getShown(this._context);
            item.shown = shown;
            if (!shown) {
                itemElement.style.display = "none";
                continue;
            } else {
                itemElement.style.display = "";

            }
            const enabled = getEnabled(this._context);
            item.enabled = enabled;
            if (!enabled) {
                itemElement.classList.add("disabled");
            } else {
                itemElement.classList.remove("disabled");
            }
        }
    }

    _updateSubMenuInfo() {
        if (!this._context) return;
        let itemElement, itemRect, subMenuElement, initialStyles, showOnLeft, subMenuWidth;
        this._itemList.forEach((item) => {
            if (item.subMenu) {
                itemElement = item.itemElement;
                itemRect = itemElement.getBoundingClientRect();
                subMenuElement = item.subMenu.menuElement;
                initialStyles = {
                    visibility: subMenuElement.style.visibility,
                    display: subMenuElement.style.display,
                };
                subMenuElement.style.display = "block";
                subMenuElement.style.visibility = "hidden";
                subMenuWidth = item.subMenu.menuElement.getBoundingClientRect().width;
                subMenuElement.style.visibility = initialStyles.visibility;
                subMenuElement.style.display = initialStyles.display;
                showOnLeft = ((itemRect.right + subMenuWidth) > window.innerWidth);
                itemElement.setAttribute("data-submenuposition", showOnLeft ? "left" : "right");
            }
        });
    }

    _showMenu(menuId, pageX, pageY) { // Shows the given menu, at the specified page coordinates
        const menu = this._menuMap[menuId];
        if (!menu) {
            console.error("Menu not found: " + menuId);
            return;
        }
        if (menu.shown) {
            return;
        }
        const menuElement = menu.menuElement;
        if (menuElement) {
            this._showMenuElement(menuElement, pageX, pageY);
            menu.shown = true;
        }
    }

    _hideMenu(menuId) { // Hides the given menu
        const menu = this._menuMap[menuId];
        if (!menu) {
            console.error("Menu not found: " + menuId);
            return;
        }
        if (!menu.shown) {
            return;
        }
        const menuElement = menu.menuElement;
        if (menuElement) {
            this._hideMenuElement(menuElement);
            menu.shown = false;
        }
    }

    _hideAllMenus() {
        for (let i = 0, len = this._menuList.length; i < len; i++) {
            const menu = this._menuList[i];
            this._hideMenu(menu.id);
        }
    }

    _showMenuElement(menuElement, pageX, pageY) { // Shows the given menu element, at the specified page coordinates
        menuElement.style.display = 'block';
        const menuHeight = menuElement.offsetHeight;
        const menuWidth = menuElement.offsetWidth;

        const offsetRect = this._offsetParent.getBoundingClientRect();
        
        const bottomContainerBorder = offsetRect.bottom + window.scrollY;
        const rightContainerBorder = offsetRect.right + window.scrollX;
        if ((pageY + menuHeight) > bottomContainerBorder) {
            pageY = bottomContainerBorder- menuHeight;
        }
        if ((pageX + menuWidth) > rightContainerBorder) {
            pageX = rightContainerBorder - menuWidth;
        }


        menuElement.style.left = pageX - offsetRect.left - window.scrollX + 'px';
        menuElement.style.top = pageY - offsetRect.top - window.scrollY + 'px';
    }

    _hideMenuElement(menuElement) {
        menuElement.style.display = 'none';
    }
}

// Some temporary vars to help avoid garbage collection

let doublePrecision = true;
let FloatArrayType = doublePrecision ? Float64Array : Float32Array;

const tempVec3a$P = new FloatArrayType(3);

const tempMat1 = new FloatArrayType(16);
const tempMat2 = new FloatArrayType(16);
const tempVec4$2 = new FloatArrayType(4);


/**
 * @private
 */
const math = {

    setDoublePrecisionEnabled(enable) {
        doublePrecision = enable;
        FloatArrayType = doublePrecision ? Float64Array : Float32Array;
    },

    getDoublePrecisionEnabled() {
        return doublePrecision;
    },

    MIN_DOUBLE: -Number.MAX_SAFE_INTEGER,
    MAX_DOUBLE: Number.MAX_SAFE_INTEGER,

    MAX_INT: 10000000,

    /**
     * The number of radiians in a degree (0.0174532925).
     * @property DEGTORAD
     * @type {Number}
     */
    DEGTORAD: 0.0174532925,

    /**
     * The number of degrees in a radian.
     * @property RADTODEG
     * @type {Number}
     */
    RADTODEG: 57.295779513,

    unglobalizeObjectId(modelId, globalId) {
        const idx = globalId.indexOf("#");
        return (idx === modelId.length && globalId.startsWith(modelId)) ? globalId.substring(idx + 1) : globalId;
    },

    globalizeObjectId(modelId, objectId) {
        return (modelId + "#" + objectId)
    },

    /**
     * Returns:
     * - x != 0 => 1/x,
     * - x == 1 => 1
     *
     * @param {number} x
     */
    safeInv(x) {
        const retVal = 1 / x;
        if (isNaN(retVal) || !isFinite(retVal)) {
            return 1;
        }
        return retVal;
    },

    /**
     * Returns a new, uninitialized two-element vector.
     * @method vec2
     * @param [values] Initial values.
     * @static
     * @returns {Number[]}
     */
    vec2(values) {
        return new FloatArrayType(values || 2);
    },

    /**
     * Returns a new, uninitialized three-element vector.
     * @method vec3
     * @param [values] Initial values.
     * @static
     * @returns {Number[]}
     */
    vec3(values) {
        return new FloatArrayType(values || 3);
    },

    /**
     * Returns a new, uninitialized four-element vector.
     * @method vec4
     * @param [values] Initial values.
     * @static
     * @returns {Number[]}
     */
    vec4(values) {
        return new FloatArrayType(values || 4);
    },

    /**
     * Returns a new, uninitialized 3x3 matrix.
     * @method mat3
     * @param [values] Initial values.
     * @static
     * @returns {Number[]}
     */
    mat3(values) {
        return new FloatArrayType(values || 9);
    },

    /**
     * Converts a 3x3 matrix to 4x4
     * @method mat3ToMat4
     * @param mat3 3x3 matrix.
     * @param mat4 4x4 matrix
     * @static
     * @returns {Number[]}
     */
    mat3ToMat4(mat3, mat4 = new FloatArrayType(16)) {
        mat4[0] = mat3[0];
        mat4[1] = mat3[1];
        mat4[2] = mat3[2];
        mat4[3] = 0;
        mat4[4] = mat3[3];
        mat4[5] = mat3[4];
        mat4[6] = mat3[5];
        mat4[7] = 0;
        mat4[8] = mat3[6];
        mat4[9] = mat3[7];
        mat4[10] = mat3[8];
        mat4[11] = 0;
        mat4[12] = 0;
        mat4[13] = 0;
        mat4[14] = 0;
        mat4[15] = 1;
        return mat4;
    },

    /**
     * Returns a new, uninitialized 4x4 matrix.
     * @method mat4
     * @param [values] Initial values.
     * @static
     * @returns {Number[]}
     */
    mat4(values) {
        return new FloatArrayType(values || 16);
    },

    /**
     * Converts a 4x4 matrix to 3x3
     * @method mat4ToMat3
     * @param mat4 4x4 matrix.
     * @param mat3 3x3 matrix
     * @static
     * @returns {Number[]}
     */
    mat4ToMat3(mat4, mat3 = new FloatArrayType(9)) {
        mat3[0] = mat4[0];
        mat3[1] = mat4[1];
        mat3[2] = mat4[2];
        
        mat3[3] = mat4[4];
        mat3[4] = mat4[5];
        mat3[5] = mat4[6];
        
        mat3[6] = mat4[8];
        mat3[7] = mat4[9];
        mat3[8] = mat4[10];
        
        return mat3;
    },

    /**
     * Converts a list of double-precision values to a list of high-part floats and a list of low-part floats.
     * @param doubleVals
     * @param floatValsHigh
     * @param floatValsLow
     */
    doublesToFloats(doubleVals, floatValsHigh, floatValsLow) {
        const floatPair = new FloatArrayType(2);
        for (let i = 0, len = doubleVals.length; i < len; i++) {
            math.splitDouble(doubleVals[i], floatPair);
            floatValsHigh[i] = floatPair[0];
            floatValsLow[i] = floatPair[1];
        }
    },

    /**
     * Splits a double value into two floats.
     * @param value
     * @param floatPair
     */
    splitDouble(value, floatPair) {
        const hi = FloatArrayType.from([value])[0];
        const low = value - hi;
        floatPair[0] = hi;
        floatPair[1] = low;
    },

    /**
     * Returns a new UUID.
     * @method createUUID
     * @static
     * @return string The new UUID
     */
    createUUID: ((() => {
        const lut = [];
        for (let i = 0; i < 256; i++) {
            lut[i] = (i < 16 ? '0' : '') + (i).toString(16);
        }
        return () => {
            const d0 = Math.random() * 0xffffffff | 0;
            const d1 = Math.random() * 0xffffffff | 0;
            const d2 = Math.random() * 0xffffffff | 0;
            const d3 = Math.random() * 0xffffffff | 0;
            return `${lut[d0 & 0xff] + lut[d0 >> 8 & 0xff] + lut[d0 >> 16 & 0xff] + lut[d0 >> 24 & 0xff]}-${lut[d1 & 0xff]}${lut[d1 >> 8 & 0xff]}-${lut[d1 >> 16 & 0x0f | 0x40]}${lut[d1 >> 24 & 0xff]}-${lut[d2 & 0x3f | 0x80]}${lut[d2 >> 8 & 0xff]}-${lut[d2 >> 16 & 0xff]}${lut[d2 >> 24 & 0xff]}${lut[d3 & 0xff]}${lut[d3 >> 8 & 0xff]}${lut[d3 >> 16 & 0xff]}${lut[d3 >> 24 & 0xff]}`;
        };
    }))(),

    /**
     * Clamps a value to the given range.
     * @param {Number} value Value to clamp.
     * @param {Number} min Lower bound.
     * @param {Number} max Upper bound.
     * @returns {Number} Clamped result.
     */
    clamp(value, min, max) {
        return Math.max(min, Math.min(max, value));
    },

    /**
     * Floating-point modulus
     * @method fmod
     * @static
     * @param {Number} a
     * @param {Number} b
     * @returns {*}
     */
    fmod(a, b) {
        if (a < b) {
            console.error("math.fmod : Attempting to find modulus within negative range - would be infinite loop - ignoring");
            return a;
        }
        while (b <= a) {
            a -= b;
        }
        return a;
    },

    /**
     * Returns true if the two 3-element vectors are the same.
     * @param v1
     * @param v2
     * @returns {Boolean}
     */
    compareVec3(v1, v2) {
        return (v1[0] === v2[0] && v1[1] === v2[1] && v1[2] === v2[2]);
    },

    /**
     * Returns true if the two 4-element vectors are the same.
     * @param v1
     * @param v2
     * @returns {Boolean}
     */
    compareVec4(v1, v2) {
        return (v1[0] === v2[0] && v1[1] === v2[1] && v1[2] === v2[2] && v1[3] === v2[3]);
    },

    /**
     * Negates a three-element vector.
     * @method negateVec3
     * @static
     * @param {Array(Number)} v Vector to negate
     * @param  {Array(Number)} [dest] Destination vector
     * @return {Array(Number)} dest if specified, v otherwise
     */
    negateVec3(v, dest) {
        if (!dest) {
            dest = v;
        }
        dest[0] = -v[0];
        dest[1] = -v[1];
        dest[2] = -v[2];
        return dest;
    },

    /**
     * Negates a four-element vector.
     * @method negateVec4
     * @static
     * @param {Array(Number)} v Vector to negate
     * @param  {Array(Number)} [dest] Destination vector
     * @return {Array(Number)} dest if specified, v otherwise
     */
    negateVec4(v, dest) {
        if (!dest) {
            dest = v;
        }
        dest[0] = -v[0];
        dest[1] = -v[1];
        dest[2] = -v[2];
        dest[3] = -v[3];
        return dest;
    },

    /**
     * Adds one four-element vector to another.
     * @method addVec4
     * @static
     * @param {Array(Number)} u First vector
     * @param {Array(Number)} v Second vector
     * @param  {Array(Number)} [dest] Destination vector
     * @return {Array(Number)} dest if specified, u otherwise
     */
    addVec4(u, v, dest) {
        if (!dest) {
            dest = u;
        }
        dest[0] = u[0] + v[0];
        dest[1] = u[1] + v[1];
        dest[2] = u[2] + v[2];
        dest[3] = u[3] + v[3];
        return dest;
    },

    /**
     * Adds a scalar value to each element of a four-element vector.
     * @method addVec4Scalar
     * @static
     * @param {Array(Number)} v The vector
     * @param {Number} s The scalar
     * @param  {Array(Number)} [dest] Destination vector
     * @return {Array(Number)} dest if specified, v otherwise
     */
    addVec4Scalar(v, s, dest) {
        if (!dest) {
            dest = v;
        }
        dest[0] = v[0] + s;
        dest[1] = v[1] + s;
        dest[2] = v[2] + s;
        dest[3] = v[3] + s;
        return dest;
    },

    /**
     * Adds one three-element vector to another.
     * @method addVec3
     * @static
     * @param {Array(Number)} u First vector
     * @param {Array(Number)} v Second vector
     * @param  {Array(Number)} [dest] Destination vector
     * @return {Array(Number)} dest if specified, u otherwise
     */
    addVec3(u, v, dest) {
        if (!dest) {
            dest = u;
        }
        dest[0] = u[0] + v[0];
        dest[1] = u[1] + v[1];
        dest[2] = u[2] + v[2];
        return dest;
    },

    /**
     * Adds a scalar value to each element of a three-element vector.
     * @method addVec4Scalar
     * @static
     * @param {Array(Number)} v The vector
     * @param {Number} s The scalar
     * @param  {Array(Number)} [dest] Destination vector
     * @return {Array(Number)} dest if specified, v otherwise
     */
    addVec3Scalar(v, s, dest) {
        if (!dest) {
            dest = v;
        }
        dest[0] = v[0] + s;
        dest[1] = v[1] + s;
        dest[2] = v[2] + s;
        return dest;
    },

    /**
     * Subtracts one four-element vector from another.
     * @method subVec4
     * @static
     * @param {Array(Number)} u First vector
     * @param {Array(Number)} v Vector to subtract
     * @param  {Array(Number)} [dest] Destination vector
     * @return {Array(Number)} dest if specified, u otherwise
     */
    subVec4(u, v, dest) {
        if (!dest) {
            dest = u;
        }
        dest[0] = u[0] - v[0];
        dest[1] = u[1] - v[1];
        dest[2] = u[2] - v[2];
        dest[3] = u[3] - v[3];
        return dest;
    },

    /**
     * Subtracts one three-element vector from another.
     * @method subVec3
     * @static
     * @param {Array(Number)} u First vector
     * @param {Array(Number)} v Vector to subtract
     * @param  {Array(Number)} [dest] Destination vector
     * @return {Array(Number)} dest if specified, u otherwise
     */
    subVec3(u, v, dest) {
        if (!dest) {
            dest = u;
        }
        dest[0] = u[0] - v[0];
        dest[1] = u[1] - v[1];
        dest[2] = u[2] - v[2];
        return dest;
    },

    /**
     * Subtracts one two-element vector from another.
     * @method subVec2
     * @static
     * @param {Array(Number)} u First vector
     * @param {Array(Number)} v Vector to subtract
     * @param  {Array(Number)} [dest] Destination vector
     * @return {Array(Number)} dest if specified, u otherwise
     */
    subVec2(u, v, dest) {
        if (!dest) {
            dest = u;
        }
        dest[0] = u[0] - v[0];
        dest[1] = u[1] - v[1];
        return dest;
    },

    /**
     * Get the geometric mean of the vectors.
     * @method geometricMeanVec2
     * @static
     * @param {...Array(Number)} vectors Vec2 to mean
     * @return {Array(Number)} The geometric mean vec2
     */
    geometricMeanVec2(...vectors) {
        const geometricMean = new FloatArrayType(vectors[0]);
        for (let i = 1; i < vectors.length; i++) {
            geometricMean[0] += vectors[i][0];
            geometricMean[1] += vectors[i][1];
        }
        geometricMean[0] /= vectors.length;
        geometricMean[1] /= vectors.length;
        return geometricMean;
    },

    /**
     * Subtracts a scalar value from each element of a four-element vector.
     * @method subVec4Scalar
     * @static
     * @param {Array(Number)} v The vector
     * @param {Number} s The scalar
     * @param  {Array(Number)} [dest] Destination vector
     * @return {Array(Number)} dest if specified, v otherwise
     */
    subVec4Scalar(v, s, dest) {
        if (!dest) {
            dest = v;
        }
        dest[0] = v[0] - s;
        dest[1] = v[1] - s;
        dest[2] = v[2] - s;
        dest[3] = v[3] - s;
        return dest;
    },

    /**
     * Sets each element of a 4-element vector to a scalar value minus the value of that element.
     * @method subScalarVec4
     * @static
     * @param {Array(Number)} v The vector
     * @param {Number} s The scalar
     * @param  {Array(Number)} [dest] Destination vector
     * @return {Array(Number)} dest if specified, v otherwise
     */
    subScalarVec4(v, s, dest) {
        if (!dest) {
            dest = v;
        }
        dest[0] = s - v[0];
        dest[1] = s - v[1];
        dest[2] = s - v[2];
        dest[3] = s - v[3];
        return dest;
    },

    /**
     * Multiplies one three-element vector by another.
     * @method mulVec3
     * @static
     * @param {Array(Number)} u First vector
     * @param {Array(Number)} v Second vector
     * @param  {Array(Number)} [dest] Destination vector
     * @return {Array(Number)} dest if specified, u otherwise
     */
    mulVec4(u, v, dest) {
        if (!dest) {
            dest = u;
        }
        dest[0] = u[0] * v[0];
        dest[1] = u[1] * v[1];
        dest[2] = u[2] * v[2];
        dest[3] = u[3] * v[3];
        return dest;
    },

    /**
     * Multiplies each element of a four-element vector by a scalar.
     * @method mulVec34calar
     * @static
     * @param {Array(Number)} v The vector
     * @param {Number} s The scalar
     * @param  {Array(Number)} [dest] Destination vector
     * @return {Array(Number)} dest if specified, v otherwise
     */
    mulVec4Scalar(v, s, dest) {
        if (!dest) {
            dest = v;
        }
        dest[0] = v[0] * s;
        dest[1] = v[1] * s;
        dest[2] = v[2] * s;
        dest[3] = v[3] * s;
        return dest;
    },

    /**
     * Multiplies each element of a three-element vector by a scalar.
     * @method mulVec3Scalar
     * @static
     * @param {Array(Number)} v The vector
     * @param {Number} s The scalar
     * @param  {Array(Number)} [dest] Destination vector
     * @return {Array(Number)} dest if specified, v otherwise
     */
    mulVec3Scalar(v, s, dest) {
        if (!dest) {
            dest = v;
        }
        dest[0] = v[0] * s;
        dest[1] = v[1] * s;
        dest[2] = v[2] * s;
        return dest;
    },

    /**
     * Multiplies each element of a two-element vector by a scalar.
     * @method mulVec2Scalar
     * @static
     * @param {Array(Number)} v The vector
     * @param {Number} s The scalar
     * @param  {Array(Number)} [dest] Destination vector
     * @return {Array(Number)} dest if specified, v otherwise
     */
    mulVec2Scalar(v, s, dest) {
        if (!dest) {
            dest = v;
        }
        dest[0] = v[0] * s;
        dest[1] = v[1] * s;
        return dest;
    },

    /**
     * Divides one three-element vector by another.
     * @method divVec3
     * @static
     * @param {Array(Number)} u First vector
     * @param {Array(Number)} v Second vector
     * @param  {Array(Number)} [dest] Destination vector
     * @return {Array(Number)} dest if specified, u otherwise
     */
    divVec3(u, v, dest) {
        if (!dest) {
            dest = u;
        }
        dest[0] = u[0] / v[0];
        dest[1] = u[1] / v[1];
        dest[2] = u[2] / v[2];
        return dest;
    },

    /**
     * Divides one four-element vector by another.
     * @method divVec4
     * @static
     * @param {Array(Number)} u First vector
     * @param {Array(Number)} v Second vector
     * @param  {Array(Number)} [dest] Destination vector
     * @return {Array(Number)} dest if specified, u otherwise
     */
    divVec4(u, v, dest) {
        if (!dest) {
            dest = u;
        }
        dest[0] = u[0] / v[0];
        dest[1] = u[1] / v[1];
        dest[2] = u[2] / v[2];
        dest[3] = u[3] / v[3];
        return dest;
    },

    /**
     * Divides a scalar by a three-element vector, returning a new vector.
     * @method divScalarVec3
     * @static
     * @param v vec3
     * @param s scalar
     * @param dest vec3 - optional destination
     * @return [] dest if specified, v otherwise
     */
    divScalarVec3(s, v, dest) {
        if (!dest) {
            dest = v;
        }
        dest[0] = s / v[0];
        dest[1] = s / v[1];
        dest[2] = s / v[2];
        return dest;
    },

    /**
     * Divides a three-element vector by a scalar.
     * @method divVec3Scalar
     * @static
     * @param v vec3
     * @param s scalar
     * @param dest vec3 - optional destination
     * @return [] dest if specified, v otherwise
     */
    divVec3Scalar(v, s, dest) {
        if (!dest) {
            dest = v;
        }
        dest[0] = v[0] / s;
        dest[1] = v[1] / s;
        dest[2] = v[2] / s;
        return dest;
    },

    /**
     * Divides a four-element vector by a scalar.
     * @method divVec4Scalar
     * @static
     * @param v vec4
     * @param s scalar
     * @param dest vec4 - optional destination
     * @return [] dest if specified, v otherwise
     */
    divVec4Scalar(v, s, dest) {
        if (!dest) {
            dest = v;
        }
        dest[0] = v[0] / s;
        dest[1] = v[1] / s;
        dest[2] = v[2] / s;
        dest[3] = v[3] / s;
        return dest;
    },


    /**
     * Divides a scalar by a four-element vector, returning a new vector.
     * @method divScalarVec4
     * @static
     * @param s scalar
     * @param v vec4
     * @param dest vec4 - optional destination
     * @return [] dest if specified, v otherwise
     */
    divScalarVec4(s, v, dest) {
        if (!dest) {
            dest = v;
        }
        dest[0] = s / v[0];
        dest[1] = s / v[1];
        dest[2] = s / v[2];
        dest[3] = s / v[3];
        return dest;
    },

    /**
     * Returns the dot product of two four-element vectors.
     * @method dotVec4
     * @static
     * @param {Array(Number)} u First vector
     * @param {Array(Number)} v Second vector
     * @return The dot product
     */
    dotVec4(u, v) {
        return (u[0] * v[0] + u[1] * v[1] + u[2] * v[2] + u[3] * v[3]);
    },

    /**
     * Returns the cross product of two four-element vectors.
     * @method cross3Vec4
     * @static
     * @param {Array(Number)} u First vector
     * @param {Array(Number)} v Second vector
     * @return The cross product
     */
    cross3Vec4(u, v) {
        const u0 = u[0];
        const u1 = u[1];
        const u2 = u[2];
        const v0 = v[0];
        const v1 = v[1];
        const v2 = v[2];
        return [
            u1 * v2 - u2 * v1,
            u2 * v0 - u0 * v2,
            u0 * v1 - u1 * v0,
            0.0];
    },

    /**
     * Returns the cross product of two three-element vectors.
     * @method cross3Vec3
     * @static
     * @param {Array(Number)} u First vector
     * @param {Array(Number)} v Second vector
     * @return The cross product
     */
    cross3Vec3(u, v, dest) {
        if (!dest) {
            dest = u;
        }
        const x = u[0];
        const y = u[1];
        const z = u[2];
        const x2 = v[0];
        const y2 = v[1];
        const z2 = v[2];
        dest[0] = y * z2 - z * y2;
        dest[1] = z * x2 - x * z2;
        dest[2] = x * y2 - y * x2;
        return dest;
    },


    sqLenVec4(v) { // TODO
        return math.dotVec4(v, v);
    },

    /**
     * Returns the length of a four-element vector.
     * @method lenVec4
     * @static
     * @param {Array(Number)} v The vector
     * @return The length
     */
    lenVec4(v) {
        return Math.sqrt(math.sqLenVec4(v));
    },

    /**
     * Returns the dot product of two three-element vectors.
     * @method dotVec3
     * @static
     * @param {Array(Number)} u First vector
     * @param {Array(Number)} v Second vector
     * @return The dot product
     */
    dotVec3(u, v) {
        return (u[0] * v[0] + u[1] * v[1] + u[2] * v[2]);
    },

    /**
     * Returns the dot product of two two-element vectors.
     * @method dotVec4
     * @static
     * @param {Array(Number)} u First vector
     * @param {Array(Number)} v Second vector
     * @return The dot product
     */
    dotVec2(u, v) {
        return (u[0] * v[0] + u[1] * v[1]);
    },


    sqLenVec3(v) {
        return math.dotVec3(v, v);
    },


    sqLenVec2(v) {
        return math.dotVec2(v, v);
    },

    /**
     * Returns the length of a three-element vector.
     * @method lenVec3
     * @static
     * @param {Array(Number)} v The vector
     * @return The length
     */
    lenVec3(v) {
        return Math.sqrt(math.sqLenVec3(v));
    },

    distVec3: ((() => {
        const vec = new FloatArrayType(3);
        return (v, w) => math.lenVec3(math.subVec3(v, w, vec));
    }))(),

    /**
     * Returns the length of a two-element vector.
     * @method lenVec2
     * @static
     * @param {Array(Number)} v The vector
     * @return The length
     */
    lenVec2(v) {
        return Math.sqrt(math.sqLenVec2(v));
    },

    distVec2: ((() => {
        const vec = new FloatArrayType(2);
        return (v, w) => math.lenVec2(math.subVec2(v, w, vec));
    }))(),

    /**
     * @method rcpVec3
     * @static
     * @param v vec3
     * @param dest vec3 - optional destination
     * @return [] dest if specified, v otherwise
     *
     */
    rcpVec3(v, dest) {
        return math.divScalarVec3(1.0, v, dest);
    },

    /**
     * Normalizes a four-element vector
     * @method normalizeVec4
     * @static
     * @param v vec4
     * @param dest vec4 - optional destination
     * @return [] dest if specified, v otherwise
     *
     */
    normalizeVec4(v, dest) {
        const f = 1.0 / math.lenVec4(v);
        return math.mulVec4Scalar(v, f, dest);
    },

    /**
     * Normalizes a three-element vector
     * @method normalizeVec4
     * @static
     */
    normalizeVec3(v, dest) {
        const f = 1.0 / math.lenVec3(v);
        return math.mulVec3Scalar(v, f, dest);
    },

    /**
     * Normalizes a two-element vector
     * @method normalizeVec2
     * @static
     */
    normalizeVec2(v, dest) {
        const f = 1.0 / math.lenVec2(v);
        return math.mulVec2Scalar(v, f, dest);
    },

    /**
     * Gets the angle between two vectors
     * @method angleVec3
     * @param v
     * @param w
     * @returns {number}
     */
    angleVec3(v, w) {
        let theta = math.dotVec3(v, w) / (Math.sqrt(math.sqLenVec3(v) * math.sqLenVec3(w)));
        theta = theta < -1 ? -1 : (theta > 1 ? 1 : theta);  // Clamp to handle numerical problems
        return Math.acos(theta);
    },

    /**
     * Creates a three-element vector from the rotation part of a sixteen-element matrix.
     * @param m
     * @param dest
     */
    vec3FromMat4Scale: ((() => {

        const tempVec3 = new FloatArrayType(3);

        return (m, dest) => {

            tempVec3[0] = m[0];
            tempVec3[1] = m[1];
            tempVec3[2] = m[2];

            dest[0] = math.lenVec3(tempVec3);

            tempVec3[0] = m[4];
            tempVec3[1] = m[5];
            tempVec3[2] = m[6];

            dest[1] = math.lenVec3(tempVec3);

            tempVec3[0] = m[8];
            tempVec3[1] = m[9];
            tempVec3[2] = m[10];

            dest[2] = math.lenVec3(tempVec3);

            return dest;
        };
    }))(),

    /**
     * Converts an n-element vector to a JSON-serializable
     * array with values rounded to two decimal places.
     */
    vecToArray: ((() => {
        function trunc(v) {
            return Math.round(v * 100000) / 100000
        }

        return v => {
            v = Array.prototype.slice.call(v);
            for (let i = 0, len = v.length; i < len; i++) {
                v[i] = trunc(v[i]);
            }
            return v;
        };
    }))(),

    /**
     * Converts a 3-element vector from an array to an object of the form ````{x:999, y:999, z:999}````.
     * @param arr
     * @returns {{x: *, y: *, z: *}}
     */
    xyzArrayToObject(arr) {
        return {"x": arr[0], "y": arr[1], "z": arr[2]};
    },

    /**
     * Converts a 3-element vector object of the form ````{x:999, y:999, z:999}```` to an array.
     * @param xyz
     * @param  [arry]
     * @returns {*[]}
     */
    xyzObjectToArray(xyz, arry) {
        arry = arry || math.vec3();
        arry[0] = xyz.x;
        arry[1] = xyz.y;
        arry[2] = xyz.z;
        return arry;
    },

    /**
     * Duplicates a 4x4 identity matrix.
     * @method dupMat4
     * @static
     */
    dupMat4(m) {
        return m.slice(0, 16);
    },

    /**
     * Extracts a 3x3 matrix from a 4x4 matrix.
     * @method mat4To3
     * @static
     */
    mat4To3(m) {
        return [
            m[0], m[1], m[2],
            m[4], m[5], m[6],
            m[8], m[9], m[10]
        ];
    },

    /**
     * Returns a 4x4 matrix with each element set to the given scalar value.
     * @method m4s
     * @static
     */
    m4s(s) {
        return [
            s, s, s, s,
            s, s, s, s,
            s, s, s, s,
            s, s, s, s
        ];
    },

    /**
     * Returns a 4x4 matrix with each element set to zero.
     * @method setMat4ToZeroes
     * @static
     */
    setMat4ToZeroes() {
        return math.m4s(0.0);
    },

    /**
     * Returns a 4x4 matrix with each element set to 1.0.
     * @method setMat4ToOnes
     * @static
     */
    setMat4ToOnes() {
        return math.m4s(1.0);
    },

    /**
     * Returns a 4x4 matrix with each element set to 1.0.
     * @method setMat4ToOnes
     * @static
     */
    diagonalMat4v(v) {
        return new FloatArrayType([
            v[0], 0.0, 0.0, 0.0,
            0.0, v[1], 0.0, 0.0,
            0.0, 0.0, v[2], 0.0,
            0.0, 0.0, 0.0, v[3]
        ]);
    },

    /**
     * Returns a 4x4 matrix with diagonal elements set to the given vector.
     * @method diagonalMat4c
     * @static
     */
    diagonalMat4c(x, y, z, w) {
        return math.diagonalMat4v([x, y, z, w]);
    },

    /**
     * Returns a 4x4 matrix with diagonal elements set to the given scalar.
     * @method diagonalMat4s
     * @static
     */
    diagonalMat4s(s) {
        return math.diagonalMat4c(s, s, s, s);
    },

    /**
     * Returns a 4x4 identity matrix.
     * @method identityMat4
     * @static
     */
    identityMat4(mat = new FloatArrayType(16)) {
        mat[0] = 1.0;
        mat[1] = 0.0;
        mat[2] = 0.0;
        mat[3] = 0.0;

        mat[4] = 0.0;
        mat[5] = 1.0;
        mat[6] = 0.0;
        mat[7] = 0.0;

        mat[8] = 0.0;
        mat[9] = 0.0;
        mat[10] = 1.0;
        mat[11] = 0.0;

        mat[12] = 0.0;
        mat[13] = 0.0;
        mat[14] = 0.0;
        mat[15] = 1.0;

        return mat;
    },

    /**
     * Returns a 3x3 identity matrix.
     * @method identityMat3
     * @static
     */
    identityMat3(mat = new FloatArrayType(9)) {
        mat[0] = 1.0;
        mat[1] = 0.0;
        mat[2] = 0.0;

        mat[3] = 0.0;
        mat[4] = 1.0;
        mat[5] = 0.0;

        mat[6] = 0.0;
        mat[7] = 0.0;
        mat[8] = 1.0;

        return mat;
    },

    /**
     * Tests if the given 4x4 matrix is the identity matrix.
     * @method isIdentityMat4
     * @static
     */
    isIdentityMat4(m) {
        if (m[0] !== 1.0 || m[1] !== 0.0 || m[2] !== 0.0 || m[3] !== 0.0 ||
            m[4] !== 0.0 || m[5] !== 1.0 || m[6] !== 0.0 || m[7] !== 0.0 ||
            m[8] !== 0.0 || m[9] !== 0.0 || m[10] !== 1.0 || m[11] !== 0.0 ||
            m[12] !== 0.0 || m[13] !== 0.0 || m[14] !== 0.0 || m[15] !== 1.0) {
            return false;
        }
        return true;
    },

    /**
     * Negates the given 4x4 matrix.
     * @method negateMat4
     * @static
     */
    negateMat4(m, dest) {
        if (!dest) {
            dest = m;
        }
        dest[0] = -m[0];
        dest[1] = -m[1];
        dest[2] = -m[2];
        dest[3] = -m[3];
        dest[4] = -m[4];
        dest[5] = -m[5];
        dest[6] = -m[6];
        dest[7] = -m[7];
        dest[8] = -m[8];
        dest[9] = -m[9];
        dest[10] = -m[10];
        dest[11] = -m[11];
        dest[12] = -m[12];
        dest[13] = -m[13];
        dest[14] = -m[14];
        dest[15] = -m[15];
        return dest;
    },

    /**
     * Adds the given 4x4 matrices together.
     * @method addMat4
     * @static
     */
    addMat4(a, b, dest) {
        if (!dest) {
            dest = a;
        }
        dest[0] = a[0] + b[0];
        dest[1] = a[1] + b[1];
        dest[2] = a[2] + b[2];
        dest[3] = a[3] + b[3];
        dest[4] = a[4] + b[4];
        dest[5] = a[5] + b[5];
        dest[6] = a[6] + b[6];
        dest[7] = a[7] + b[7];
        dest[8] = a[8] + b[8];
        dest[9] = a[9] + b[9];
        dest[10] = a[10] + b[10];
        dest[11] = a[11] + b[11];
        dest[12] = a[12] + b[12];
        dest[13] = a[13] + b[13];
        dest[14] = a[14] + b[14];
        dest[15] = a[15] + b[15];
        return dest;
    },

    /**
     * Adds the given scalar to each element of the given 4x4 matrix.
     * @method addMat4Scalar
     * @static
     */
    addMat4Scalar(m, s, dest) {
        if (!dest) {
            dest = m;
        }
        dest[0] = m[0] + s;
        dest[1] = m[1] + s;
        dest[2] = m[2] + s;
        dest[3] = m[3] + s;
        dest[4] = m[4] + s;
        dest[5] = m[5] + s;
        dest[6] = m[6] + s;
        dest[7] = m[7] + s;
        dest[8] = m[8] + s;
        dest[9] = m[9] + s;
        dest[10] = m[10] + s;
        dest[11] = m[11] + s;
        dest[12] = m[12] + s;
        dest[13] = m[13] + s;
        dest[14] = m[14] + s;
        dest[15] = m[15] + s;
        return dest;
    },

    /**
     * Adds the given scalar to each element of the given 4x4 matrix.
     * @method addScalarMat4
     * @static
     */
    addScalarMat4(s, m, dest) {
        return math.addMat4Scalar(m, s, dest);
    },

    /**
     * Subtracts the second 4x4 matrix from the first.
     * @method subMat4
     * @static
     */
    subMat4(a, b, dest) {
        if (!dest) {
            dest = a;
        }
        dest[0] = a[0] - b[0];
        dest[1] = a[1] - b[1];
        dest[2] = a[2] - b[2];
        dest[3] = a[3] - b[3];
        dest[4] = a[4] - b[4];
        dest[5] = a[5] - b[5];
        dest[6] = a[6] - b[6];
        dest[7] = a[7] - b[7];
        dest[8] = a[8] - b[8];
        dest[9] = a[9] - b[9];
        dest[10] = a[10] - b[10];
        dest[11] = a[11] - b[11];
        dest[12] = a[12] - b[12];
        dest[13] = a[13] - b[13];
        dest[14] = a[14] - b[14];
        dest[15] = a[15] - b[15];
        return dest;
    },

    /**
     * Subtracts the given scalar from each element of the given 4x4 matrix.
     * @method subMat4Scalar
     * @static
     */
    subMat4Scalar(m, s, dest) {
        if (!dest) {
            dest = m;
        }
        dest[0] = m[0] - s;
        dest[1] = m[1] - s;
        dest[2] = m[2] - s;
        dest[3] = m[3] - s;
        dest[4] = m[4] - s;
        dest[5] = m[5] - s;
        dest[6] = m[6] - s;
        dest[7] = m[7] - s;
        dest[8] = m[8] - s;
        dest[9] = m[9] - s;
        dest[10] = m[10] - s;
        dest[11] = m[11] - s;
        dest[12] = m[12] - s;
        dest[13] = m[13] - s;
        dest[14] = m[14] - s;
        dest[15] = m[15] - s;
        return dest;
    },

    /**
     * Subtracts the given scalar from each element of the given 4x4 matrix.
     * @method subScalarMat4
     * @static
     */
    subScalarMat4(s, m, dest) {
        if (!dest) {
            dest = m;
        }
        dest[0] = s - m[0];
        dest[1] = s - m[1];
        dest[2] = s - m[2];
        dest[3] = s - m[3];
        dest[4] = s - m[4];
        dest[5] = s - m[5];
        dest[6] = s - m[6];
        dest[7] = s - m[7];
        dest[8] = s - m[8];
        dest[9] = s - m[9];
        dest[10] = s - m[10];
        dest[11] = s - m[11];
        dest[12] = s - m[12];
        dest[13] = s - m[13];
        dest[14] = s - m[14];
        dest[15] = s - m[15];
        return dest;
    },

    /**
     * Multiplies the two given 4x4 matrix by each other.
     * @method mulMat4
     * @static
     */
    mulMat4(a, b, dest) {
        if (!dest) {
            dest = a;
        }

        // Cache the matrix values (makes for huge speed increases!)
        const a00 = a[0];

        const a01 = a[1];
        const a02 = a[2];
        const a03 = a[3];
        const a10 = a[4];
        const a11 = a[5];
        const a12 = a[6];
        const a13 = a[7];
        const a20 = a[8];
        const a21 = a[9];
        const a22 = a[10];
        const a23 = a[11];
        const a30 = a[12];
        const a31 = a[13];
        const a32 = a[14];
        const a33 = a[15];
        const b00 = b[0];
        const b01 = b[1];
        const b02 = b[2];
        const b03 = b[3];
        const b10 = b[4];
        const b11 = b[5];
        const b12 = b[6];
        const b13 = b[7];
        const b20 = b[8];
        const b21 = b[9];
        const b22 = b[10];
        const b23 = b[11];
        const b30 = b[12];
        const b31 = b[13];
        const b32 = b[14];
        const b33 = b[15];

        dest[0] = b00 * a00 + b01 * a10 + b02 * a20 + b03 * a30;
        dest[1] = b00 * a01 + b01 * a11 + b02 * a21 + b03 * a31;
        dest[2] = b00 * a02 + b01 * a12 + b02 * a22 + b03 * a32;
        dest[3] = b00 * a03 + b01 * a13 + b02 * a23 + b03 * a33;
        dest[4] = b10 * a00 + b11 * a10 + b12 * a20 + b13 * a30;
        dest[5] = b10 * a01 + b11 * a11 + b12 * a21 + b13 * a31;
        dest[6] = b10 * a02 + b11 * a12 + b12 * a22 + b13 * a32;
        dest[7] = b10 * a03 + b11 * a13 + b12 * a23 + b13 * a33;
        dest[8] = b20 * a00 + b21 * a10 + b22 * a20 + b23 * a30;
        dest[9] = b20 * a01 + b21 * a11 + b22 * a21 + b23 * a31;
        dest[10] = b20 * a02 + b21 * a12 + b22 * a22 + b23 * a32;
        dest[11] = b20 * a03 + b21 * a13 + b22 * a23 + b23 * a33;
        dest[12] = b30 * a00 + b31 * a10 + b32 * a20 + b33 * a30;
        dest[13] = b30 * a01 + b31 * a11 + b32 * a21 + b33 * a31;
        dest[14] = b30 * a02 + b31 * a12 + b32 * a22 + b33 * a32;
        dest[15] = b30 * a03 + b31 * a13 + b32 * a23 + b33 * a33;

        return dest;
    },

    /**
     * Multiplies the two given 3x3 matrices by each other.
     * @method mulMat4
     * @static
     */
    mulMat3(a, b, dest) {
        if (!dest) {
            dest = new FloatArrayType(9);
        }

        const a11 = a[0];
        const a12 = a[3];
        const a13 = a[6];
        const a21 = a[1];
        const a22 = a[4];
        const a23 = a[7];
        const a31 = a[2];
        const a32 = a[5];
        const a33 = a[8];
        const b11 = b[0];
        const b12 = b[3];
        const b13 = b[6];
        const b21 = b[1];
        const b22 = b[4];
        const b23 = b[7];
        const b31 = b[2];
        const b32 = b[5];
        const b33 = b[8];

        dest[0] = a11 * b11 + a12 * b21 + a13 * b31;
        dest[3] = a11 * b12 + a12 * b22 + a13 * b32;
        dest[6] = a11 * b13 + a12 * b23 + a13 * b33;

        dest[1] = a21 * b11 + a22 * b21 + a23 * b31;
        dest[4] = a21 * b12 + a22 * b22 + a23 * b32;
        dest[7] = a21 * b13 + a22 * b23 + a23 * b33;

        dest[2] = a31 * b11 + a32 * b21 + a33 * b31;
        dest[5] = a31 * b12 + a32 * b22 + a33 * b32;
        dest[8] = a31 * b13 + a32 * b23 + a33 * b33;

        return dest;
    },

    /**
     * Multiplies each element of the given 4x4 matrix by the given scalar.
     * @method mulMat4Scalar
     * @static
     */
    mulMat4Scalar(m, s, dest) {
        if (!dest) {
            dest = m;
        }
        dest[0] = m[0] * s;
        dest[1] = m[1] * s;
        dest[2] = m[2] * s;
        dest[3] = m[3] * s;
        dest[4] = m[4] * s;
        dest[5] = m[5] * s;
        dest[6] = m[6] * s;
        dest[7] = m[7] * s;
        dest[8] = m[8] * s;
        dest[9] = m[9] * s;
        dest[10] = m[10] * s;
        dest[11] = m[11] * s;
        dest[12] = m[12] * s;
        dest[13] = m[13] * s;
        dest[14] = m[14] * s;
        dest[15] = m[15] * s;
        return dest;
    },

    /**
     * Multiplies the given 4x4 matrix by the given four-element vector.
     * @method mulMat4v4
     * @static
     */
    mulMat4v4(m, v, dest = math.vec4()) {
        const v0 = v[0];
        const v1 = v[1];
        const v2 = v[2];
        const v3 = v[3];
        dest[0] = m[0] * v0 + m[4] * v1 + m[8] * v2 + m[12] * v3;
        dest[1] = m[1] * v0 + m[5] * v1 + m[9] * v2 + m[13] * v3;
        dest[2] = m[2] * v0 + m[6] * v1 + m[10] * v2 + m[14] * v3;
        dest[3] = m[3] * v0 + m[7] * v1 + m[11] * v2 + m[15] * v3;
        return dest;
    },

    /**
     * Transposes the given 4x4 matrix.
     * @method transposeMat4
     * @static
     */
    transposeMat4(mat, dest) {
        // If we are transposing ourselves we can skip a few steps but have to cache some values
        const m4 = mat[4];

        const m14 = mat[14];
        const m8 = mat[8];
        const m13 = mat[13];
        const m12 = mat[12];
        const m9 = mat[9];
        if (!dest || mat === dest) {
            const a01 = mat[1];
            const a02 = mat[2];
            const a03 = mat[3];
            const a12 = mat[6];
            const a13 = mat[7];
            const a23 = mat[11];
            mat[1] = m4;
            mat[2] = m8;
            mat[3] = m12;
            mat[4] = a01;
            mat[6] = m9;
            mat[7] = m13;
            mat[8] = a02;
            mat[9] = a12;
            mat[11] = m14;
            mat[12] = a03;
            mat[13] = a13;
            mat[14] = a23;
            return mat;
        }
        dest[0] = mat[0];
        dest[1] = m4;
        dest[2] = m8;
        dest[3] = m12;
        dest[4] = mat[1];
        dest[5] = mat[5];
        dest[6] = m9;
        dest[7] = m13;
        dest[8] = mat[2];
        dest[9] = mat[6];
        dest[10] = mat[10];
        dest[11] = m14;
        dest[12] = mat[3];
        dest[13] = mat[7];
        dest[14] = mat[11];
        dest[15] = mat[15];
        return dest;
    },

    /**
     * Transposes the given 3x3 matrix.
     *
     * @method transposeMat3
     * @static
     */
    transposeMat3(mat, dest) {
        if (dest === mat) {
            const a01 = mat[1];
            const a02 = mat[2];
            const a12 = mat[5];
            dest[1] = mat[3];
            dest[2] = mat[6];
            dest[3] = a01;
            dest[5] = mat[7];
            dest[6] = a02;
            dest[7] = a12;
        } else {
            dest[0] = mat[0];
            dest[1] = mat[3];
            dest[2] = mat[6];
            dest[3] = mat[1];
            dest[4] = mat[4];
            dest[5] = mat[7];
            dest[6] = mat[2];
            dest[7] = mat[5];
            dest[8] = mat[8];
        }
        return dest;
    },

    /**
     * Returns the determinant of the given 4x4 matrix.
     * @method determinantMat4
     * @static
     */
    determinantMat4(mat) {
        // Cache the matrix values (makes for huge speed increases!)
        const a00 = mat[0];

        const a01 = mat[1];
        const a02 = mat[2];
        const a03 = mat[3];
        const a10 = mat[4];
        const a11 = mat[5];
        const a12 = mat[6];
        const a13 = mat[7];
        const a20 = mat[8];
        const a21 = mat[9];
        const a22 = mat[10];
        const a23 = mat[11];
        const a30 = mat[12];
        const a31 = mat[13];
        const a32 = mat[14];
        const a33 = mat[15];
        return a30 * a21 * a12 * a03 - a20 * a31 * a12 * a03 - a30 * a11 * a22 * a03 + a10 * a31 * a22 * a03 +
            a20 * a11 * a32 * a03 - a10 * a21 * a32 * a03 - a30 * a21 * a02 * a13 + a20 * a31 * a02 * a13 +
            a30 * a01 * a22 * a13 - a00 * a31 * a22 * a13 - a20 * a01 * a32 * a13 + a00 * a21 * a32 * a13 +
            a30 * a11 * a02 * a23 - a10 * a31 * a02 * a23 - a30 * a01 * a12 * a23 + a00 * a31 * a12 * a23 +
            a10 * a01 * a32 * a23 - a00 * a11 * a32 * a23 - a20 * a11 * a02 * a33 + a10 * a21 * a02 * a33 +
            a20 * a01 * a12 * a33 - a00 * a21 * a12 * a33 - a10 * a01 * a22 * a33 + a00 * a11 * a22 * a33;
    },

    /**
     * Returns the inverse of the given 4x4 matrix.
     * @method inverseMat4
     * @static
     */
    inverseMat4(mat, dest) {
        if (!dest) {
            dest = mat;
        }

        // Cache the matrix values (makes for huge speed increases!)
        const a00 = mat[0];

        const a01 = mat[1];
        const a02 = mat[2];
        const a03 = mat[3];
        const a10 = mat[4];
        const a11 = mat[5];
        const a12 = mat[6];
        const a13 = mat[7];
        const a20 = mat[8];
        const a21 = mat[9];
        const a22 = mat[10];
        const a23 = mat[11];
        const a30 = mat[12];
        const a31 = mat[13];
        const a32 = mat[14];
        const a33 = mat[15];
        const b00 = a00 * a11 - a01 * a10;
        const b01 = a00 * a12 - a02 * a10;
        const b02 = a00 * a13 - a03 * a10;
        const b03 = a01 * a12 - a02 * a11;
        const b04 = a01 * a13 - a03 * a11;
        const b05 = a02 * a13 - a03 * a12;
        const b06 = a20 * a31 - a21 * a30;
        const b07 = a20 * a32 - a22 * a30;
        const b08 = a20 * a33 - a23 * a30;
        const b09 = a21 * a32 - a22 * a31;
        const b10 = a21 * a33 - a23 * a31;
        const b11 = a22 * a33 - a23 * a32;

        // Calculate the determinant (inlined to avoid double-caching)
        const invDet = 1 / (b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06);

        dest[0] = (a11 * b11 - a12 * b10 + a13 * b09) * invDet;
        dest[1] = (-a01 * b11 + a02 * b10 - a03 * b09) * invDet;
        dest[2] = (a31 * b05 - a32 * b04 + a33 * b03) * invDet;
        dest[3] = (-a21 * b05 + a22 * b04 - a23 * b03) * invDet;
        dest[4] = (-a10 * b11 + a12 * b08 - a13 * b07) * invDet;
        dest[5] = (a00 * b11 - a02 * b08 + a03 * b07) * invDet;
        dest[6] = (-a30 * b05 + a32 * b02 - a33 * b01) * invDet;
        dest[7] = (a20 * b05 - a22 * b02 + a23 * b01) * invDet;
        dest[8] = (a10 * b10 - a11 * b08 + a13 * b06) * invDet;
        dest[9] = (-a00 * b10 + a01 * b08 - a03 * b06) * invDet;
        dest[10] = (a30 * b04 - a31 * b02 + a33 * b00) * invDet;
        dest[11] = (-a20 * b04 + a21 * b02 - a23 * b00) * invDet;
        dest[12] = (-a10 * b09 + a11 * b07 - a12 * b06) * invDet;
        dest[13] = (a00 * b09 - a01 * b07 + a02 * b06) * invDet;
        dest[14] = (-a30 * b03 + a31 * b01 - a32 * b00) * invDet;
        dest[15] = (a20 * b03 - a21 * b01 + a22 * b00) * invDet;

        return dest;
    },

    /**
     * Returns the trace of the given 4x4 matrix.
     * @method traceMat4
     * @static
     */
    traceMat4(m) {
        return (m[0] + m[5] + m[10] + m[15]);
    },

    /**
     * Returns 4x4 translation matrix.
     * @method translationMat4
     * @static
     */
    translationMat4v(v, dest) {
        const m = dest || math.identityMat4();
        m[12] = v[0];
        m[13] = v[1];
        m[14] = v[2];
        return m;
    },

    /**
     * Returns 3x3 translation matrix.
     * @method translationMat3
     * @static
     */
    translationMat3v(v, dest) {
        const m = dest || math.identityMat3();
        m[6] = v[0];
        m[7] = v[1];
        return m;
    },

    /**
     * Returns 4x4 translation matrix.
     * @method translationMat4c
     * @static
     */
    translationMat4c: ((() => {
        const xyz = new FloatArrayType(3);
        return (x, y, z, dest) => {
            xyz[0] = x;
            xyz[1] = y;
            xyz[2] = z;
            return math.translationMat4v(xyz, dest);
        };
    }))(),

    /**
     * Returns 4x4 translation matrix.
     * @method translationMat4s
     * @static
     */
    translationMat4s(s, dest) {
        return math.translationMat4c(s, s, s, dest);
    },

    /**
     * Efficiently post-concatenates a translation to the given matrix.
     * @param v
     * @param m
     */
    translateMat4v(xyz, m) {
        return math.translateMat4c(xyz[0], xyz[1], xyz[2], m);
    },

    /**
     * Efficiently post-concatenates a translation to the given matrix.
     * @param x
     * @param y
     * @param z
     * @param m
     */

    translateMat4c(x, y, z, m) {

        const m3 = m[3];
        m[0] += m3 * x;
        m[1] += m3 * y;
        m[2] += m3 * z;

        const m7 = m[7];
        m[4] += m7 * x;
        m[5] += m7 * y;
        m[6] += m7 * z;

        const m11 = m[11];
        m[8] += m11 * x;
        m[9] += m11 * y;
        m[10] += m11 * z;

        const m15 = m[15];
        m[12] += m15 * x;
        m[13] += m15 * y;
        m[14] += m15 * z;

        return m;
    },

    /**
     * Creates a new matrix that replaces the translation in the rightmost column of the given
     * affine matrix with the given translation.
     * @param m
     * @param translation
     * @param dest
     * @returns {*}
     */
    setMat4Translation(m, translation, dest) {

        dest[0] = m[0];
        dest[1] = m[1];
        dest[2] = m[2];
        dest[3] = m[3];

        dest[4] = m[4];
        dest[5] = m[5];
        dest[6] = m[6];
        dest[7] = m[7];

        dest[8] = m[8];
        dest[9] = m[9];
        dest[10] = m[10];
        dest[11] = m[11];

        dest[12] = translation[0];
        dest[13] = translation[1];
        dest[14] = translation[2];
        dest[15] = m[15];

        return dest;
    },

    /**
     * Returns 4x4 rotation matrix.
     * @method rotationMat4v
     * @static
     */
    rotationMat4v(anglerad, axis, m) {
        const ax = math.normalizeVec4([axis[0], axis[1], axis[2], 0.0], []);
        const s = Math.sin(anglerad);
        const c = Math.cos(anglerad);
        const q = 1.0 - c;

        const x = ax[0];
        const y = ax[1];
        const z = ax[2];

        let xy;
        let yz;
        let zx;
        let xs;
        let ys;
        let zs;

        //xx = x * x; used once
        //yy = y * y; used once
        //zz = z * z; used once
        xy = x * y;
        yz = y * z;
        zx = z * x;
        xs = x * s;
        ys = y * s;
        zs = z * s;

        m = m || math.mat4();

        m[0] = (q * x * x) + c;
        m[1] = (q * xy) + zs;
        m[2] = (q * zx) - ys;
        m[3] = 0.0;

        m[4] = (q * xy) - zs;
        m[5] = (q * y * y) + c;
        m[6] = (q * yz) + xs;
        m[7] = 0.0;

        m[8] = (q * zx) + ys;
        m[9] = (q * yz) - xs;
        m[10] = (q * z * z) + c;
        m[11] = 0.0;

        m[12] = 0.0;
        m[13] = 0.0;
        m[14] = 0.0;
        m[15] = 1.0;

        return m;
    },

    /**
     * Returns 4x4 rotation matrix.
     * @method rotationMat4c
     * @static
     */
    rotationMat4c(anglerad, x, y, z, mat) {
        return math.rotationMat4v(anglerad, [x, y, z], mat);
    },

    /**
     * Returns 4x4 scale matrix.
     * @method scalingMat4v
     * @static
     */
    scalingMat4v(v, m = math.identityMat4()) {
        m[0] = v[0];
        m[5] = v[1];
        m[10] = v[2];
        return m;
    },

    /**
     * Returns 3x3 scale matrix.
     * @method scalingMat3v
     * @static
     */
    scalingMat3v(v, m = math.identityMat3()) {
        m[0] = v[0];
        m[4] = v[1];
        return m;
    },

    /**
     * Returns 4x4 scale matrix.
     * @method scalingMat4c
     * @static
     */
    scalingMat4c: ((() => {
        const xyz = new FloatArrayType(3);
        return (x, y, z, dest) => {
            xyz[0] = x;
            xyz[1] = y;
            xyz[2] = z;
            return math.scalingMat4v(xyz, dest);
        };
    }))(),

    /**
     * Efficiently post-concatenates a scaling to the given matrix.
     * @method scaleMat4c
     * @param x
     * @param y
     * @param z
     * @param m
     */
    scaleMat4c(x, y, z, m) {

        m[0] *= x;
        m[4] *= y;
        m[8] *= z;

        m[1] *= x;
        m[5] *= y;
        m[9] *= z;

        m[2] *= x;
        m[6] *= y;
        m[10] *= z;

        m[3] *= x;
        m[7] *= y;
        m[11] *= z;
        return m;
    },

    /**
     * Efficiently post-concatenates a scaling to the given matrix.
     * @method scaleMat4c
     * @param xyz
     * @param m
     */
    scaleMat4v(xyz, m) {

        const x = xyz[0];
        const y = xyz[1];
        const z = xyz[2];

        m[0] *= x;
        m[4] *= y;
        m[8] *= z;
        m[1] *= x;
        m[5] *= y;
        m[9] *= z;
        m[2] *= x;
        m[6] *= y;
        m[10] *= z;
        m[3] *= x;
        m[7] *= y;
        m[11] *= z;

        return m;
    },

    /**
     * Returns 4x4 scale matrix.
     * @method scalingMat4s
     * @static
     */
    scalingMat4s(s) {
        return math.scalingMat4c(s, s, s);
    },

    /**
     * Creates a matrix from a quaternion rotation and vector translation
     *
     * @param {Number[]} q Rotation quaternion
     * @param {Number[]} v Translation vector
     * @param {Number[]} dest Destination matrix
     * @returns {Number[]} dest
     */
    rotationTranslationMat4(q, v, dest = math.mat4()) {
        const x = q[0];
        const y = q[1];
        const z = q[2];
        const w = q[3];

        const x2 = x + x;
        const y2 = y + y;
        const z2 = z + z;
        const xx = x * x2;
        const xy = x * y2;
        const xz = x * z2;
        const yy = y * y2;
        const yz = y * z2;
        const zz = z * z2;
        const wx = w * x2;
        const wy = w * y2;
        const wz = w * z2;

        dest[0] = 1 - (yy + zz);
        dest[1] = xy + wz;
        dest[2] = xz - wy;
        dest[3] = 0;
        dest[4] = xy - wz;
        dest[5] = 1 - (xx + zz);
        dest[6] = yz + wx;
        dest[7] = 0;
        dest[8] = xz + wy;
        dest[9] = yz - wx;
        dest[10] = 1 - (xx + yy);
        dest[11] = 0;
        dest[12] = v[0];
        dest[13] = v[1];
        dest[14] = v[2];
        dest[15] = 1;

        return dest;
    },

    /**
     * Gets Euler angles from a 4x4 matrix.
     *
     * @param {Number[]} mat The 4x4 matrix.
     * @param {String} order Desired Euler angle order: "XYZ", "YXZ", "ZXY" etc.
     * @param {Number[]} [dest] Destination Euler angles, created by default.
     * @returns {Number[]} The Euler angles (in degrees).
     */
    mat4ToEuler(mat, order, dest = math.vec4()) {
        const clamp = math.clamp;

        // Assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)

        const m11 = mat[0];

        const m12 = mat[4];
        const m13 = mat[8];
        const m21 = mat[1];
        const m22 = mat[5];
        const m23 = mat[9];
        const m31 = mat[2];
        const m32 = mat[6];
        const m33 = mat[10];

        if (order === 'XYZ') {

            dest[1] = Math.asin(clamp(m13, -1, 1));

            if (Math.abs(m13) < 0.99999) {
                dest[0] = Math.atan2(-m23, m33);
                dest[2] = Math.atan2(-m12, m11);
            } else {
                dest[0] = Math.atan2(m32, m22);
                dest[2] = 0;

            }

        } else if (order === 'YXZ') {

            dest[0] = Math.asin(-clamp(m23, -1, 1));

            if (Math.abs(m23) < 0.99999) {
                dest[1] = Math.atan2(m13, m33);
                dest[2] = Math.atan2(m21, m22);
            } else {
                dest[1] = Math.atan2(-m31, m11);
                dest[2] = 0;
            }

        } else if (order === 'ZXY') {

            dest[0] = Math.asin(clamp(m32, -1, 1));

            if (Math.abs(m32) < 0.99999) {
                dest[1] = Math.atan2(-m31, m33);
                dest[2] = Math.atan2(-m12, m22);
            } else {
                dest[1] = 0;
                dest[2] = Math.atan2(m21, m11);
            }

        } else if (order === 'ZYX') {

            dest[1] = Math.asin(-clamp(m31, -1, 1));

            if (Math.abs(m31) < 0.99999) {
                dest[0] = Math.atan2(m32, m33);
                dest[2] = Math.atan2(m21, m11);
            } else {
                dest[0] = 0;
                dest[2] = Math.atan2(-m12, m22);
            }

        } else if (order === 'YZX') {

            dest[2] = Math.asin(clamp(m21, -1, 1));

            if (Math.abs(m21) < 0.99999) {
                dest[0] = Math.atan2(-m23, m22);
                dest[1] = Math.atan2(-m31, m11);
            } else {
                dest[0] = 0;
                dest[1] = Math.atan2(m13, m33);
            }

        } else if (order === 'XZY') {

            dest[2] = Math.asin(-clamp(m12, -1, 1));

            if (Math.abs(m12) < 0.99999) {
                dest[0] = Math.atan2(m32, m22);
                dest[1] = Math.atan2(m13, m11);
            } else {
                dest[0] = Math.atan2(-m23, m33);
                dest[1] = 0;
            }
        }

        dest[0] *= math.RADTODEG;
        dest[1] *= math.RADTODEG;
        dest[2] *= math.RADTODEG;

        return dest;
    },

    composeMat4(position, quaternion, scale, mat = math.mat4()) {
        math.quaternionToRotationMat4(quaternion, mat);
        math.scaleMat4v(scale, mat);
        math.translateMat4v(position, mat);

        return mat;
    },

    decomposeMat4: (() => {

        const vec = new FloatArrayType(3);
        const matrix = new FloatArrayType(16);

        return function decompose(mat, position, quaternion, scale) {

            vec[0] = mat[0];
            vec[1] = mat[1];
            vec[2] = mat[2];

            let sx = math.lenVec3(vec);

            vec[0] = mat[4];
            vec[1] = mat[5];
            vec[2] = mat[6];

            const sy = math.lenVec3(vec);

            vec[8] = mat[8];
            vec[9] = mat[9];
            vec[10] = mat[10];

            const sz = math.lenVec3(vec);

            // if determine is negative, we need to invert one scale
            const det = math.determinantMat4(mat);

            if (det < 0) {
                sx = -sx;
            }

            position[0] = mat[12];
            position[1] = mat[13];
            position[2] = mat[14];

            // scale the rotation part
            matrix.set(mat);

            const invSX = 1 / sx;
            const invSY = 1 / sy;
            const invSZ = 1 / sz;

            matrix[0] *= invSX;
            matrix[1] *= invSX;
            matrix[2] *= invSX;

            matrix[4] *= invSY;
            matrix[5] *= invSY;
            matrix[6] *= invSY;

            matrix[8] *= invSZ;
            matrix[9] *= invSZ;
            matrix[10] *= invSZ;

            math.mat4ToQuaternion(matrix, quaternion);

            scale[0] = sx;
            scale[1] = sy;
            scale[2] = sz;

            return this;

        };

    })(),

    /** @private */
    getColMat4(mat, c) {
        const i = c * 4;
        return [mat[i], mat[i + 1], mat[i + 2], mat[i + 3]];
    },

    /** @private */
    setRowMat4(mat, r, v) {
        mat[r] = v[0];
        mat[r + 4] = v[1];
        mat[r + 8] = v[2];
        mat[r + 12] = v[3];
    },

    /**
     * Returns a 4x4 'lookat' viewing transform matrix.
     * @method lookAtMat4v
     * @param pos vec3 position of the viewer
     * @param target vec3 point the viewer is looking at
     * @param up vec3 pointing "up"
     * @param dest mat4 Optional, mat4 matrix will be written into
     *
     * @return {mat4} dest if specified, a new mat4 otherwise
     */
    lookAtMat4v(pos, target, up, dest) {
        if (!dest) {
            dest = math.mat4();
        }

        const posx = pos[0];
        const posy = pos[1];
        const posz = pos[2];
        const upx = up[0];
        const upy = up[1];
        const upz = up[2];
        const targetx = target[0];
        const targety = target[1];
        const targetz = target[2];

        if (posx === targetx && posy === targety && posz === targetz) {
            return math.identityMat4();
        }

        let z0;
        let z1;
        let z2;
        let x0;
        let x1;
        let x2;
        let y0;
        let y1;
        let y2;
        let len;

        //vec3.direction(eye, center, z);
        z0 = posx - targetx;
        z1 = posy - targety;
        z2 = posz - targetz;

        // normalize (no check needed for 0 because of early return)
        len = 1 / Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2);
        z0 *= len;
        z1 *= len;
        z2 *= len;

        //vec3.normalize(vec3.cross(up, z, x));
        x0 = upy * z2 - upz * z1;
        x1 = upz * z0 - upx * z2;
        x2 = upx * z1 - upy * z0;
        len = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2);
        if (!len) {
            x0 = 0;
            x1 = 0;
            x2 = 0;
        } else {
            len = 1 / len;
            x0 *= len;
            x1 *= len;
            x2 *= len;
        }

        //vec3.normalize(vec3.cross(z, x, y));
        y0 = z1 * x2 - z2 * x1;
        y1 = z2 * x0 - z0 * x2;
        y2 = z0 * x1 - z1 * x0;

        len = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2);
        if (!len) {
            y0 = 0;
            y1 = 0;
            y2 = 0;
        } else {
            len = 1 / len;
            y0 *= len;
            y1 *= len;
            y2 *= len;
        }

        dest[0] = x0;
        dest[1] = y0;
        dest[2] = z0;
        dest[3] = 0;
        dest[4] = x1;
        dest[5] = y1;
        dest[6] = z1;
        dest[7] = 0;
        dest[8] = x2;
        dest[9] = y2;
        dest[10] = z2;
        dest[11] = 0;
        dest[12] = -(x0 * posx + x1 * posy + x2 * posz);
        dest[13] = -(y0 * posx + y1 * posy + y2 * posz);
        dest[14] = -(z0 * posx + z1 * posy + z2 * posz);
        dest[15] = 1;

        return dest;
    },

    /**
     * Returns a 4x4 'lookat' viewing transform matrix.
     * @method lookAtMat4c
     * @static
     */
    lookAtMat4c(posx, posy, posz, targetx, targety, targetz, upx, upy, upz) {
        return math.lookAtMat4v([posx, posy, posz], [targetx, targety, targetz], [upx, upy, upz], []);
    },

    /**
     * Returns a 4x4 orthographic projection matrix.
     * @method orthoMat4c
     * @static
     */
    orthoMat4c(left, right, bottom, top, near, far, dest) {
        if (!dest) {
            dest = math.mat4();
        }
        const rl = (right - left);
        const tb = (top - bottom);
        const fn = (far - near);

        dest[0] = 2.0 / rl;
        dest[1] = 0.0;
        dest[2] = 0.0;
        dest[3] = 0.0;

        dest[4] = 0.0;
        dest[5] = 2.0 / tb;
        dest[6] = 0.0;
        dest[7] = 0.0;

        dest[8] = 0.0;
        dest[9] = 0.0;
        dest[10] = -2 / fn;
        dest[11] = 0.0;

        dest[12] = -(left + right) / rl;
        dest[13] = -(top + bottom) / tb;
        dest[14] = -(far + near) / fn;
        dest[15] = 1.0;

        return dest;
    },

    /**
     * Returns a 4x4 perspective projection matrix.
     * @method frustumMat4v
     * @static
     */
    frustumMat4v(fmin, fmax, m) {
        if (!m) {
            m = math.mat4();
        }

        const fmin4 = [fmin[0], fmin[1], fmin[2], 0.0];
        const fmax4 = [fmax[0], fmax[1], fmax[2], 0.0];

        math.addVec4(fmax4, fmin4, tempMat1);
        math.subVec4(fmax4, fmin4, tempMat2);

        const t = 2.0 * fmin4[2];

        const tempMat20 = tempMat2[0];
        const tempMat21 = tempMat2[1];
        const tempMat22 = tempMat2[2];

        m[0] = t / tempMat20;
        m[1] = 0.0;
        m[2] = 0.0;
        m[3] = 0.0;

        m[4] = 0.0;
        m[5] = t / tempMat21;
        m[6] = 0.0;
        m[7] = 0.0;

        m[8] = tempMat1[0] / tempMat20;
        m[9] = tempMat1[1] / tempMat21;
        m[10] = -tempMat1[2] / tempMat22;
        m[11] = -1;

        m[12] = 0.0;
        m[13] = 0.0;
        m[14] = -t * fmax4[2] / tempMat22;
        m[15] = 0.0;

        return m;
    },

    /**
     * Returns a 4x4 perspective projection matrix.
     * @method frustumMat4v
     * @static
     */
    frustumMat4(left, right, bottom, top, near, far, dest) {
        if (!dest) {
            dest = math.mat4();
        }
        const rl = (right - left);
        const tb = (top - bottom);
        const fn = (far - near);
        dest[0] = (near * 2) / rl;
        dest[1] = 0;
        dest[2] = 0;
        dest[3] = 0;
        dest[4] = 0;
        dest[5] = (near * 2) / tb;
        dest[6] = 0;
        dest[7] = 0;
        dest[8] = (right + left) / rl;
        dest[9] = (top + bottom) / tb;
        dest[10] = -(far + near) / fn;
        dest[11] = -1;
        dest[12] = 0;
        dest[13] = 0;
        dest[14] = -(far * near * 2) / fn;
        dest[15] = 0;
        return dest;
    },

    /**
     * Returns a 4x4 perspective projection matrix.
     * @method perspectiveMat4v
     * @static
     */
    perspectiveMat4(fovyrad, aspectratio, znear, zfar, m) {
        const pmin = [];
        const pmax = [];

        pmin[2] = znear;
        pmax[2] = zfar;

        pmax[1] = pmin[2] * Math.tan(fovyrad / 2.0);
        pmin[1] = -pmax[1];

        pmax[0] = pmax[1] * aspectratio;
        pmin[0] = -pmax[0];

        return math.frustumMat4v(pmin, pmax, m);
    },

    /**
     * Returns true if the two 4x4 matrices are the same.
     * @param m1
     * @param m2
     * @returns {Boolean}
     */
    compareMat4(m1, m2) {
        return m1[0] === m2[0] &&
            m1[1] === m2[1] &&
            m1[2] === m2[2] &&
            m1[3] === m2[3] &&
            m1[4] === m2[4] &&
            m1[5] === m2[5] &&
            m1[6] === m2[6] &&
            m1[7] === m2[7] &&
            m1[8] === m2[8] &&
            m1[9] === m2[9] &&
            m1[10] === m2[10] &&
            m1[11] === m2[11] &&
            m1[12] === m2[12] &&
            m1[13] === m2[13] &&
            m1[14] === m2[14] &&
            m1[15] === m2[15];
    },

    /**
     * Transforms a three-element position by a 4x4 matrix.
     * @method transformPoint3
     * @static
     */
    transformPoint3(m, p, dest = math.vec3()) {

        const x = p[0];
        const y = p[1];
        const z = p[2];

        dest[0] = (m[0] * x) + (m[4] * y) + (m[8] * z) + m[12];
        dest[1] = (m[1] * x) + (m[5] * y) + (m[9] * z) + m[13];
        dest[2] = (m[2] * x) + (m[6] * y) + (m[10] * z) + m[14];

        return dest;
    },

    /**
     * Transforms a homogeneous coordinate by a 4x4 matrix.
     * @method transformPoint3
     * @static
     */
    transformPoint4(m, v, dest = math.vec4()) {
        dest[0] = m[0] * v[0] + m[4] * v[1] + m[8] * v[2] + m[12] * v[3];
        dest[1] = m[1] * v[0] + m[5] * v[1] + m[9] * v[2] + m[13] * v[3];
        dest[2] = m[2] * v[0] + m[6] * v[1] + m[10] * v[2] + m[14] * v[3];
        dest[3] = m[3] * v[0] + m[7] * v[1] + m[11] * v[2] + m[15] * v[3];

        return dest;
    },


    /**
     * Transforms an array of three-element positions by a 4x4 matrix.
     * @method transformPoints3
     * @static
     */
    transformPoints3(m, points, points2) {
        const result = points2 || [];
        const len = points.length;
        let p0;
        let p1;
        let p2;
        let pi;

        // cache values
        const m0 = m[0];

        const m1 = m[1];
        const m2 = m[2];
        const m3 = m[3];
        const m4 = m[4];
        const m5 = m[5];
        const m6 = m[6];
        const m7 = m[7];
        const m8 = m[8];
        const m9 = m[9];
        const m10 = m[10];
        const m11 = m[11];
        const m12 = m[12];
        const m13 = m[13];
        const m14 = m[14];
        const m15 = m[15];

        let r;

        for (let i = 0; i < len; ++i) {

            // cache values
            pi = points[i];

            p0 = pi[0];
            p1 = pi[1];
            p2 = pi[2];

            r = result[i] || (result[i] = [0, 0, 0]);

            r[0] = (m0 * p0) + (m4 * p1) + (m8 * p2) + m12;
            r[1] = (m1 * p0) + (m5 * p1) + (m9 * p2) + m13;
            r[2] = (m2 * p0) + (m6 * p1) + (m10 * p2) + m14;
            r[3] = (m3 * p0) + (m7 * p1) + (m11 * p2) + m15;
        }

        result.length = len;

        return result;
    },

    /**
     * Transforms an array of positions by a 4x4 matrix.
     * @method transformPositions3
     * @static
     */
    transformPositions3(m, p, p2 = p) {
        let i;
        const len = p.length;

        let x;
        let y;
        let z;

        const m0 = m[0];
        const m1 = m[1];
        const m2 = m[2];
        m[3];
        const m4 = m[4];
        const m5 = m[5];
        const m6 = m[6];
        m[7];
        const m8 = m[8];
        const m9 = m[9];
        const m10 = m[10];
        m[11];
        const m12 = m[12];
        const m13 = m[13];
        const m14 = m[14];
        m[15];

        for (i = 0; i < len; i += 3) {

            x = p[i + 0];
            y = p[i + 1];
            z = p[i + 2];

            p2[i + 0] = (m0 * x) + (m4 * y) + (m8 * z) + m12;
            p2[i + 1] = (m1 * x) + (m5 * y) + (m9 * z) + m13;
            p2[i + 2] = (m2 * x) + (m6 * y) + (m10 * z) + m14;
        }

        return p2;
    },

    /**
     * Transforms an array of positions by a 4x4 matrix.
     * @method transformPositions4
     * @static
     */
    transformPositions4(m, p, p2 = p) {
        let i;
        const len = p.length;

        let x;
        let y;
        let z;

        const m0 = m[0];
        const m1 = m[1];
        const m2 = m[2];
        const m3 = m[3];
        const m4 = m[4];
        const m5 = m[5];
        const m6 = m[6];
        const m7 = m[7];
        const m8 = m[8];
        const m9 = m[9];
        const m10 = m[10];
        const m11 = m[11];
        const m12 = m[12];
        const m13 = m[13];
        const m14 = m[14];
        const m15 = m[15];

        for (i = 0; i < len; i += 4) {

            x = p[i + 0];
            y = p[i + 1];
            z = p[i + 2];

            p2[i + 0] = (m0 * x) + (m4 * y) + (m8 * z) + m12;
            p2[i + 1] = (m1 * x) + (m5 * y) + (m9 * z) + m13;
            p2[i + 2] = (m2 * x) + (m6 * y) + (m10 * z) + m14;
            p2[i + 3] = (m3 * x) + (m7 * y) + (m11 * z) + m15;
        }

        return p2;
    },

    /**
     * Transforms a three-element vector by a 4x4 matrix.
     * @method transformVec3
     * @static
     */
    transformVec3(m, v, dest) {
        const v0 = v[0];
        const v1 = v[1];
        const v2 = v[2];
        dest = dest || this.vec3();
        dest[0] = (m[0] * v0) + (m[4] * v1) + (m[8] * v2);
        dest[1] = (m[1] * v0) + (m[5] * v1) + (m[9] * v2);
        dest[2] = (m[2] * v0) + (m[6] * v1) + (m[10] * v2);
        return dest;
    },

    /**
     * Transforms a four-element vector by a 4x4 matrix.
     * @method transformVec4
     * @static
     */
    transformVec4(m, v, dest) {
        const v0 = v[0];
        const v1 = v[1];
        const v2 = v[2];
        const v3 = v[3];
        dest = dest || math.vec4();
        dest[0] = m[0] * v0 + m[4] * v1 + m[8] * v2 + m[12] * v3;
        dest[1] = m[1] * v0 + m[5] * v1 + m[9] * v2 + m[13] * v3;
        dest[2] = m[2] * v0 + m[6] * v1 + m[10] * v2 + m[14] * v3;
        dest[3] = m[3] * v0 + m[7] * v1 + m[11] * v2 + m[15] * v3;
        return dest;
    },

    /**
     * Rotate a 2D vector around a center point.
     *
     * @param a
     * @param center
     * @param angle
     * @returns {math}
     */
    rotateVec2(a, center, angle, dest = a) {
        const c = Math.cos(angle);
        const s = Math.sin(angle);
        const x = a[0] - center[0];
        const y = a[1] - center[1];
        dest[0] = x * c - y * s + center[0];
        dest[1] = x * s + y * c + center[1];
        return a;
    },

    /**
     * Rotate a 3D vector around the x-axis
     *
     * @method rotateVec3X
     * @param {Number[]} a The vec3 point to rotate
     * @param {Number[]} b The origin of the rotation
     * @param {Number} c The angle of rotation
     * @param {Number[]} dest The receiving vec3
     * @returns {Number[]} dest
     * @static
     */
    rotateVec3X(a, b, c, dest) {
        const p = [];
        const r = [];

        //Translate point to the origin
        p[0] = a[0] - b[0];
        p[1] = a[1] - b[1];
        p[2] = a[2] - b[2];

        //perform rotation
        r[0] = p[0];
        r[1] = p[1] * Math.cos(c) - p[2] * Math.sin(c);
        r[2] = p[1] * Math.sin(c) + p[2] * Math.cos(c);

        //translate to correct position
        dest[0] = r[0] + b[0];
        dest[1] = r[1] + b[1];
        dest[2] = r[2] + b[2];

        return dest;
    },

    /**
     * Rotate a 3D vector around the y-axis
     *
     * @method rotateVec3Y
     * @param {Number[]} a The vec3 point to rotate
     * @param {Number[]} b The origin of the rotation
     * @param {Number} c The angle of rotation
     * @param {Number[]} dest The receiving vec3
     * @returns {Number[]} dest
     * @static
     */
    rotateVec3Y(a, b, c, dest) {
        const p = [];
        const r = [];

        //Translate point to the origin
        p[0] = a[0] - b[0];
        p[1] = a[1] - b[1];
        p[2] = a[2] - b[2];

        //perform rotation
        r[0] = p[2] * Math.sin(c) + p[0] * Math.cos(c);
        r[1] = p[1];
        r[2] = p[2] * Math.cos(c) - p[0] * Math.sin(c);

        //translate to correct position
        dest[0] = r[0] + b[0];
        dest[1] = r[1] + b[1];
        dest[2] = r[2] + b[2];

        return dest;
    },

    /**
     * Rotate a 3D vector around the z-axis
     *
     * @method rotateVec3Z
     * @param {Number[]} a The vec3 point to rotate
     * @param {Number[]} b The origin of the rotation
     * @param {Number} c The angle of rotation
     * @param {Number[]} dest The receiving vec3
     * @returns {Number[]} dest
     * @static
     */
    rotateVec3Z(a, b, c, dest) {
        const p = [];
        const r = [];

        //Translate point to the origin
        p[0] = a[0] - b[0];
        p[1] = a[1] - b[1];
        p[2] = a[2] - b[2];

        //perform rotation
        r[0] = p[0] * Math.cos(c) - p[1] * Math.sin(c);
        r[1] = p[0] * Math.sin(c) + p[1] * Math.cos(c);
        r[2] = p[2];

        //translate to correct position
        dest[0] = r[0] + b[0];
        dest[1] = r[1] + b[1];
        dest[2] = r[2] + b[2];

        return dest;
    },

    /**
     * Transforms a four-element vector by a 4x4 projection matrix.
     *
     * @method projectVec4
     * @param {Number[]} p 3D View-space coordinate
     * @param {Number[]} q 2D Projected coordinate
     * @returns {Number[]} 2D Projected coordinate
     * @static
     */
    projectVec4(p, q) {
        const f = 1.0 / p[3];
        q = q || math.vec2();
        q[0] = p[0] * f;
        q[1] = p[1] * f;
        return q;
    },

    /**
     * Unprojects a three-element vector.
     *
     * @method unprojectVec3
     * @param {Number[]} p 3D Projected coordinate
     * @param {Number[]} viewMat View matrix
     * @returns {Number[]} projMat Projection matrix
     * @static
     */
    unprojectVec3: ((() => {
        const mat = new FloatArrayType(16);
        const mat2 = new FloatArrayType(16);
        const mat3 = new FloatArrayType(16);
        return function (p, viewMat, projMat, q) {
            return this.transformVec3(this.mulMat4(this.inverseMat4(viewMat, mat), this.inverseMat4(projMat, mat2), mat3), p, q)
        };
    }))(),

    /**
     * Linearly interpolates between two 3D vectors.
     * @method lerpVec3
     * @static
     */
    lerpVec3(t, t1, t2, p1, p2, dest) {
        const result = dest || math.vec3();
        const f = (t - t1) / (t2 - t1);
        result[0] = p1[0] + (f * (p2[0] - p1[0]));
        result[1] = p1[1] + (f * (p2[1] - p1[1]));
        result[2] = p1[2] + (f * (p2[2] - p1[2]));
        return result;
    },

    /**
     * Linearly interpolates between two 4x4 matrices.
     * @method lerpMat4
     * @static
     */
    lerpMat4(t, t1, t2, m1, m2, dest) {
        const result = dest || math.mat4();
        const f = (t - t1) / (t2 - t1);
        result[0] = m1[0] + (f * (m2[0] - m1[0]));
        result[1] = m1[1] + (f * (m2[1] - m1[1]));
        result[2] = m1[2] + (f * (m2[2] - m1[2]));
        result[3] = m1[3] + (f * (m2[3] - m1[3]));
        result[4] = m1[4] + (f * (m2[4] - m1[4]));
        result[5] = m1[5] + (f * (m2[5] - m1[5]));
        result[6] = m1[6] + (f * (m2[6] - m1[6]));
        result[7] = m1[7] + (f * (m2[7] - m1[7]));
        result[8] = m1[8] + (f * (m2[8] - m1[8]));
        result[9] = m1[9] + (f * (m2[9] - m1[9]));
        result[10] = m1[10] + (f * (m2[10] - m1[10]));
        result[11] = m1[11] + (f * (m2[11] - m1[11]));
        result[12] = m1[12] + (f * (m2[12] - m1[12]));
        result[13] = m1[13] + (f * (m2[13] - m1[13]));
        result[14] = m1[14] + (f * (m2[14] - m1[14]));
        result[15] = m1[15] + (f * (m2[15] - m1[15]));
        return result;
    },


    /**
     * Flattens a two-dimensional array into a one-dimensional array.
     *
     * @method flatten
     * @static
     * @param {Array of Arrays} a A 2D array
     * @returns Flattened 1D array
     */
    flatten(a) {

        const result = [];

        let i;
        let leni;
        let j;
        let lenj;
        let item;

        for (i = 0, leni = a.length; i < leni; i++) {
            item = a[i];
            for (j = 0, lenj = item.length; j < lenj; j++) {
                result.push(item[j]);
            }
        }

        return result;
    },


    identityQuaternion(dest = math.vec4()) {
        dest[0] = 0.0;
        dest[1] = 0.0;
        dest[2] = 0.0;
        dest[3] = 1.0;
        return dest;
    },

    /**
     * Initializes a quaternion from Euler angles.
     *
     * @param {Number[]} euler The Euler angles (in degrees).
     * @param {String} order Euler angle order: "XYZ", "YXZ", "ZXY" etc.
     * @param {Number[]} [dest] Destination quaternion, created by default.
     * @returns {Number[]} The quaternion.
     */
    eulerToQuaternion(euler, order, dest = math.vec4()) {
        // http://www.mathworks.com/matlabcentral/fileexchange/
        // 	20696-function-to-convert-between-dcm-euler-angles-quaternions-and-euler-vectors/
        //	content/SpinCalc.m

        const a = (euler[0] * math.DEGTORAD) / 2;
        const b = (euler[1] * math.DEGTORAD) / 2;
        const c = (euler[2] * math.DEGTORAD) / 2;

        const c1 = Math.cos(a);
        const c2 = Math.cos(b);
        const c3 = Math.cos(c);
        const s1 = Math.sin(a);
        const s2 = Math.sin(b);
        const s3 = Math.sin(c);

        if (order === 'XYZ') {

            dest[0] = s1 * c2 * c3 + c1 * s2 * s3;
            dest[1] = c1 * s2 * c3 - s1 * c2 * s3;
            dest[2] = c1 * c2 * s3 + s1 * s2 * c3;
            dest[3] = c1 * c2 * c3 - s1 * s2 * s3;

        } else if (order === 'YXZ') {

            dest[0] = s1 * c2 * c3 + c1 * s2 * s3;
            dest[1] = c1 * s2 * c3 - s1 * c2 * s3;
            dest[2] = c1 * c2 * s3 - s1 * s2 * c3;
            dest[3] = c1 * c2 * c3 + s1 * s2 * s3;

        } else if (order === 'ZXY') {

            dest[0] = s1 * c2 * c3 - c1 * s2 * s3;
            dest[1] = c1 * s2 * c3 + s1 * c2 * s3;
            dest[2] = c1 * c2 * s3 + s1 * s2 * c3;
            dest[3] = c1 * c2 * c3 - s1 * s2 * s3;

        } else if (order === 'ZYX') {

            dest[0] = s1 * c2 * c3 - c1 * s2 * s3;
            dest[1] = c1 * s2 * c3 + s1 * c2 * s3;
            dest[2] = c1 * c2 * s3 - s1 * s2 * c3;
            dest[3] = c1 * c2 * c3 + s1 * s2 * s3;

        } else if (order === 'YZX') {

            dest[0] = s1 * c2 * c3 + c1 * s2 * s3;
            dest[1] = c1 * s2 * c3 + s1 * c2 * s3;
            dest[2] = c1 * c2 * s3 - s1 * s2 * c3;
            dest[3] = c1 * c2 * c3 - s1 * s2 * s3;

        } else if (order === 'XZY') {

            dest[0] = s1 * c2 * c3 - c1 * s2 * s3;
            dest[1] = c1 * s2 * c3 - s1 * c2 * s3;
            dest[2] = c1 * c2 * s3 + s1 * s2 * c3;
            dest[3] = c1 * c2 * c3 + s1 * s2 * s3;
        }

        return dest;
    },

    mat4ToQuaternion(m, dest = math.vec4()) {
        // http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm

        // Assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)

        const m11 = m[0];
        const m12 = m[4];
        const m13 = m[8];
        const m21 = m[1];
        const m22 = m[5];
        const m23 = m[9];
        const m31 = m[2];
        const m32 = m[6];
        const m33 = m[10];
        let s;

        const trace = m11 + m22 + m33;

        if (trace > 0) {

            s = 0.5 / Math.sqrt(trace + 1.0);

            dest[3] = 0.25 / s;
            dest[0] = (m32 - m23) * s;
            dest[1] = (m13 - m31) * s;
            dest[2] = (m21 - m12) * s;

        } else if (m11 > m22 && m11 > m33) {

            s = 2.0 * Math.sqrt(1.0 + m11 - m22 - m33);

            dest[3] = (m32 - m23) / s;
            dest[0] = 0.25 * s;
            dest[1] = (m12 + m21) / s;
            dest[2] = (m13 + m31) / s;

        } else if (m22 > m33) {

            s = 2.0 * Math.sqrt(1.0 + m22 - m11 - m33);

            dest[3] = (m13 - m31) / s;
            dest[0] = (m12 + m21) / s;
            dest[1] = 0.25 * s;
            dest[2] = (m23 + m32) / s;

        } else {

            s = 2.0 * Math.sqrt(1.0 + m33 - m11 - m22);

            dest[3] = (m21 - m12) / s;
            dest[0] = (m13 + m31) / s;
            dest[1] = (m23 + m32) / s;
            dest[2] = 0.25 * s;
        }

        return dest;
    },

    vec3PairToQuaternion(u, v, dest = math.vec4()) {
        const norm_u_norm_v = Math.sqrt(math.dotVec3(u, u) * math.dotVec3(v, v));
        let real_part = norm_u_norm_v + math.dotVec3(u, v);

        if (real_part < 0.00000001 * norm_u_norm_v) {

            // If u and v are exactly opposite, rotate 180 degrees
            // around an arbitrary orthogonal axis. Axis normalisation
            // can happen later, when we normalise the quaternion.

            real_part = 0.0;

            if (Math.abs(u[0]) > Math.abs(u[2])) {

                dest[0] = -u[1];
                dest[1] = u[0];
                dest[2] = 0;

            } else {
                dest[0] = 0;
                dest[1] = -u[2];
                dest[2] = u[1];
            }

        } else {

            // Otherwise, build quaternion the standard way.
            math.cross3Vec3(u, v, dest);
        }

        dest[3] = real_part;

        return math.normalizeQuaternion(dest);
    },

    angleAxisToQuaternion(angleAxis, dest = math.vec4()) {
        const halfAngle = angleAxis[3] / 2.0;
        const fsin = Math.sin(halfAngle);
        dest[0] = fsin * angleAxis[0];
        dest[1] = fsin * angleAxis[1];
        dest[2] = fsin * angleAxis[2];
        dest[3] = Math.cos(halfAngle);
        return dest;
    },

    quaternionToEuler: ((() => {
        const mat = new FloatArrayType(16);
        return (q, order, dest) => {
            dest = dest || math.vec3();
            math.quaternionToRotationMat4(q, mat);
            math.mat4ToEuler(mat, order, dest);
            return dest;
        };
    }))(),

    mulQuaternions(p, q, dest = math.vec4()) {
        const p0 = p[0];
        const p1 = p[1];
        const p2 = p[2];
        const p3 = p[3];
        const q0 = q[0];
        const q1 = q[1];
        const q2 = q[2];
        const q3 = q[3];
        dest[0] = p3 * q0 + p0 * q3 + p1 * q2 - p2 * q1;
        dest[1] = p3 * q1 + p1 * q3 + p2 * q0 - p0 * q2;
        dest[2] = p3 * q2 + p2 * q3 + p0 * q1 - p1 * q0;
        dest[3] = p3 * q3 - p0 * q0 - p1 * q1 - p2 * q2;
        return dest;
    },

    vec3ApplyQuaternion(q, vec, dest = math.vec3()) {
        const x = vec[0];
        const y = vec[1];
        const z = vec[2];

        const qx = q[0];
        const qy = q[1];
        const qz = q[2];
        const qw = q[3];

        // calculate quat * vector

        const ix = qw * x + qy * z - qz * y;
        const iy = qw * y + qz * x - qx * z;
        const iz = qw * z + qx * y - qy * x;
        const iw = -qx * x - qy * y - qz * z;

        // calculate result * inverse quat

        dest[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy;
        dest[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz;
        dest[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx;

        return dest;
    },

    quaternionToMat4(q, dest) {

        dest = math.identityMat4(dest);

        const q0 = q[0];  //x
        const q1 = q[1];  //y
        const q2 = q[2];  //z
        const q3 = q[3];  //w

        const tx = 2.0 * q0;
        const ty = 2.0 * q1;
        const tz = 2.0 * q2;

        const twx = tx * q3;
        const twy = ty * q3;
        const twz = tz * q3;

        const txx = tx * q0;
        const txy = ty * q0;
        const txz = tz * q0;

        const tyy = ty * q1;
        const tyz = tz * q1;
        const tzz = tz * q2;

        dest[0] = 1.0 - (tyy + tzz);
        dest[1] = txy + twz;
        dest[2] = txz - twy;

        dest[4] = txy - twz;
        dest[5] = 1.0 - (txx + tzz);
        dest[6] = tyz + twx;

        dest[8] = txz + twy;
        dest[9] = tyz - twx;

        dest[10] = 1.0 - (txx + tyy);

        return dest;
    },

    quaternionToRotationMat4(q, m) {
        const x = q[0];
        const y = q[1];
        const z = q[2];
        const w = q[3];

        const x2 = x + x;
        const y2 = y + y;
        const z2 = z + z;
        const xx = x * x2;
        const xy = x * y2;
        const xz = x * z2;
        const yy = y * y2;
        const yz = y * z2;
        const zz = z * z2;
        const wx = w * x2;
        const wy = w * y2;
        const wz = w * z2;

        m[0] = 1 - (yy + zz);
        m[4] = xy - wz;
        m[8] = xz + wy;

        m[1] = xy + wz;
        m[5] = 1 - (xx + zz);
        m[9] = yz - wx;

        m[2] = xz - wy;
        m[6] = yz + wx;
        m[10] = 1 - (xx + yy);

        // last column
        m[3] = 0;
        m[7] = 0;
        m[11] = 0;

        // bottom row
        m[12] = 0;
        m[13] = 0;
        m[14] = 0;
        m[15] = 1;

        return m;
    },

    normalizeQuaternion(q, dest = q) {
        const len = math.lenVec4([q[0], q[1], q[2], q[3]]);
        dest[0] = q[0] / len;
        dest[1] = q[1] / len;
        dest[2] = q[2] / len;
        dest[3] = q[3] / len;
        return dest;
    },

    conjugateQuaternion(q, dest = q) {
        dest[0] = -q[0];
        dest[1] = -q[1];
        dest[2] = -q[2];
        dest[3] = q[3];
        return dest;
    },

    inverseQuaternion(q, dest) {
        return math.normalizeQuaternion(math.conjugateQuaternion(q, dest));
    },

    quaternionToAngleAxis(q, angleAxis = math.vec4()) {
        q = math.normalizeQuaternion(q, tempVec4$2);
        const q3 = q[3];
        const angle = 2 * Math.acos(q3);
        const s = Math.sqrt(1 - q3 * q3);
        if (s < 0.001) { // test to avoid divide by zero, s is always positive due to sqrt
            angleAxis[0] = q[0];
            angleAxis[1] = q[1];
            angleAxis[2] = q[2];
        } else {
            angleAxis[0] = q[0] / s;
            angleAxis[1] = q[1] / s;
            angleAxis[2] = q[2] / s;
        }
        angleAxis[3] = angle; // * 57.295779579;
        return angleAxis;
    },

    //------------------------------------------------------------------------------------------------------------------
    // Boundaries
    //------------------------------------------------------------------------------------------------------------------

    /**
     * Returns a new, uninitialized 3D axis-aligned bounding box.
     *
     * @private
     */
    AABB3(values) {
        return new FloatArrayType(values || 6);
    },

    /**
     * Returns a new, uninitialized 2D axis-aligned bounding box.
     *
     * @private
     */
    AABB2(values) {
        return new FloatArrayType(values || 4);
    },

    /**
     * Returns a new, uninitialized 3D oriented bounding box (OBB).
     *
     * @private
     */
    OBB3(values) {
        return new FloatArrayType(values || 32);
    },

    /**
     * Returns a new, uninitialized 2D oriented bounding box (OBB).
     *
     * @private
     */
    OBB2(values) {
        return new FloatArrayType(values || 16);
    },

    /** Returns a new 3D bounding sphere */
    Sphere3(x, y, z, r) {
        return new FloatArrayType([x, y, z, r]);
    },

    /**
     * Transforms an OBB3 by a 4x4 matrix.
     *
     * @private
     */
    transformOBB3(m, p, p2 = p) {
        let i;
        const len = p.length;

        let x;
        let y;
        let z;

        const m0 = m[0];
        const m1 = m[1];
        const m2 = m[2];
        const m3 = m[3];
        const m4 = m[4];
        const m5 = m[5];
        const m6 = m[6];
        const m7 = m[7];
        const m8 = m[8];
        const m9 = m[9];
        const m10 = m[10];
        const m11 = m[11];
        const m12 = m[12];
        const m13 = m[13];
        const m14 = m[14];
        const m15 = m[15];

        for (i = 0; i < len; i += 4) {

            x = p[i + 0];
            y = p[i + 1];
            z = p[i + 2];

            p2[i + 0] = (m0 * x) + (m4 * y) + (m8 * z) + m12;
            p2[i + 1] = (m1 * x) + (m5 * y) + (m9 * z) + m13;
            p2[i + 2] = (m2 * x) + (m6 * y) + (m10 * z) + m14;
            p2[i + 3] = (m3 * x) + (m7 * y) + (m11 * z) + m15;
        }

        return p2;
    },

    /** Returns true if the first AABB contains the second AABB.
     * @param aabb1
     * @param aabb2
     * @returns {Boolean}
     */
    containsAABB3: function (aabb1, aabb2) {
        const result = (
            aabb1[0] <= aabb2[0] && aabb2[3] <= aabb1[3] &&
            aabb1[1] <= aabb2[1] && aabb2[4] <= aabb1[4] &&
            aabb1[2] <= aabb2[2] && aabb2[5] <= aabb1[5]);
        return result;
    },


    /**
     * Gets the diagonal size of an AABB3 given as minima and maxima.
     *
     * @private
     */
    getAABB3Diag: ((() => {

        const min = new FloatArrayType(3);
        const max = new FloatArrayType(3);
        const tempVec3 = new FloatArrayType(3);

        return aabb => {

            min[0] = aabb[0];
            min[1] = aabb[1];
            min[2] = aabb[2];

            max[0] = aabb[3];
            max[1] = aabb[4];
            max[2] = aabb[5];

            math.subVec3(max, min, tempVec3);

            return Math.abs(math.lenVec3(tempVec3));
        };
    }))(),

    /**
     * Get a diagonal boundary size that is symmetrical about the given point.
     *
     * @private
     */
    getAABB3DiagPoint: ((() => {

        const min = new FloatArrayType(3);
        const max = new FloatArrayType(3);
        const tempVec3 = new FloatArrayType(3);

        return (aabb, p) => {

            min[0] = aabb[0];
            min[1] = aabb[1];
            min[2] = aabb[2];

            max[0] = aabb[3];
            max[1] = aabb[4];
            max[2] = aabb[5];

            const diagVec = math.subVec3(max, min, tempVec3);

            const xneg = p[0] - aabb[0];
            const xpos = aabb[3] - p[0];
            const yneg = p[1] - aabb[1];
            const ypos = aabb[4] - p[1];
            const zneg = p[2] - aabb[2];
            const zpos = aabb[5] - p[2];

            diagVec[0] += (xneg > xpos) ? xneg : xpos;
            diagVec[1] += (yneg > ypos) ? yneg : ypos;
            diagVec[2] += (zneg > zpos) ? zneg : zpos;

            return Math.abs(math.lenVec3(diagVec));
        };
    }))(),

    /**
     * Gets the area of an AABB.
     *
     * @private
     */
    getAABB3Area(aabb) {
        const width = (aabb[3] - aabb[0]);
        const height = (aabb[4] - aabb[1]);
        const depth = (aabb[5] - aabb[2]);
        return (width * height * depth);
    },

    /**
     * Gets the center of an AABB.
     *
     * @private
     */
    getAABB3Center(aabb, dest) {
        const r = dest || math.vec3();

        r[0] = (aabb[0] + aabb[3]) / 2;
        r[1] = (aabb[1] + aabb[4]) / 2;
        r[2] = (aabb[2] + aabb[5]) / 2;

        return r;
    },

    /**
     * Gets the center of a 2D AABB.
     *
     * @private
     */
    getAABB2Center(aabb, dest) {
        const r = dest || math.vec2();

        r[0] = (aabb[2] + aabb[0]) / 2;
        r[1] = (aabb[3] + aabb[1]) / 2;

        return r;
    },

    /**
     * Collapses a 3D axis-aligned boundary, ready to expand to fit 3D points.
     * Creates new AABB if none supplied.
     *
     * @private
     */
    collapseAABB3(aabb = math.AABB3()) {
        aabb[0] = math.MAX_DOUBLE;
        aabb[1] = math.MAX_DOUBLE;
        aabb[2] = math.MAX_DOUBLE;
        aabb[3] = math.MIN_DOUBLE;
        aabb[4] = math.MIN_DOUBLE;
        aabb[5] = math.MIN_DOUBLE;

        return aabb;
    },

    /**
     * Converts an axis-aligned 3D boundary into an oriented boundary consisting of
     * an array of eight 3D positions, one for each corner of the boundary.
     *
     * @private
     */
    AABB3ToOBB3(aabb, obb = math.OBB3()) {
        obb[0] = aabb[0];
        obb[1] = aabb[1];
        obb[2] = aabb[2];
        obb[3] = 1;

        obb[4] = aabb[3];
        obb[5] = aabb[1];
        obb[6] = aabb[2];
        obb[7] = 1;

        obb[8] = aabb[3];
        obb[9] = aabb[4];
        obb[10] = aabb[2];
        obb[11] = 1;

        obb[12] = aabb[0];
        obb[13] = aabb[4];
        obb[14] = aabb[2];
        obb[15] = 1;

        obb[16] = aabb[0];
        obb[17] = aabb[1];
        obb[18] = aabb[5];
        obb[19] = 1;

        obb[20] = aabb[3];
        obb[21] = aabb[1];
        obb[22] = aabb[5];
        obb[23] = 1;

        obb[24] = aabb[3];
        obb[25] = aabb[4];
        obb[26] = aabb[5];
        obb[27] = 1;

        obb[28] = aabb[0];
        obb[29] = aabb[4];
        obb[30] = aabb[5];
        obb[31] = 1;

        return obb;
    },

    /**
     * Finds the minimum axis-aligned 3D boundary enclosing the homogeneous 3D points (x,y,z,w) given in a flattened array.
     *
     * @private
     */
    positions3ToAABB3: ((() => {

        const p = new FloatArrayType(3);

        return (positions, aabb, positionsDecodeMatrix) => {
            aabb = aabb || math.AABB3();

            let xmin = math.MAX_DOUBLE;
            let ymin = math.MAX_DOUBLE;
            let zmin = math.MAX_DOUBLE;
            let xmax = math.MIN_DOUBLE;
            let ymax = math.MIN_DOUBLE;
            let zmax = math.MIN_DOUBLE;

            let x;
            let y;
            let z;

            for (let i = 0, len = positions.length; i < len; i += 3) {

                if (positionsDecodeMatrix) {

                    p[0] = positions[i + 0];
                    p[1] = positions[i + 1];
                    p[2] = positions[i + 2];

                    math.decompressPosition(p, positionsDecodeMatrix, p);

                    x = p[0];
                    y = p[1];
                    z = p[2];

                } else {
                    x = positions[i + 0];
                    y = positions[i + 1];
                    z = positions[i + 2];
                }

                if (x < xmin) {
                    xmin = x;
                }

                if (y < ymin) {
                    ymin = y;
                }

                if (z < zmin) {
                    zmin = z;
                }

                if (x > xmax) {
                    xmax = x;
                }

                if (y > ymax) {
                    ymax = y;
                }

                if (z > zmax) {
                    zmax = z;
                }
            }

            aabb[0] = xmin;
            aabb[1] = ymin;
            aabb[2] = zmin;
            aabb[3] = xmax;
            aabb[4] = ymax;
            aabb[5] = zmax;

            return aabb;
        };
    }))(),

    /**
     * Finds the minimum axis-aligned 3D boundary enclosing the homogeneous 3D points (x,y,z,w) given in a flattened array.
     *
     * @private
     */
    OBB3ToAABB3(obb, aabb = math.AABB3()) {
        let xmin = math.MAX_DOUBLE;
        let ymin = math.MAX_DOUBLE;
        let zmin = math.MAX_DOUBLE;
        let xmax = math.MIN_DOUBLE;
        let ymax = math.MIN_DOUBLE;
        let zmax = math.MIN_DOUBLE;

        let x;
        let y;
        let z;

        for (let i = 0, len = obb.length; i < len; i += 4) {

            x = obb[i + 0];
            y = obb[i + 1];
            z = obb[i + 2];

            if (x < xmin) {
                xmin = x;
            }

            if (y < ymin) {
                ymin = y;
            }

            if (z < zmin) {
                zmin = z;
            }

            if (x > xmax) {
                xmax = x;
            }

            if (y > ymax) {
                ymax = y;
            }

            if (z > zmax) {
                zmax = z;
            }
        }

        aabb[0] = xmin;
        aabb[1] = ymin;
        aabb[2] = zmin;
        aabb[3] = xmax;
        aabb[4] = ymax;
        aabb[5] = zmax;

        return aabb;
    },

    /**
     * Finds the minimum axis-aligned 3D boundary enclosing the given 3D points.
     *
     * @private
     */
    points3ToAABB3(points, aabb = math.AABB3()) {
        let xmin = math.MAX_DOUBLE;
        let ymin = math.MAX_DOUBLE;
        let zmin = math.MAX_DOUBLE;
        let xmax = math.MIN_DOUBLE;
        let ymax = math.MIN_DOUBLE;
        let zmax = math.MIN_DOUBLE;

        let x;
        let y;
        let z;

        for (let i = 0, len = points.length; i < len; i++) {

            x = points[i][0];
            y = points[i][1];
            z = points[i][2];

            if (x < xmin) {
                xmin = x;
            }

            if (y < ymin) {
                ymin = y;
            }

            if (z < zmin) {
                zmin = z;
            }

            if (x > xmax) {
                xmax = x;
            }

            if (y > ymax) {
                ymax = y;
            }

            if (z > zmax) {
                zmax = z;
            }
        }

        aabb[0] = xmin;
        aabb[1] = ymin;
        aabb[2] = zmin;
        aabb[3] = xmax;
        aabb[4] = ymax;
        aabb[5] = zmax;

        return aabb;
    },

    /**
     * Finds the minimum boundary sphere enclosing the given 3D points.
     *
     * @private
     */
    points3ToSphere3: ((() => {

        const tempVec3 = new FloatArrayType(3);

        return (points, sphere) => {

            sphere = sphere || math.vec4();

            let x = 0;
            let y = 0;
            let z = 0;

            let i;
            const numPoints = points.length;

            for (i = 0; i < numPoints; i++) {
                x += points[i][0];
                y += points[i][1];
                z += points[i][2];
            }

            sphere[0] = x / numPoints;
            sphere[1] = y / numPoints;
            sphere[2] = z / numPoints;

            let radius = 0;
            let dist;

            for (i = 0; i < numPoints; i++) {

                dist = Math.abs(math.lenVec3(math.subVec3(points[i], sphere, tempVec3)));

                if (dist > radius) {
                    radius = dist;
                }
            }

            sphere[3] = radius;

            return sphere;
        };
    }))(),

    /**
     * Finds the minimum boundary sphere enclosing the given 3D positions.
     *
     * @private
     */
    positions3ToSphere3: ((() => {

        const tempVec3a = new FloatArrayType(3);
        const tempVec3b = new FloatArrayType(3);

        return (positions, sphere) => {

            sphere = sphere || math.vec4();

            let x = 0;
            let y = 0;
            let z = 0;

            let i;
            const lenPositions = positions.length;
            let radius = 0;

            for (i = 0; i < lenPositions; i += 3) {
                x += positions[i];
                y += positions[i + 1];
                z += positions[i + 2];
            }

            const numPositions = lenPositions / 3;

            sphere[0] = x / numPositions;
            sphere[1] = y / numPositions;
            sphere[2] = z / numPositions;

            let dist;

            for (i = 0; i < lenPositions; i += 3) {

                tempVec3a[0] = positions[i];
                tempVec3a[1] = positions[i + 1];
                tempVec3a[2] = positions[i + 2];

                dist = Math.abs(math.lenVec3(math.subVec3(tempVec3a, sphere, tempVec3b)));

                if (dist > radius) {
                    radius = dist;
                }
            }

            sphere[3] = radius;

            return sphere;
        };
    }))(),

    /**
     * Finds the minimum boundary sphere enclosing the given 3D points.
     *
     * @private
     */
    OBB3ToSphere3: ((() => {

        const point = new FloatArrayType(3);
        const tempVec3 = new FloatArrayType(3);

        return (points, sphere) => {

            sphere = sphere || math.vec4();

            let x = 0;
            let y = 0;
            let z = 0;

            let i;
            const lenPoints = points.length;
            const numPoints = lenPoints / 4;

            for (i = 0; i < lenPoints; i += 4) {
                x += points[i + 0];
                y += points[i + 1];
                z += points[i + 2];
            }

            sphere[0] = x / numPoints;
            sphere[1] = y / numPoints;
            sphere[2] = z / numPoints;

            let radius = 0;
            let dist;

            for (i = 0; i < lenPoints; i += 4) {

                point[0] = points[i + 0];
                point[1] = points[i + 1];
                point[2] = points[i + 2];

                dist = Math.abs(math.lenVec3(math.subVec3(point, sphere, tempVec3)));

                if (dist > radius) {
                    radius = dist;
                }
            }

            sphere[3] = radius;

            return sphere;
        };
    }))(),

    /**
     * Gets the center of a bounding sphere.
     *
     * @private
     */
    getSphere3Center(sphere, dest = math.vec3()) {
        dest[0] = sphere[0];
        dest[1] = sphere[1];
        dest[2] = sphere[2];

        return dest;
    },

    /**
     * Gets the 3D center of the given flat array of 3D positions.
     *
     * @private
     */
    getPositionsCenter(positions, center = math.vec3()) {
        let xCenter = 0;
        let yCenter = 0;
        let zCenter = 0;
        for (var i = 0, len = positions.length; i < len; i += 3) {
            xCenter += positions[i + 0];
            yCenter += positions[i + 1];
            zCenter += positions[i + 2];
        }
        const numPositions = positions.length / 3;
        center[0] = xCenter / numPositions;
        center[1] = yCenter / numPositions;
        center[2] = zCenter / numPositions;
        return center;
    },

    /**
     * Expands the first axis-aligned 3D boundary to enclose the second, if required.
     *
     * @private
     */
    expandAABB3(aabb1, aabb2) {

        if (aabb1[0] > aabb2[0]) {
            aabb1[0] = aabb2[0];
        }

        if (aabb1[1] > aabb2[1]) {
            aabb1[1] = aabb2[1];
        }

        if (aabb1[2] > aabb2[2]) {
            aabb1[2] = aabb2[2];
        }

        if (aabb1[3] < aabb2[3]) {
            aabb1[3] = aabb2[3];
        }

        if (aabb1[4] < aabb2[4]) {
            aabb1[4] = aabb2[4];
        }

        if (aabb1[5] < aabb2[5]) {
            aabb1[5] = aabb2[5];
        }

        return aabb1;
    },

    /**
     * Expands an axis-aligned 3D boundary to enclose the given point, if needed.
     *
     * @private
     */
    expandAABB3Point3(aabb, p) {

        if (aabb[0] > p[0]) {
            aabb[0] = p[0];
        }

        if (aabb[1] > p[1]) {
            aabb[1] = p[1];
        }

        if (aabb[2] > p[2]) {
            aabb[2] = p[2];
        }

        if (aabb[3] < p[0]) {
            aabb[3] = p[0];
        }

        if (aabb[4] < p[1]) {
            aabb[4] = p[1];
        }

        if (aabb[5] < p[2]) {
            aabb[5] = p[2];
        }

        return aabb;
    },

    /**
     * Expands an axis-aligned 3D boundary to enclose the given points, if needed.
     *
     * @private
     */
    expandAABB3Points3(aabb, positions) {
        var x;
        var y;
        var z;
        for (var i = 0, len = positions.length; i < len; i += 3) {
            x = positions[i];
            y = positions[i + 1];
            z = positions[i + 2];
            if (aabb[0] > x) {
                aabb[0] = x;
            }
            if (aabb[1] > y) {
                aabb[1] = y;
            }
            if (aabb[2] > z) {
                aabb[2] = z;
            }
            if (aabb[3] < x) {
                aabb[3] = x;
            }
            if (aabb[4] < y) {
                aabb[4] = y;
            }
            if (aabb[5] < z) {
                aabb[5] = z;
            }
        }
        return aabb;
    },

    /**
     * Collapses a 2D axis-aligned boundary, ready to expand to fit 2D points.
     * Creates new AABB if none supplied.
     *
     * @private
     */
    collapseAABB2(aabb = math.AABB2()) {
        aabb[0] = math.MAX_DOUBLE;
        aabb[1] = math.MAX_DOUBLE;
        aabb[2] = math.MIN_DOUBLE;
        aabb[3] = math.MIN_DOUBLE;

        return aabb;
    },

    point3AABB3Intersect(aabb, p) {
        return aabb[0] > p[0] || aabb[3] < p[0] || aabb[1] > p[1] || aabb[4] < p[1] || aabb[2] > p[2] || aabb[5] < p[2];
    },

    point3AABB3AbsoluteIntersect(aabb, p) {
        return (
            aabb[0] <= p[0] && aabb[3] >= p[0] &&
            aabb[1] <= p[1] && aabb[4] >= p[1] &&
            aabb[2] <= p[2] && aabb[5] >= p[2]
        );
    },

    /**
     *
     * @param dir
     * @param constant
     * @param aabb
     * @returns {number}
     */
    planeAABB3Intersect(dir, constant, aabb) {
        let min, max;
        if (dir[0] > 0) {
            min = dir[0] * aabb[0];
            max = dir[0] * aabb[3];
        } else {
            min = dir[0] * aabb[3];
            max = dir[0] * aabb[0];
        }
        if (dir[1] > 0) {
            min += dir[1] * aabb[1];
            max += dir[1] * aabb[4];
        } else {
            min += dir[1] * aabb[4];
            max += dir[1] * aabb[1];
        }
        if (dir[2] > 0) {
            min += dir[2] * aabb[2];
            max += dir[2] * aabb[5];
        } else {
            min += dir[2] * aabb[5];
            max += dir[2] * aabb[2];
        }
        const outside = (min <= -constant) && (max <= -constant);
        if (outside) {
            return -1;
        }

        const inside = (min >= -constant) && (max >= -constant);
        if (inside) {
            return 1;
        }

        return 0;
    },

    /**
     * Finds the minimum 2D projected axis-aligned boundary enclosing the given 3D points.
     *
     * @private
     */
    OBB3ToAABB2(points, aabb = math.AABB2()) {
        let xmin = math.MAX_DOUBLE;
        let ymin = math.MAX_DOUBLE;
        let xmax = math.MIN_DOUBLE;
        let ymax = math.MIN_DOUBLE;

        let x;
        let y;
        let w;
        let f;

        for (let i = 0, len = points.length; i < len; i += 4) {

            x = points[i + 0];
            y = points[i + 1];
            w = points[i + 3] || 1.0;

            f = 1.0 / w;

            x *= f;
            y *= f;

            if (x < xmin) {
                xmin = x;
            }

            if (y < ymin) {
                ymin = y;
            }

            if (x > xmax) {
                xmax = x;
            }

            if (y > ymax) {
                ymax = y;
            }
        }

        aabb[0] = xmin;
        aabb[1] = ymin;
        aabb[2] = xmax;
        aabb[3] = ymax;

        return aabb;
    },

    /**
     * Expands the first axis-aligned 2D boundary to enclose the second, if required.
     *
     * @private
     */
    expandAABB2(aabb1, aabb2) {

        if (aabb1[0] > aabb2[0]) {
            aabb1[0] = aabb2[0];
        }

        if (aabb1[1] > aabb2[1]) {
            aabb1[1] = aabb2[1];
        }

        if (aabb1[2] < aabb2[2]) {
            aabb1[2] = aabb2[2];
        }

        if (aabb1[3] < aabb2[3]) {
            aabb1[3] = aabb2[3];
        }

        return aabb1;
    },

    /**
     * Expands an axis-aligned 2D boundary to enclose the given point, if required.
     *
     * @private
     */
    expandAABB2Point2(aabb, p) {

        if (aabb[0] > p[0]) {
            aabb[0] = p[0];
        }

        if (aabb[1] > p[1]) {
            aabb[1] = p[1];
        }

        if (aabb[2] < p[0]) {
            aabb[2] = p[0];
        }

        if (aabb[3] < p[1]) {
            aabb[3] = p[1];
        }

        return aabb;
    },

    AABB2ToCanvas(aabb, canvasWidth, canvasHeight, aabb2 = aabb) {
        const xmin = (aabb[0] + 1.0) * 0.5;
        const ymin = (aabb[1] + 1.0) * 0.5;
        const xmax = (aabb[2] + 1.0) * 0.5;
        const ymax = (aabb[3] + 1.0) * 0.5;

        aabb2[0] = Math.floor(xmin * canvasWidth);
        aabb2[1] = canvasHeight - Math.floor(ymax * canvasHeight);
        aabb2[2] = Math.floor(xmax * canvasWidth);
        aabb2[3] = canvasHeight - Math.floor(ymin * canvasHeight);

        return aabb2;
    },

    //------------------------------------------------------------------------------------------------------------------
    // Curves
    //------------------------------------------------------------------------------------------------------------------

    tangentQuadraticBezier(t, p0, p1, p2) {
        return 2 * (1 - t) * (p1 - p0) + 2 * t * (p2 - p1);
    },

    tangentQuadraticBezier3(t, p0, p1, p2, p3) {
        return -3 * p0 * (1 - t) * (1 - t) +
            3 * p1 * (1 - t) * (1 - t) - 6 * t * p1 * (1 - t) +
            6 * t * p2 * (1 - t) - 3 * t * t * p2 +
            3 * t * t * p3;
    },

    tangentSpline(t) {
        const h00 = 6 * t * t - 6 * t;
        const h10 = 3 * t * t - 4 * t + 1;
        const h01 = -6 * t * t + 6 * t;
        const h11 = 3 * t * t - 2 * t;
        return h00 + h10 + h01 + h11;
    },

    catmullRomInterpolate(p0, p1, p2, p3, t) {
        const v0 = (p2 - p0) * 0.5;
        const v1 = (p3 - p1) * 0.5;
        const t2 = t * t;
        const t3 = t * t2;
        return (2 * p1 - 2 * p2 + v0 + v1) * t3 + (-3 * p1 + 3 * p2 - 2 * v0 - v1) * t2 + v0 * t + p1;
    },

// Bezier Curve formulii from http://en.wikipedia.org/wiki/B%C3%A9zier_curve

// Quad Bezier Functions

    b2p0(t, p) {
        const k = 1 - t;
        return k * k * p;

    },

    b2p1(t, p) {
        return 2 * (1 - t) * t * p;
    },

    b2p2(t, p) {
        return t * t * p;
    },

    b2(t, p0, p1, p2) {
        return this.b2p0(t, p0) + this.b2p1(t, p1) + this.b2p2(t, p2);
    },

// Cubic Bezier Functions

    b3p0(t, p) {
        const k = 1 - t;
        return k * k * k * p;
    },

    b3p1(t, p) {
        const k = 1 - t;
        return 3 * k * k * t * p;
    },

    b3p2(t, p) {
        const k = 1 - t;
        return 3 * k * t * t * p;
    },

    b3p3(t, p) {
        return t * t * t * p;
    },

    b3(t, p0, p1, p2, p3) {
        return this.b3p0(t, p0) + this.b3p1(t, p1) + this.b3p2(t, p2) + this.b3p3(t, p3);
    },

    //------------------------------------------------------------------------------------------------------------------
    // Geometry
    //------------------------------------------------------------------------------------------------------------------

    /**
     * Calculates the normal vector of a triangle.
     *
     * @private
     */
    triangleNormal(a, b, c, normal = math.vec3()) {
        const p1x = b[0] - a[0];
        const p1y = b[1] - a[1];
        const p1z = b[2] - a[2];

        const p2x = c[0] - a[0];
        const p2y = c[1] - a[1];
        const p2z = c[2] - a[2];

        const p3x = p1y * p2z - p1z * p2y;
        const p3y = p1z * p2x - p1x * p2z;
        const p3z = p1x * p2y - p1y * p2x;

        const mag = Math.sqrt(p3x * p3x + p3y * p3y + p3z * p3z);
        if (mag === 0) {
            normal[0] = 0;
            normal[1] = 0;
            normal[2] = 0;
        } else {
            normal[0] = p3x / mag;
            normal[1] = p3y / mag;
            normal[2] = p3z / mag;
        }

        return normal
    },

    /**
     * Finds the intersection of a 3D ray with a 3D triangle.
     *
     * @private
     */
    rayTriangleIntersect: ((() => {

        const tempVec3 = new FloatArrayType(3);
        const tempVec3b = new FloatArrayType(3);
        const tempVec3c = new FloatArrayType(3);
        const tempVec3d = new FloatArrayType(3);
        const tempVec3e = new FloatArrayType(3);

        return (origin, dir, a, b, c, isect) => {

            isect = isect || math.vec3();

            const EPSILON = 0.000001;

            const edge1 = math.subVec3(b, a, tempVec3);
            const edge2 = math.subVec3(c, a, tempVec3b);

            const pvec = math.cross3Vec3(dir, edge2, tempVec3c);
            const det = math.dotVec3(edge1, pvec);
            if (det < EPSILON) {
                return null;
            }

            const tvec = math.subVec3(origin, a, tempVec3d);
            const u = math.dotVec3(tvec, pvec);
            if (u < 0 || u > det) {
                return null;
            }

            const qvec = math.cross3Vec3(tvec, edge1, tempVec3e);
            const v = math.dotVec3(dir, qvec);
            if (v < 0 || u + v > det) {
                return null;
            }

            const t = math.dotVec3(edge2, qvec) / det;
            isect[0] = origin[0] + t * dir[0];
            isect[1] = origin[1] + t * dir[1];
            isect[2] = origin[2] + t * dir[2];

            return isect;
        };
    }))(),

    /**
     * Finds the intersection of a 3D ray with a plane defined by 3 points.
     *
     * @private
     */
    rayPlaneIntersect: ((() => {

        const tempVec3 = new FloatArrayType(3);
        const tempVec3b = new FloatArrayType(3);
        const tempVec3c = new FloatArrayType(3);
        const tempVec3d = new FloatArrayType(3);

        return (origin, dir, a, b, c, isect) => {

            isect = isect || math.vec3();

            dir = math.normalizeVec3(dir, tempVec3);

            const edge1 = math.subVec3(b, a, tempVec3b);
            const edge2 = math.subVec3(c, a, tempVec3c);

            const n = math.cross3Vec3(edge1, edge2, tempVec3d);
            math.normalizeVec3(n, n);

            const d = -math.dotVec3(a, n);

            const t = -(math.dotVec3(origin, n) + d) / math.dotVec3(dir, n);

            isect[0] = origin[0] + t * dir[0];
            isect[1] = origin[1] + t * dir[1];
            isect[2] = origin[2] + t * dir[2];

            return isect;
        };
    }))(),

    /**
     * Gets barycentric coordinates from cartesian coordinates within a triangle.
     * Gets barycentric coordinates from cartesian coordinates within a triangle.
     *
     * @private
     */
    cartesianToBarycentric: ((() => {

        const tempVec3 = new FloatArrayType(3);
        const tempVec3b = new FloatArrayType(3);
        const tempVec3c = new FloatArrayType(3);

        return (cartesian, a, b, c, dest) => {

            const v0 = math.subVec3(c, a, tempVec3);
            const v1 = math.subVec3(b, a, tempVec3b);
            const v2 = math.subVec3(cartesian, a, tempVec3c);

            const dot00 = math.dotVec3(v0, v0);
            const dot01 = math.dotVec3(v0, v1);
            const dot02 = math.dotVec3(v0, v2);
            const dot11 = math.dotVec3(v1, v1);
            const dot12 = math.dotVec3(v1, v2);

            const denom = (dot00 * dot11 - dot01 * dot01);

            // Colinear or singular triangle

            if (denom === 0) {

                // Arbitrary location outside of triangle

                return null;
            }

            const invDenom = 1 / denom;

            const u = (dot11 * dot02 - dot01 * dot12) * invDenom;
            const v = (dot00 * dot12 - dot01 * dot02) * invDenom;

            dest[0] = 1 - u - v;
            dest[1] = v;
            dest[2] = u;

            return dest;
        };
    }))(),

    /**
     * Returns true if the given barycentric coordinates are within their triangle.
     *
     * @private
     */
    barycentricInsideTriangle(bary) {

        const v = bary[1];
        const u = bary[2];

        return (u >= 0) && (v >= 0) && (u + v < 1);
    },

    /**
     * Gets cartesian coordinates from barycentric coordinates within a triangle.
     *
     * @private
     */
    barycentricToCartesian(bary, a, b, c, cartesian = math.vec3()) {
        const u = bary[0];
        const v = bary[1];
        const w = bary[2];

        cartesian[0] = a[0] * u + b[0] * v + c[0] * w;
        cartesian[1] = a[1] * u + b[1] * v + c[1] * w;
        cartesian[2] = a[2] * u + b[2] * v + c[2] * w;

        return cartesian;
    },


    /**
     * Given geometry defined as an array of positions, optional normals, option uv and an array of indices, returns
     * modified arrays that have duplicate vertices removed.
     *
     * Note: does not work well when co-incident vertices have same positions but different normals and UVs.
     *
     * @param positions
     * @param normals
     * @param uv
     * @param indices
     * @returns {{positions: Array, indices: Array}}
     * @private
     */
    mergeVertices(positions, normals, uv, indices) {
        const positionsMap = {}; // Hashmap for looking up vertices by position coordinates (and making sure they are unique)
        const indicesLookup = [];
        const uniquePositions = [];
        const uniqueNormals = normals ? [] : null;
        const uniqueUV = uv ? [] : null;
        const indices2 = [];
        let vx;
        let vy;
        let vz;
        let key;
        const precisionPoints = 4; // number of decimal points, e.g. 4 for epsilon of 0.0001
        const precision = 10 ** precisionPoints;
        let i;
        let len;
        let uvi = 0;
        for (i = 0, len = positions.length; i < len; i += 3) {
            vx = positions[i];
            vy = positions[i + 1];
            vz = positions[i + 2];
            key = `${Math.round(vx * precision)}_${Math.round(vy * precision)}_${Math.round(vz * precision)}`;
            if (positionsMap[key] === undefined) {
                positionsMap[key] = uniquePositions.length / 3;
                uniquePositions.push(vx);
                uniquePositions.push(vy);
                uniquePositions.push(vz);
                if (normals) {
                    uniqueNormals.push(normals[i]);
                    uniqueNormals.push(normals[i + 1]);
                    uniqueNormals.push(normals[i + 2]);
                }
                if (uv) {
                    uniqueUV.push(uv[uvi]);
                    uniqueUV.push(uv[uvi + 1]);
                }
            }
            indicesLookup[i / 3] = positionsMap[key];
            uvi += 2;
        }
        for (i = 0, len = indices.length; i < len; i++) {
            indices2[i] = indicesLookup[indices[i]];
        }
        const result = {
            positions: uniquePositions,
            indices: indices2
        };
        if (uniqueNormals) {
            result.normals = uniqueNormals;
        }
        if (uniqueUV) {
            result.uv = uniqueUV;

        }
        return result;
    },

    /**
     * Builds normal vectors from positions and indices.
     *
     * @private
     */
    buildNormals: ((() => {

        const a = new FloatArrayType(3);
        const b = new FloatArrayType(3);
        const c = new FloatArrayType(3);
        const ab = new FloatArrayType(3);
        const ac = new FloatArrayType(3);
        const crossVec = new FloatArrayType(3);

        return (positions, indices, normals) => {

            let i;
            let len;
            const nvecs = new Array(positions.length / 3);
            let j0;
            let j1;
            let j2;

            for (i = 0, len = indices.length; i < len; i += 3) {

                j0 = indices[i];
                j1 = indices[i + 1];
                j2 = indices[i + 2];

                a[0] = positions[j0 * 3];
                a[1] = positions[j0 * 3 + 1];
                a[2] = positions[j0 * 3 + 2];

                b[0] = positions[j1 * 3];
                b[1] = positions[j1 * 3 + 1];
                b[2] = positions[j1 * 3 + 2];

                c[0] = positions[j2 * 3];
                c[1] = positions[j2 * 3 + 1];
                c[2] = positions[j2 * 3 + 2];

                math.subVec3(b, a, ab);
                math.subVec3(c, a, ac);

                const normVec = math.vec3();

                math.normalizeVec3(math.cross3Vec3(ab, ac, crossVec), normVec);

                if (!nvecs[j0]) {
                    nvecs[j0] = [];
                }
                if (!nvecs[j1]) {
                    nvecs[j1] = [];
                }
                if (!nvecs[j2]) {
                    nvecs[j2] = [];
                }

                nvecs[j0].push(normVec);
                nvecs[j1].push(normVec);
                nvecs[j2].push(normVec);
            }

            normals = (normals && normals.length === positions.length) ? normals : new Float32Array(positions.length);

            let count;
            let x;
            let y;
            let z;

            for (i = 0, len = nvecs.length; i < len; i++) {  // Now go through and average out everything

                count = nvecs[i].length;

                x = 0;
                y = 0;
                z = 0;

                for (let j = 0; j < count; j++) {
                    x += nvecs[i][j][0];
                    y += nvecs[i][j][1];
                    z += nvecs[i][j][2];
                }

                normals[i * 3] = (x / count);
                normals[i * 3 + 1] = (y / count);
                normals[i * 3 + 2] = (z / count);
            }

            return normals;
        };
    }))(),

    /**
     * Builds vertex tangent vectors from positions, UVs and indices.
     *
     * @private
     */
    buildTangents: ((() => {

        const tempVec3 = new FloatArrayType(3);
        const tempVec3b = new FloatArrayType(3);
        const tempVec3c = new FloatArrayType(3);
        const tempVec3d = new FloatArrayType(3);
        const tempVec3e = new FloatArrayType(3);
        const tempVec3f = new FloatArrayType(3);
        const tempVec3g = new FloatArrayType(3);

        return (positions, indices, uv) => {

            const tangents = new Float32Array(positions.length);

            // The vertex arrays needs to be calculated
            // before the calculation of the tangents

            for (let location = 0; location < indices.length; location += 3) {

                // Recontructing each vertex and UV coordinate into the respective vectors

                let index = indices[location];

                const v0 = positions.subarray(index * 3, index * 3 + 3);
                const uv0 = uv.subarray(index * 2, index * 2 + 2);

                index = indices[location + 1];

                const v1 = positions.subarray(index * 3, index * 3 + 3);
                const uv1 = uv.subarray(index * 2, index * 2 + 2);

                index = indices[location + 2];

                const v2 = positions.subarray(index * 3, index * 3 + 3);
                const uv2 = uv.subarray(index * 2, index * 2 + 2);

                const deltaPos1 = math.subVec3(v1, v0, tempVec3);
                const deltaPos2 = math.subVec3(v2, v0, tempVec3b);

                const deltaUV1 = math.subVec2(uv1, uv0, tempVec3c);
                const deltaUV2 = math.subVec2(uv2, uv0, tempVec3d);

                const r = 1 / ((deltaUV1[0] * deltaUV2[1]) - (deltaUV1[1] * deltaUV2[0]));

                const tangent = math.mulVec3Scalar(
                    math.subVec3(
                        math.mulVec3Scalar(deltaPos1, deltaUV2[1], tempVec3e),
                        math.mulVec3Scalar(deltaPos2, deltaUV1[1], tempVec3f),
                        tempVec3g
                    ),
                    r,
                    tempVec3f
                );

                // Average the value of the vectors

                let addTo;

                for (let v = 0; v < 3; v++) {
                    addTo = indices[location + v] * 3;
                    tangents[addTo] += tangent[0];
                    tangents[addTo + 1] += tangent[1];
                    tangents[addTo + 2] += tangent[2];
                }
            }

            return tangents;
        };
    }))(),

    /**
     * Builds vertex and index arrays needed by color-indexed triangle picking.
     *
     * @private
     */
    buildPickTriangles(positions, indices, compressGeometry) {

        const numIndices = indices.length;
        const pickPositions = compressGeometry ? new Uint16Array(numIndices * 9) : new Float32Array(numIndices * 9);
        const pickColors = new Uint8Array(numIndices * 12);
        let primIndex = 0;
        let vi;// Positions array index
        let pvi = 0;// Picking positions array index
        let pci = 0; // Picking color array index

        // Triangle indices
        let i;
        let r;
        let g;
        let b;
        let a;

        for (let location = 0; location < numIndices; location += 3) {

            // Primitive-indexed triangle pick color

            a = (primIndex >> 24 & 0xFF);
            b = (primIndex >> 16 & 0xFF);
            g = (primIndex >> 8 & 0xFF);
            r = (primIndex & 0xFF);

            // A

            i = indices[location];
            vi = i * 3;

            pickPositions[pvi++] = positions[vi];
            pickPositions[pvi++] = positions[vi + 1];
            pickPositions[pvi++] = positions[vi + 2];

            pickColors[pci++] = r;
            pickColors[pci++] = g;
            pickColors[pci++] = b;
            pickColors[pci++] = a;

            // B

            i = indices[location + 1];
            vi = i * 3;

            pickPositions[pvi++] = positions[vi];
            pickPositions[pvi++] = positions[vi + 1];
            pickPositions[pvi++] = positions[vi + 2];

            pickColors[pci++] = r;
            pickColors[pci++] = g;
            pickColors[pci++] = b;
            pickColors[pci++] = a;

            // C

            i = indices[location + 2];
            vi = i * 3;

            pickPositions[pvi++] = positions[vi];
            pickPositions[pvi++] = positions[vi + 1];
            pickPositions[pvi++] = positions[vi + 2];

            pickColors[pci++] = r;
            pickColors[pci++] = g;
            pickColors[pci++] = b;
            pickColors[pci++] = a;

            primIndex++;
        }

        return {
            positions: pickPositions,
            colors: pickColors
        };
    },

    /**
     * Converts surface-perpendicular face normals to vertex normals. Assumes that the mesh contains disjoint triangles
     * that don't share vertex array elements. Works by finding groups of vertices that have the same location and
     * averaging their normal vectors.
     *
     * @returns {{positions: Array, normals: *}}
     */
    faceToVertexNormals(positions, normals, options = {}) {
        const smoothNormalsAngleThreshold = options.smoothNormalsAngleThreshold || 20;
        const vertexMap = {};
        const vertexNormals = [];
        const vertexNormalAccum = {};
        let acc;
        let vx;
        let vy;
        let vz;
        let key;
        const precisionPoints = 4; // number of decimal points, e.g. 4 for epsilon of 0.0001
        const precision = 10 ** precisionPoints;
        let posi;
        let i;
        let j;
        let len;
        let a;
        let b;

        for (i = 0, len = positions.length; i < len; i += 3) {

            posi = i / 3;

            vx = positions[i];
            vy = positions[i + 1];
            vz = positions[i + 2];

            key = `${Math.round(vx * precision)}_${Math.round(vy * precision)}_${Math.round(vz * precision)}`;

            if (vertexMap[key] === undefined) {
                vertexMap[key] = [posi];
            } else {
                vertexMap[key].push(posi);
            }

            const normal = math.normalizeVec3([normals[i], normals[i + 1], normals[i + 2]]);

            vertexNormals[posi] = normal;

            acc = math.vec4([normal[0], normal[1], normal[2], 1]);

            vertexNormalAccum[posi] = acc;
        }

        for (key in vertexMap) {

            if (vertexMap.hasOwnProperty(key)) {

                const vertices = vertexMap[key];
                const numVerts = vertices.length;

                for (i = 0; i < numVerts; i++) {

                    const ii = vertices[i];

                    acc = vertexNormalAccum[ii];

                    for (j = 0; j < numVerts; j++) {

                        if (i === j) {
                            continue;
                        }

                        const jj = vertices[j];

                        a = vertexNormals[ii];
                        b = vertexNormals[jj];

                        const angle = Math.abs(math.angleVec3(a, b) / math.DEGTORAD);

                        if (angle < smoothNormalsAngleThreshold) {

                            acc[0] += b[0];
                            acc[1] += b[1];
                            acc[2] += b[2];
                            acc[3] += 1.0;
                        }
                    }
                }
            }
        }

        for (i = 0, len = normals.length; i < len; i += 3) {

            acc = vertexNormalAccum[i / 3];

            normals[i + 0] = acc[0] / acc[3];
            normals[i + 1] = acc[1] / acc[3];
            normals[i + 2] = acc[2] / acc[3];

        }
    },

    //------------------------------------------------------------------------------------------------------------------
    // Ray casting
    //------------------------------------------------------------------------------------------------------------------

    /**
     Transforms a ray by a matrix.
     @method transformRay
     @static
     @param {Number[]} matrix 4x4 matrix
     @param {Number[]} rayOrigin The ray origin
     @param {Number[]} rayDir The ray direction
     @param {Number[]} rayOriginDest The transformed ray origin
     @param {Number[]} rayDirDest The transformed ray direction
     */
    transformRay: ((() => {

        const tempVec4a = new FloatArrayType(4);
        const tempVec4b = new FloatArrayType(4);

        return (matrix, rayOrigin, rayDir, rayOriginDest, rayDirDest) => {

            tempVec4a[0] = rayOrigin[0];
            tempVec4a[1] = rayOrigin[1];
            tempVec4a[2] = rayOrigin[2];
            tempVec4a[3] = 1;

            math.transformVec4(matrix, tempVec4a, tempVec4b);

            rayOriginDest[0] = tempVec4b[0];
            rayOriginDest[1] = tempVec4b[1];
            rayOriginDest[2] = tempVec4b[2];

            tempVec4a[0] = rayDir[0];
            tempVec4a[1] = rayDir[1];
            tempVec4a[2] = rayDir[2];

            math.transformVec3(matrix, tempVec4a, tempVec4b);

            math.normalizeVec3(tempVec4b);

            rayDirDest[0] = tempVec4b[0];
            rayDirDest[1] = tempVec4b[1];
            rayDirDest[2] = tempVec4b[2];
        };
    }))(),

    /**
     Transforms a Canvas-space position into a World-space ray, in the context of a Camera.
     @method canvasPosToWorldRay
     @static
     @param {Number[]} viewMatrix View matrix
     @param {Number[]} projMatrix Projection matrix
     @param {String} projection Projection type (e.g. "ortho")
     @param {Number[]} canvasPos The Canvas-space position.
     @param {Number[]} worldRayOrigin The World-space ray origin.
     @param {Number[]} worldRayDir The World-space ray direction.
     */
    canvasPosToWorldRay: ((() => {

        const pvMatInv = new FloatArrayType(16);
        const vec4Near = new FloatArrayType(4);
        const vec4Far  = new FloatArrayType(4);

        const clipToWorld = (clipX, clipY, clipZ, isOrtho, outVec4) => {
            outVec4[0] = clipX;
            outVec4[1] = clipY;
            outVec4[2] = clipZ;
            outVec4[3] = 1;

            math.transformVec4(pvMatInv, outVec4, outVec4);
            if (! isOrtho)
                math.mulVec4Scalar(outVec4, 1 / outVec4[3]);
        };

        return (canvas, viewMatrix, projMatrix, projection, canvasPos, worldRayOrigin, worldRayDir) => {
            const isOrtho = projection === "ortho";

            math.mulMat4(projMatrix, viewMatrix, pvMatInv);
            math.inverseMat4(pvMatInv, pvMatInv);

            // Calculate clip space coordinates, which will be in range
            // of x=[-1..1] and y=[-1..1], with y=(+1) at top
            // clientWidth/Height needs to be used in case canvas.width/height is scaled down,
            // but not reflecting client dimensions (e.g. when using FastNavPlugin)
            const clipX =     2 * canvasPos[0] / canvas.clientWidth - 1;  // Calculate clip space coordinates
            const clipY = 1 - 2 * canvasPos[1] / canvas.clientHeight;

            clipToWorld(clipX, clipY, -1, isOrtho, vec4Near);

            clipToWorld(clipX, clipY,  1, isOrtho, vec4Far);

            worldRayOrigin[0] = vec4Near[0];
            worldRayOrigin[1] = vec4Near[1];
            worldRayOrigin[2] = vec4Near[2];

            math.subVec3(vec4Far, vec4Near, worldRayDir);

            math.normalizeVec3(worldRayDir);
        };
    }))(),

    /**
     Transforms a Canvas-space position to a Mesh's Local-space coordinate system, in the context of a Camera.
     @method canvasPosToLocalRay
     @static
     @param {Camera} camera The Camera.
     @param {Mesh} mesh The Mesh.
     @param {Number[]} viewMatrix View matrix
     @param {Number[]} projMatrix Projection matrix
     @param {Number[]} worldMatrix Modeling matrix
     @param {Number[]} canvasPos The Canvas-space position.
     @param {Number[]} localRayOrigin The Local-space ray origin.
     @param {Number[]} localRayDir The Local-space ray direction.
     */
    canvasPosToLocalRay: ((() => {

        const worldRayOrigin = new FloatArrayType(3);
        const worldRayDir = new FloatArrayType(3);

        return (canvas, viewMatrix, projMatrix, projection, worldMatrix, canvasPos, localRayOrigin, localRayDir) => {
            math.canvasPosToWorldRay(canvas, viewMatrix, projMatrix, projection, canvasPos, worldRayOrigin, worldRayDir);
            math.worldRayToLocalRay(worldMatrix, worldRayOrigin, worldRayDir, localRayOrigin, localRayDir);
        };
    }))(),

    /**
     Transforms a ray from World-space to a Mesh's Local-space coordinate system.
     @method worldRayToLocalRay
     @static
     @param {Number[]} worldMatrix The World transform matrix
     @param {Number[]} worldRayOrigin The World-space ray origin.
     @param {Number[]} worldRayDir The World-space ray direction.
     @param {Number[]} localRayOrigin The Local-space ray origin.
     @param {Number[]} localRayDir The Local-space ray direction.
     */
    worldRayToLocalRay: ((() => {

        const tempMat4 = new FloatArrayType(16);
        const tempVec4a = new FloatArrayType(4);
        const tempVec4b = new FloatArrayType(4);

        return (worldMatrix, worldRayOrigin, worldRayDir, localRayOrigin, localRayDir) => {

            const modelMatInverse = math.inverseMat4(worldMatrix, tempMat4);

            tempVec4a[0] = worldRayOrigin[0];
            tempVec4a[1] = worldRayOrigin[1];
            tempVec4a[2] = worldRayOrigin[2];
            tempVec4a[3] = 1;

            math.transformVec4(modelMatInverse, tempVec4a, tempVec4b);

            localRayOrigin[0] = tempVec4b[0];
            localRayOrigin[1] = tempVec4b[1];
            localRayOrigin[2] = tempVec4b[2];

            math.transformVec3(modelMatInverse, worldRayDir, localRayDir);
        };
    }))(),

    buildKDTree: ((() => {

        const KD_TREE_MAX_DEPTH = 10;
        const KD_TREE_MIN_TRIANGLES = 20;

        const dimLength = new Float32Array();

        function buildNode(triangles, indices, positions, depth) {
            const aabb = new FloatArrayType(6);

            const node = {
                triangles: null,
                left: null,
                right: null,
                leaf: false,
                splitDim: 0,
                aabb
            };

            aabb[0] = aabb[1] = aabb[2] = Number.POSITIVE_INFINITY;
            aabb[3] = aabb[4] = aabb[5] = Number.NEGATIVE_INFINITY;

            let t;
            let len;

            for (t = 0, len = triangles.length; t < len; ++t) {
                var ii = triangles[t] * 3;
                for (let j = 0; j < 3; ++j) {
                    const pi = indices[ii + j] * 3;
                    if (positions[pi] < aabb[0]) {
                        aabb[0] = positions[pi];
                    }
                    if (positions[pi] > aabb[3]) {
                        aabb[3] = positions[pi];
                    }
                    if (positions[pi + 1] < aabb[1]) {
                        aabb[1] = positions[pi + 1];
                    }
                    if (positions[pi + 1] > aabb[4]) {
                        aabb[4] = positions[pi + 1];
                    }
                    if (positions[pi + 2] < aabb[2]) {
                        aabb[2] = positions[pi + 2];
                    }
                    if (positions[pi + 2] > aabb[5]) {
                        aabb[5] = positions[pi + 2];
                    }
                }
            }

            if (triangles.length < KD_TREE_MIN_TRIANGLES || depth > KD_TREE_MAX_DEPTH) {
                node.triangles = triangles;
                node.leaf = true;
                return node;
            }

            dimLength[0] = aabb[3] - aabb[0];
            dimLength[1] = aabb[4] - aabb[1];
            dimLength[2] = aabb[5] - aabb[2];

            let dim = 0;

            if (dimLength[1] > dimLength[dim]) {
                dim = 1;
            }

            if (dimLength[2] > dimLength[dim]) {
                dim = 2;
            }

            node.splitDim = dim;

            const mid = (aabb[dim] + aabb[dim + 3]) / 2;
            const left = new Array(triangles.length);
            let numLeft = 0;
            const right = new Array(triangles.length);
            let numRight = 0;

            for (t = 0, len = triangles.length; t < len; ++t) {

                var ii = triangles[t] * 3;
                const i0 = indices[ii];
                const i1 = indices[ii + 1];
                const i2 = indices[ii + 2];

                const pi0 = i0 * 3;
                const pi1 = i1 * 3;
                const pi2 = i2 * 3;

                if (positions[pi0 + dim] <= mid || positions[pi1 + dim] <= mid || positions[pi2 + dim] <= mid) {
                    left[numLeft++] = triangles[t];
                } else {
                    right[numRight++] = triangles[t];
                }
            }

            left.length = numLeft;
            right.length = numRight;

            node.left = buildNode(left, indices, positions, depth + 1);
            node.right = buildNode(right, indices, positions, depth + 1);

            return node;
        }

        return (indices, positions) => {
            const numTris = indices.length / 3;
            const triangles = new Array(numTris);
            for (let i = 0; i < numTris; ++i) {
                triangles[i] = i;
            }
            return buildNode(triangles, indices, positions, 0);
        };
    }))(),


    decompressPosition(position, decodeMatrix, dest) {
        dest = dest || position;
        dest[0] = position[0] * decodeMatrix[0] + decodeMatrix[12];
        dest[1] = position[1] * decodeMatrix[5] + decodeMatrix[13];
        dest[2] = position[2] * decodeMatrix[10] + decodeMatrix[14];
    },

    decompressPositions(positions, decodeMatrix, dest = new Float32Array(positions.length)) {
        for (let i = 0, len = positions.length; i < len; i += 3) {
            dest[i + 0] = positions[i + 0] * decodeMatrix[0] + decodeMatrix[12];
            dest[i + 1] = positions[i + 1] * decodeMatrix[5] + decodeMatrix[13];
            dest[i + 2] = positions[i + 2] * decodeMatrix[10] + decodeMatrix[14];
        }
        return dest;
    },

    decompressUV(uv, decodeMatrix, dest) {
        dest[0] = uv[0] * decodeMatrix[0] + decodeMatrix[6];
        dest[1] = uv[1] * decodeMatrix[4] + decodeMatrix[7];
    },

    decompressUVs(uvs, decodeMatrix, dest = new Float32Array(uvs.length)) {
        for (let i = 0, len = uvs.length; i < len; i += 3) {
            dest[i + 0] = uvs[i + 0] * decodeMatrix[0] + decodeMatrix[6];
            dest[i + 1] = uvs[i + 1] * decodeMatrix[4] + decodeMatrix[7];
        }
        return dest;
    },

    octDecodeVec2(oct, result) {
        let x = oct[0];
        let y = oct[1];
        x = (2 * x + 1) / 255;
        y = (2 * y + 1) / 255;
        const z = 1 - Math.abs(x) - Math.abs(y);
        if (z < 0) {
            x = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1);
            y = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1);
        }
        const length = Math.sqrt(x * x + y * y + z * z);
        result[0] = x / length;
        result[1] = y / length;
        result[2] = z / length;
        return result;
    },

    octDecodeVec2s(octs, result) {
        for (let i = 0, j = 0, len = octs.length; i < len; i += 2) {
            let x = octs[i + 0];
            let y = octs[i + 1];
            x = (2 * x + 1) / 255;
            y = (2 * y + 1) / 255;
            const z = 1 - Math.abs(x) - Math.abs(y);
            if (z < 0) {
                x = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1);
                y = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1);
            }
            const length = Math.sqrt(x * x + y * y + z * z);
            result[j + 0] = x / length;
            result[j + 1] = y / length;
            result[j + 2] = z / length;
            j += 3;
        }
        return result;
    }
};

math.buildEdgeIndices = (function () {

    const uniquePositions = [];
    const indicesLookup = [];
    const indicesReverseLookup = [];
    const weldedIndices = [];

    // TODO: Optimize with caching, but need to cater to both compressed and uncompressed positions

    const faces = [];
    let numFaces = 0;
    const compa = new Uint16Array(3);
    const compb = new Uint16Array(3);
    const compc = new Uint16Array(3);
    const a = math.vec3();
    const b = math.vec3();
    const c = math.vec3();
    const cb = math.vec3();
    const ab = math.vec3();
    const cross = math.vec3();
    const normal = math.vec3();

    function weldVertices(positions, indices) {
        const positionsMap = {}; // Hashmap for looking up vertices by position coordinates (and making sure they are unique)
        let vx;
        let vy;
        let vz;
        let key;
        const precisionPoints = 4; // number of decimal points, e.g. 4 for epsilon of 0.0001
        const precision = Math.pow(10, precisionPoints);
        let i;
        let len;
        let lenUniquePositions = 0;
        for (i = 0, len = positions.length; i < len; i += 3) {
            vx = positions[i];
            vy = positions[i + 1];
            vz = positions[i + 2];
            key = Math.round(vx * precision) + '_' + Math.round(vy * precision) + '_' + Math.round(vz * precision);
            if (positionsMap[key] === undefined) {
                positionsMap[key] = lenUniquePositions / 3;
                uniquePositions[lenUniquePositions++] = vx;
                uniquePositions[lenUniquePositions++] = vy;
                uniquePositions[lenUniquePositions++] = vz;
            }
            indicesLookup[i / 3] = positionsMap[key];
        }
        for (i = 0, len = indices.length; i < len; i++) {
            weldedIndices[i] = indicesLookup[indices[i]];
            indicesReverseLookup[weldedIndices[i]] = indices[i];
        }
    }

    function buildFaces(numIndices, positionsDecodeMatrix) {
        numFaces = 0;
        for (let i = 0, len = numIndices; i < len; i += 3) {
            const ia = ((weldedIndices[i]) * 3);
            const ib = ((weldedIndices[i + 1]) * 3);
            const ic = ((weldedIndices[i + 2]) * 3);
            if (positionsDecodeMatrix) {
                compa[0] = uniquePositions[ia];
                compa[1] = uniquePositions[ia + 1];
                compa[2] = uniquePositions[ia + 2];
                compb[0] = uniquePositions[ib];
                compb[1] = uniquePositions[ib + 1];
                compb[2] = uniquePositions[ib + 2];
                compc[0] = uniquePositions[ic];
                compc[1] = uniquePositions[ic + 1];
                compc[2] = uniquePositions[ic + 2];
                // Decode
                math.decompressPosition(compa, positionsDecodeMatrix, a);
                math.decompressPosition(compb, positionsDecodeMatrix, b);
                math.decompressPosition(compc, positionsDecodeMatrix, c);
            } else {
                a[0] = uniquePositions[ia];
                a[1] = uniquePositions[ia + 1];
                a[2] = uniquePositions[ia + 2];
                b[0] = uniquePositions[ib];
                b[1] = uniquePositions[ib + 1];
                b[2] = uniquePositions[ib + 2];
                c[0] = uniquePositions[ic];
                c[1] = uniquePositions[ic + 1];
                c[2] = uniquePositions[ic + 2];
            }
            math.subVec3(c, b, cb);
            math.subVec3(a, b, ab);
            math.cross3Vec3(cb, ab, cross);
            math.normalizeVec3(cross, normal);
            const face = faces[numFaces] || (faces[numFaces] = {normal: math.vec3()});
            face.normal[0] = normal[0];
            face.normal[1] = normal[1];
            face.normal[2] = normal[2];
            numFaces++;
        }
    }

    return function (positions, indices, positionsDecodeMatrix, edgeThreshold) {
        weldVertices(positions, indices);
        buildFaces(indices.length, positionsDecodeMatrix);
        const edgeIndices = [];
        const thresholdDot = Math.cos(math.DEGTORAD * edgeThreshold);
        const edges = {};
        let edge1;
        let edge2;
        let index1;
        let index2;
        let key;
        let largeIndex = false;
        let edge;
        let normal1;
        let normal2;
        let dot;
        let ia;
        let ib;
        for (let i = 0, len = indices.length; i < len; i += 3) {
            const faceIndex = i / 3;
            for (let j = 0; j < 3; j++) {
                edge1 = weldedIndices[i + j];
                edge2 = weldedIndices[i + ((j + 1) % 3)];
                index1 = Math.min(edge1, edge2);
                index2 = Math.max(edge1, edge2);
                key = index1 + "," + index2;
                if (edges[key] === undefined) {
                    edges[key] = {
                        index1: index1,
                        index2: index2,
                        face1: faceIndex,
                        face2: undefined
                    };
                } else {
                    edges[key].face2 = faceIndex;
                }
            }
        }
        for (key in edges) {
            edge = edges[key];
            // an edge is only rendered if the angle (in degrees) between the face normals of the adjoining faces exceeds this value. default = 1 degree.
            if (edge.face2 !== undefined) {
                normal1 = faces[edge.face1].normal;
                normal2 = faces[edge.face2].normal;
                dot = math.dotVec3(normal1, normal2);
                if (dot > thresholdDot) {
                    continue;
                }
            }
            ia = indicesReverseLookup[edge.index1];
            ib = indicesReverseLookup[edge.index2];
            if (!largeIndex && ia > 65535 || ib > 65535) {
                largeIndex = true;
            }
            edgeIndices.push(ia);
            edgeIndices.push(ib);
        }
        return (largeIndex) ? new Uint32Array(edgeIndices) : new Uint16Array(edgeIndices);
    };
})();


/**
 * Returns `true` if a plane clips the given 3D positions.
 * @param {Number[]} pos Position in plane
 * @param {Number[]} dir Direction of plane
 * @param {number} positions Flat array of 3D positions.
 * @param {number} numElementsPerPosition Number of elements perposition - usually either 3 or 4.
 * @returns {boolean}
 */
math.planeClipsPositions3 = function (pos, dir, positions, numElementsPerPosition = 3) {
    for (let i = 0, len = positions.length; i < len; i += numElementsPerPosition) {
        tempVec3a$P[0] = positions[i + 0] - pos[0];
        tempVec3a$P[1] = positions[i + 1] - pos[1];
        tempVec3a$P[2] = positions[i + 2] - pos[2];
        let dotProduct = tempVec3a$P[0] * dir[0] + tempVec3a$P[1] * dir[1] + tempVec3a$P[2] * dir[2];
        if (dotProduct < 0) {
            return true;
        }
    }
    return false;
};

// Fast queue that avoids using potentially inefficient array .shift() calls
// Based on https://github.com/creationix/fastqueue

/** @private */
class Queue {

    constructor() {

        this._head = [];
        this._headLength = 0;
        this._tail = [];
        this._index = 0;
        this._length = 0;
    }

    get length() {
        return this._length;
    }

    shift() {
        if (this._index >= this._headLength) {
            const t = this._head;
            t.length = 0;
            this._head = this._tail;
            this._tail = t;
            this._index = 0;
            this._headLength = this._head.length;
            if (!this._headLength) {
                return;
            }
        }
        const value = this._head[this._index];
        if (this._index < 0) {
            delete this._head[this._index++];
        }
        else {
            this._head[this._index++] = undefined;
        }
        this._length--;
        return value;
    }

    push(item) {
        this._length++;
        this._tail.push(item);
        return this;
    };

    unshift(item) {
        this._head[--this._index] = item;
        this._length++;
        return this;
    }
}

/**
 * xeokit runtime statistics.
 * @type {{components: {models: number, objects: number, scenes: number, meshes: number}, memory: {indices: number, uvs: number, textures: number, materials: number, transforms: number, positions: number, programs: number, normals: number, meshes: number, colors: number}, build: {version: string}, client: {browser: string}, frame: {frameCount: number, useProgram: number, bindTexture: number, drawElements: number, bindArray: number, tasksRun: number, fps: number, drawArrays: number, tasksScheduled: number}}}
 */
const stats = {
    components: {
        scenes: 0},
    memory: {
        meshes: 0,
        positions: 0,
        colors: 0,
        normals: 0,
        uvs: 0,
        indices: 0,
        textures: 0,
        materials: 0,
        programs: 0
    },
    frame: {
        frameCount: 0,
        fps: 0,
        useProgram: 0,
        bindTexture: 0,
        bindArray: 0,
        drawElements: 0,
        drawArrays: 0,
        tasksRun: 0,
        tasksScheduled: 0
    }
};

/**
 * @private
 */

function xmlToJson(node, attributeRenamer) {
    if (node.nodeType === node.TEXT_NODE) {
        var v = node.nodeValue;
        if (v.match(/^\s+$/) === null) {
            return v;
        }
    } else if (node.nodeType === node.ELEMENT_NODE ||
        node.nodeType === node.DOCUMENT_NODE) {
        var json = {type: node.nodeName, children: []};

        if (node.nodeType === node.ELEMENT_NODE) {
            for (var j = 0; j < node.attributes.length; j++) {
                var attribute = node.attributes[j];
                var nm = attributeRenamer[attribute.nodeName] || attribute.nodeName;
                json[nm] = attribute.nodeValue;
            }
        }

        for (var i = 0; i < node.childNodes.length; i++) {
            var item = node.childNodes[i];
            var j = xmlToJson(item, attributeRenamer);
            if (j) json.children.push(j);
        }

        return json;
    }
}

/**
 * @private
 */
function clone(ob) {
    return JSON.parse(JSON.stringify(ob));
}

/**
 * @private
 */
var guidChars = [["0", 10], ["A", 26], ["a", 26], ["_", 1], ["$", 1]].map(function (a) {
    var li = [];
    var st = a[0].charCodeAt(0);
    var en = st + a[1];
    for (var i = st; i < en; ++i) {
        li.push(i);
    }
    return String.fromCharCode.apply(null, li);
}).join("");

/**
 * @private
 */
function b64(v, len) {
    var r = (!len || len === 4) ? [0, 6, 12, 18] : [0, 6];
    return r.map(function (i) {
        return guidChars.substr(parseInt(v / (1 << i)) % 64, 1)
    }).reverse().join("");
}

/**
 * @private
 */
function compressGuid(g) {
    var bs = [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30].map(function (i) {
        return parseInt(g.substr(i, 2), 16);
    });
    return b64(bs[0], 2) + [1, 4, 7, 10, 13].map(function (i) {
        return b64((bs[i] << 16) + (bs[i + 1] << 8) + bs[i + 2]);
    }).join("");
}

/**
 * @private
 */
function findNodeOfType(m, t) {
    var li = [];
    var _ = function (n) {
        if (n.type === t) li.push(n);
        (n.children || []).forEach(function (c) {
            _(c);
        });
    };
    _(m);
    return li;
}

/**
 * @private
 */
function timeout(dt) {
    return new Promise(function (resolve, reject) {
        setTimeout(resolve, dt);
    });
}

/**
 * @private
 */
function httpRequest(args) {
    return new Promise(function (resolve, reject) {
        var xhr = new XMLHttpRequest();
        xhr.open(args.method || "GET", args.url, true);
        xhr.onload = function (e) {
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                    resolve(xhr.responseXML);
                } else {
                    reject(xhr.statusText);
                }
            }
        };
        xhr.send(null);
    });
}

/**
 * @private
 */
const queryString = function () {
    // This function is anonymous, is executed immediately and
    // the return value is assigned to QueryString!
    var query_string = {};
    var query = window.location.search.substring(1);
    var vars = query.split("&");
    for (var i = 0; i < vars.length; i++) {
        var pair = vars[i].split("=");
        // If first entry with this name
        if (typeof query_string[pair[0]] === "undefined") {
            query_string[pair[0]] = decodeURIComponent(pair[1]);
            // If second entry with this name
        } else if (typeof query_string[pair[0]] === "string") {
            var arr = [query_string[pair[0]], decodeURIComponent(pair[1])];
            query_string[pair[0]] = arr;
            // If third or later entry with this name
        } else {
            query_string[pair[0]].push(decodeURIComponent(pair[1]));
        }
    }
    return query_string;
}();

/**
 * @private
 */
function loadJSON(url, ok, err) {
    // Avoid checking ok and err on each use.
    var defaultCallback = (_value) => undefined;
    ok = ok || defaultCallback;
    err = err || defaultCallback;

    var request = new XMLHttpRequest();
    request.overrideMimeType("application/json");
    request.open('GET', url, true);
    request.addEventListener('load', function (event) {
        var response = event.target.response;
        if (this.status === 200) {
            var json;
            try {
                json = JSON.parse(response);
            } catch (e) {
                err(`utils.loadJSON(): Failed to parse JSON response - ${e}`);
            }
            ok(json);
        } else if (this.status === 0) {
            // Some browsers return HTTP Status 0 when using non-http protocol
            // e.g. 'file://' or 'data://'. Handle as success.
            console.warn('loadFile: HTTP Status 0 received.');
            try {
                ok(JSON.parse(response));
            } catch (e) {
                err(`utils.loadJSON(): Failed to parse JSON response - ${e}`);
            }
        } else {
            err(event);
        }
    }, false);

    request.addEventListener('error', function (event) {
        err(event);
    }, false);
    request.send(null);
}

/**
 * @private
 */
function loadArraybuffer$1(url, ok, err) {
    // Check for data: URI
    var defaultCallback = (_value) => undefined;
    ok = ok || defaultCallback;
    err = err || defaultCallback;
    const dataUriRegex = /^data:(.*?)(;base64)?,(.*)$/;
    const dataUriRegexResult = url.match(dataUriRegex);
    if (dataUriRegexResult) { // Safari can't handle data URIs through XMLHttpRequest
        const isBase64 = !!dataUriRegexResult[2];
        var data = dataUriRegexResult[3];
        data = window.decodeURIComponent(data);
        if (isBase64) {
            data = window.atob(data);
        }
        try {
            const buffer = new ArrayBuffer(data.length);
            const view = new Uint8Array(buffer);
            for (var i = 0; i < data.length; i++) {
                view[i] = data.charCodeAt(i);
            }
            core.scheduleTask(() => {
                ok(buffer);
            });
        } catch (error) {
            core.scheduleTask(() => {
                err(error);
            });
        }
    } else {
        const request = new XMLHttpRequest();
        request.open('GET', url, true);
        request.responseType = 'arraybuffer';
        request.onreadystatechange = function () {
            if (request.readyState === 4) {
                if (request.status === 200) {
                    ok(request.response);
                } else {
                    err('loadArrayBuffer error : ' + request.response);
                }
            }
        };
        request.send(null);
    }
}

/**
 Tests if the given object is an array
 @private
 */
function isArray$1(value) {
    return value && !(value.propertyIsEnumerable('length')) && typeof value === 'object' && typeof value.length === 'number';
}

/**
 Tests if the given value is a string
 @param value
 @returns {Boolean}
 @private
 */
function isString(value) {
    return (typeof value === 'string' || value instanceof String);
}

/**
 Tests if the given value is a number
 @param value
 @returns {Boolean}
 @private
 */
function isNumeric(value) {
    return !isNaN(parseFloat(value)) && isFinite(value);
}

/**
 Tests if the given value is an ID
 @param value
 @returns {Boolean}
 @private
 */
function isID(value) {
    return utils.isString(value) || utils.isNumeric(value);
}

/**
 Tests if the given components are the same, where the components can be either IDs or instances.
 @param c1
 @param c2
 @returns {Boolean}
 @private
 */
function isSameComponent(c1, c2) {
    if (!c1 || !c2) {
        return false;
    }
    const id1 = (utils.isNumeric(c1) || utils.isString(c1)) ? `${c1}` : c1.id;
    const id2 = (utils.isNumeric(c2) || utils.isString(c2)) ? `${c2}` : c2.id;
    return id1 === id2;
}

/**
 Tests if the given value is a function
 @param value
 @returns {Boolean}
 @private
 */
function isFunction$1(value) {
    return (typeof value === "function");
}

/**
 Tests if the given value is a JavaScript JSON object, eg, ````{ foo: "bar" }````.
 @param value
 @returns {Boolean}
 @private
 */
function isObject$1(value) {
    const objectConstructor = {}.constructor;
    return (!!value && value.constructor === objectConstructor);
}

/** Returns a shallow copy
 */
function copy(o) {
    return utils.apply(o, {});
}

/** Add properties of o to o2, overwriting them on o2 if already there
 */
function apply(o, o2) {
    for (const name in o) {
        if (o.hasOwnProperty(name)) {
            o2[name] = o[name];
        }
    }
    return o2;
}

/**
 Add non-null/defined properties of o to o2
 @private
 */
function apply2(o, o2) {
    for (const name in o) {
        if (o.hasOwnProperty(name)) {
            if (o[name] !== undefined && o[name] !== null) {
                o2[name] = o[name];
            }
        }
    }
    return o2;
}

/**
 Add properties of o to o2 where undefined or null on o2
 @private
 */
function applyIf(o, o2) {
    for (const name in o) {
        if (o.hasOwnProperty(name)) {
            if (o2[name] === undefined || o2[name] === null) {
                o2[name] = o[name];
            }
        }
    }
    return o2;
}

/**
 Returns true if the given map is empty.
 @param obj
 @returns {Boolean}
 @private
 */
function isEmptyObject$1(obj) {
    for (const name in obj) {
        if (obj.hasOwnProperty(name)) {
            return false;
        }
    }
    return true;
}

/**
 Returns the given ID as a string, in quotes if the ID was a string to begin with.

 This is useful for logging IDs.

 @param {Number| String} id The ID
 @returns {String}
 @private
 */
function inQuotes(id) {
    return utils.isNumeric(id) ? (`${id}`) : (`'${id}'`);
}

/**
 Returns the concatenation of two typed arrays.
 @param a
 @param b
 @returns {*|a}
 @private
 */
function concat(a, b) {
    const c = new a.constructor(a.length + b.length);
    c.set(a);
    c.set(b, a.length);
    return c;
}

function flattenParentChildHierarchy(root) {
    var list = [];

    function visit(node) {
        node.id = node.uuid;
        delete node.oid;
        list.push(node);
        var children = node.children;

        if (children) {
            for (var i = 0, len = children.length; i < len; i++) {
                const child = children[i];
                child.parent = node.id;
                visit(children[i]);
            }
        }
        node.children = [];
    }

    visit(root);
    return list;
}

/**
 * @private
 */
const utils = {
    xmlToJson: xmlToJson,
    clone: clone,
    compressGuid: compressGuid,
    findNodeOfType: findNodeOfType,
    timeout: timeout,
    httpRequest: httpRequest,
    loadJSON: loadJSON,
    loadArraybuffer: loadArraybuffer$1,
    queryString: queryString,
    isArray: isArray$1,
    isString: isString,
    isNumeric: isNumeric,
    isID: isID,
    isSameComponent: isSameComponent,
    isFunction: isFunction$1,
    isObject: isObject$1,
    copy: copy,
    apply: apply,
    apply2: apply2,
    applyIf: applyIf,
    isEmptyObject: isEmptyObject$1,
    inQuotes: inQuotes,
    concat: concat,
    flattenParentChildHierarchy: flattenParentChildHierarchy
};

const scenesRenderInfo = {}; // Used for throttling FPS for each Scene
const sceneIDMap = new Map$1$1(); // Ensures unique scene IDs
const taskQueue = new Queue(); // Task queue, which is pumped on each frame; tasks are pushed to it with calls to xeokit.schedule
const tickEvent = {sceneId: null, time: null, startTime: null, prevTime: null, deltaTime: null};
const taskBudget = 10; // Millisecs we're allowed to spend on tasks in each frame
const fpsSamples = [];
const numFPSSamples = 30;

let lastTime = 0;
let elapsedTime;
let totalFPS = 0;


/**
 * @private
 */
function Core() {

    /**
     Semantic version number. The value for this is set by an expression that's concatenated to
     the end of the built binary by the xeokit build script.
     @property version
     @namespace xeokit
     @type {String}
     */
    this.version = "1.0.0";

    /**
     Existing {@link Scene}s , mapped to their IDs
     @property scenes
     @namespace xeokit
     @type {Scene}
     */
    this.scenes = {};

    this._superTypes = {}; // For each component type, a list of its supertypes, ordered upwards in the hierarchy.

    /**
     * Registers a scene on xeokit.
     * This is called within the xeokit.Scene constructor.
     * @private
     */
    this._addScene = function (scene) {
        if (scene.id) { // User-supplied ID
            if (core.scenes[scene.id]) {
                console.error(`[ERROR] Scene ${utils.inQuotes(scene.id)} already exists`);
                return;
            }
        } else { // Auto-generated ID
            scene.id = sceneIDMap.addItem({});
        }
        core.scenes[scene.id] = scene;
        const ticksPerOcclusionTest = scene.ticksPerOcclusionTest;
        const ticksPerRender = scene.ticksPerRender;
        scenesRenderInfo[scene.id] = {
            ticksPerOcclusionTest: ticksPerOcclusionTest,
            ticksPerRender: ticksPerRender,
            renderCountdown: ticksPerRender
        };
        stats.components.scenes++;
        scene.once("destroyed", () => { // Unregister destroyed scenes
            sceneIDMap.removeItem(scene.id);
            delete core.scenes[scene.id];
            delete scenesRenderInfo[scene.id];
            stats.components.scenes--;
        });
    };

    /**
     * @private
     */
    this.clear = function () {
        let scene;
        for (const id in core.scenes) {
            if (core.scenes.hasOwnProperty(id)) {
                scene = core.scenes[id];
                // Only clear the default Scene
                // but destroy all the others
                if (id === "default.scene") {
                    scene.clear();
                } else {
                    scene.destroy();
                    delete core.scenes[scene.id];
                }
            }
        }
    };

    /**
     * Schedule a task to run at the next frame.
     *
     * Internally, this pushes the task to a FIFO queue. Within each frame interval, xeokit processes the queue
     * for a certain period of time, popping tasks and running them. After each frame interval, tasks that did not
     * get a chance to run during the task are left in the queue to be run next time.
     *
     * @param {Function} callback Callback that runs the task.
     * @param {Object} [scope] Scope for the callback.
     */
    this.scheduleTask = function (callback, scope = null) {
        taskQueue.push(callback);
        taskQueue.push(scope);
    };

    this.runTasks = function (until = -1) { // Pops and processes tasks in the queue, until the given number of milliseconds has elapsed.
        let time = (new Date()).getTime();
        let callback;
        let scope;
        let tasksRun = 0;
        while (taskQueue.length > 0 && (until < 0 || time < until)) {
            callback = taskQueue.shift();
            scope = taskQueue.shift();
            if (scope) {
                callback.call(scope);
            } else {
                callback();
            }
            time = (new Date()).getTime();
            tasksRun++;
        }
        return tasksRun;
    };

    this.getNumTasks = function () {
        return taskQueue.length;
    };
}

/**
 * @private
 * @type {Core}
 */
const core = new Core();

const frame = function () {
    let time = Date.now();
    elapsedTime = time - lastTime;
    if (lastTime > 0 && elapsedTime > 0) { // Log FPS stats
        var newFPS = 1000 / elapsedTime; // Moving average of FPS
        totalFPS += newFPS;
        fpsSamples.push(newFPS);
        if (fpsSamples.length >= numFPSSamples) {
            totalFPS -= fpsSamples.shift();
        }
        stats.frame.fps = Math.round(totalFPS / fpsSamples.length);
    }
    for (let id in core.scenes) {
        core.scenes[id].compile();
    }
    runTasks(time);
    lastTime = time;
};

function customSetInterval(callback, interval) {
    let expected = Date.now() + interval;
    function loop() {
        const elapsed = Date.now() - expected;
        callback();
        expected += interval;
        setTimeout(loop, Math.max(0, interval - elapsed));
    }
    loop();
    return {
        cancel: function() {
            // No need to do anything, setTimeout cannot be directly cancelled
        }
    };
}

customSetInterval(() => {
    frame();
}, 100);

const renderFrame = function () {
    let time = Date.now();
    elapsedTime = time - lastTime;
    if (lastTime > 0 && elapsedTime > 0) { // Log FPS stats
        var newFPS = 1000 / elapsedTime; // Moving average of FPS
        totalFPS += newFPS;
        fpsSamples.push(newFPS);
        if (fpsSamples.length >= numFPSSamples) {
            totalFPS -= fpsSamples.shift();
        }
        stats.frame.fps = Math.round(totalFPS / fpsSamples.length);
    }
    runTasks(time);
    fireTickEvents(time);
    renderScenes();
    (window.requestPostAnimationFrame !== undefined) ? window.requestPostAnimationFrame(frame) : requestAnimationFrame(renderFrame);
};

renderFrame();

function runTasks(time) { // Process as many enqueued tasks as we can within the per-frame task budget
    const tasksRun = core.runTasks(time + taskBudget);
    const tasksScheduled = core.getNumTasks();
    stats.frame.tasksRun = tasksRun;
    stats.frame.tasksScheduled = tasksScheduled;
    stats.frame.tasksBudget = taskBudget;
}

function fireTickEvents(time) { // Fire tick event on each Scene
    tickEvent.time = time;
    for (var id in core.scenes) {
        if (core.scenes.hasOwnProperty(id)) {
            var scene = core.scenes[id];
            tickEvent.sceneId = id;
            tickEvent.startTime = scene.startTime;
            tickEvent.deltaTime = tickEvent.prevTime != null ? tickEvent.time - tickEvent.prevTime : 0;
            /**
             * Fired on each game loop iteration.
             *
             * @event tick
             * @param {String} sceneID The ID of this Scene.
             * @param {Number} startTime The time in seconds since 1970 that this Scene was instantiated.
             * @param {Number} time The time in seconds since 1970 of this "tick" event.
             * @param {Number} prevTime The time of the previous "tick" event from this Scene.
             * @param {Number} deltaTime The time in seconds since the previous "tick" event from this Scene.
             */
            scene.fire("tick", tickEvent, true);
        }
    }
    tickEvent.prevTime = time;
}

function renderScenes() {
    const scenes = core.scenes;
    const forceRender = false;
    let scene;
    let renderInfo;
    let ticksPerOcclusionTest;
    let ticksPerRender;
    let id;
    for (id in scenes) {
        if (scenes.hasOwnProperty(id)) {

            scene = scenes[id];
            renderInfo = scenesRenderInfo[id];

            if (!renderInfo) {
                renderInfo = scenesRenderInfo[id] = {}; // FIXME
            }

            ticksPerOcclusionTest = scene.ticksPerOcclusionTest;
            if (renderInfo.ticksPerOcclusionTest !== ticksPerOcclusionTest) {
                renderInfo.ticksPerOcclusionTest = ticksPerOcclusionTest;
                renderInfo.renderCountdown = ticksPerOcclusionTest;
            }
            if (--scene.occlusionTestCountdown <= 0) {
                scene.doOcclusionTest();
                scene.occlusionTestCountdown = ticksPerOcclusionTest;
            }

            ticksPerRender = scene.ticksPerRender;
            if (renderInfo.ticksPerRender !== ticksPerRender) {
                renderInfo.ticksPerRender = ticksPerRender;
                renderInfo.renderCountdown = ticksPerRender;
            }
            if (--renderInfo.renderCountdown === 0) {
                scene.render(forceRender);
                renderInfo.renderCountdown = ticksPerRender;
            }
        }
    }
}

/**
 * @desc Base class for all xeokit components.
 *
 * ## Component IDs
 *
 * Every Component has an ID that's unique within the parent {@link Scene}. xeokit generates
 * the IDs automatically by default, however you can also specify them yourself. In the example below, we're creating a
 * scene comprised of {@link Scene}, {@link Material}, {@link ReadableGeometry} and
 * {@link Mesh} components, while letting xeokit generate its own ID for
 * the {@link ReadableGeometry}:
 *
 *````JavaScript
 * import {Viewer, Mesh, buildTorusGeometry, ReadableGeometry, PhongMaterial, Texture, Fresnel} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *        canvasId: "myCanvas"
 *    });
 *
 * viewer.scene.camera.eye = [0, 0, 5];
 * viewer.scene.camera.look = [0, 0, 0];
 * viewer.scene.camera.up = [0, 1, 0];
 *
 * new Mesh(viewer.scene, {
 *      geometry: new ReadableGeometry(viewer.scene, buildTorusGeometry({
 *          center: [0, 0, 0],
 *          radius: 1.5,
 *          tube: 0.5,
 *          radialSegments: 32,
 *          tubeSegments: 24,
 *          arc: Math.PI * 2.0
 *      }),
 *      material: new PhongMaterial(viewer.scene, {
 *          id: "myMaterial",
 *          ambient: [0.9, 0.3, 0.9],
 *          shininess: 30,
 *          diffuseMap: new Texture(viewer.scene, {
 *              src: "textures/diffuse/uvGrid2.jpg"
 *          }),
 *          specularFresnel: new Fresnel(viewer.scene, {
 *              leftColor: [1.0, 1.0, 1.0],
 *              rightColor: [0.0, 0.0, 0.0],
 *              power: 4
 *          })
 *     })
 * });
 *````
 *
 * We can then find those components like this:
 *
 * ````javascript
 * // Find the Material
 * var material = viewer.scene.components["myMaterial"];
 *
 * // Find all PhongMaterials in the Scene
 * var phongMaterials = viewer.scene.types["PhongMaterial"];
 *
 * // Find our Material within the PhongMaterials
 * var materialAgain = phongMaterials["myMaterial"];
 * ````
 *
 * ## Restriction on IDs
 *
 * Auto-generated IDs are of the form ````"__0"````, ````"__1"````, ````"__2"```` ... and so on.
 *
 * Scene maintains a map of these IDs, along with a counter that it increments each time it generates a new ID.
 *
 * If Scene has created the IDs listed above, and we then destroy the ````Component```` with ID ````"__1"````,
 * Scene will mark that ID as available, and will reuse it for the next default ID.
 *
 * Therefore, two restrictions your on IDs:
 *
 * * don't use IDs that begin with two underscores, and
 * * don't reuse auto-generated IDs of destroyed Components.
 *
 * ## Logging
 *
 * Components have methods to log ID-prefixed messages to the JavaScript console:
 *
 * ````javascript
 * material.log("Everything is fine, situation normal.");
 * material.warn("Wait, whats that red light?");
 * material.error("Aw, snap!");
 * ````
 *
 * The logged messages will look like this in the console:
 *
 * ````text
 * [LOG]   myMaterial: Everything is fine, situation normal.
 * [WARN]  myMaterial: Wait, whats that red light..
 * [ERROR] myMaterial: Aw, snap!
 * ````
 *
 * ## Destruction
 *
 * Get notification of destruction of Components:
 *
 * ````javascript
 * material.once("destroyed", function() {
 *     this.log("Component was destroyed: " + this.id);
 * });
 * ````
 *
 * Or get notification of destruction of any Component within its {@link Scene}:
 *
 * ````javascript
 * scene.on("componentDestroyed", function(component) {
 *     this.log("Component was destroyed: " + component.id);
 * });
 * ````
 *
 * Then destroy a component like this:
 *
 * ````javascript
 * material.destroy();
 * ````
 */
class Component {

    /**
     @private
     */
    get type() {
        return "Component";
    }

    /**
     * @private
     */
    get isComponent() {
        return true;
    }

    constructor(owner = null, cfg = {}) {

        /**
         * The parent {@link Scene} that contains this Component.
         *
         * @property scene
         * @type {Scene}
         * @final
         */
        this.scene = null;

        if (this.type === "Scene") {
            this.scene = this;
            /**
             * The viewer that contains this Scene.
             * @property viewer
             * @type {Viewer}
             */
            this.viewer = cfg.viewer;
        } else {
            if (owner.type === "Scene") {
                this.scene = owner;
            } else if (owner instanceof Component) {
                this.scene = owner.scene;
            } else {
                throw "Invalid param: owner must be a Component"
            }
            this._owner = owner;
        }

        this._dontClear = !!cfg.dontClear; // Prevent Scene#clear from destroying this component

        this._renderer = this.scene._renderer;

        /**
         Arbitrary, user-defined metadata on this component.

         @property metadata
         @type Object
         */
        this.meta = cfg.meta || {};


        /**
         * ID of this Component, unique within the {@link Scene}.
         *
         * Components are mapped by this ID in {@link Scene#components}.
         *
         * @property id
         * @type {String|Number}
         */
        this.id = cfg.id; // Auto-generated by Scene by default

        /**
         True as soon as this Component has been destroyed

         @property destroyed
         @type {Boolean}
         */
        this.destroyed = false;

        this._attached = {}; // Attached components with names.
        this._attachments = null; // Attached components keyed to IDs - lazy-instantiated
        this._subIdMap = null; // Subscription subId pool
        this._subIdEvents = null; // Subscription subIds mapped to event names
        this._eventSubs = null; // Event names mapped to subscribers
        this._eventSubsNum = null;
        this._events = null; // Maps names to events
        this._eventCallDepth = 0; // Helps us catch stack overflows from recursive events
        this._ownedComponents = null; // // Components created with #create - lazy-instantiated

        if (this !== this.scene) { // Don't add scene to itself
            this.scene._addComponent(this); // Assigns this component an automatic ID if not yet assigned
        }

        this._updateScheduled = false; // True when #_update will be called on next tick

        if (owner) {
            owner._own(this);
        }
    }

    // /**
    //  * Unique ID for this Component within its {@link Scene}.
    //  *
    //  * @property
    //  * @type {String}
    //  */
    // get id() {
    //     return this._id;
    // }

    /**
     Indicates that we need to redraw the scene.

     This is called by certain subclasses after they have made some sort of state update that requires the
     renderer to perform a redraw.

     For example: a {@link Mesh} calls this on itself whenever you update its
     {@link Mesh#layer} property, which manually controls its render order in
     relation to other Meshes.

     If this component has a ````castsShadow```` property that's set ````true````, then this will also indicate
     that the renderer needs to redraw shadow map associated with this component. Components like
     {@link DirLight} have that property set when they produce light that creates shadows, while
     components like {@link Mesh"}}layer{{/crossLink}} have that property set when they cast shadows.

     @protected
     */
    glRedraw() {
        if (!this._renderer) { // Called from a constructor
            return;
        }
        this._renderer.imageDirty();
        if (this.castsShadow) { // Light source or object
            this._renderer.shadowsDirty();
        }
    }

    /**
     Indicates that we need to re-sort the renderer's state-ordered drawables list.

     For efficiency, the renderer keeps its list of drawables ordered so that runs of the same state updates can be
     combined.  This method is called by certain subclasses after they have made some sort of state update that would
     require re-ordering of the drawables list.

     For example: a {@link DirLight} calls this on itself whenever you update {@link DirLight#dir}.

     @protected
     */
    glResort() {
        if (!this._renderer) { // Called from a constructor
            return;
        }
        this._renderer.needStateSort();
    }

    /**
     * The {@link Component} that owns the lifecycle of this Component, if any.
     *
     * When that component is destroyed, this component will be automatically destroyed also.
     *
     * Will be null if this Component has no owner.
     *
     * @property owner
     * @type {Component}
     */
    get owner() {
        return this._owner;
    }

    /**
     * Tests if this component is of the given type, or is a subclass of the given type.
     * @type {Boolean}
     */
    isType(type) {
        return this.type === type;
    }

    /**
     * Fires an event on this component.
     *
     * Notifies existing subscribers to the event, optionally retains the event to give to
     * any subsequent notifications on the event as they are made.
     *
     * @param {String} event The event type name
     * @param {Object} value The event parameters
     * @param {Boolean} [forget=false] When true, does not retain for subsequent subscribers
     */
    fire(event, value, forget) {
        if (!this._events) {
            this._events = {};
        }
        if (!this._eventSubs) {
            this._eventSubs = {};
            this._eventSubsNum = {};
        }
        if (forget !== true) {
            this._events[event] = value || true; // Save notification
        }
        const subs = this._eventSubs[event];
        let sub;
        if (subs) { // Notify subscriptions
            for (const subId in subs) {
                if (subs.hasOwnProperty(subId)) {
                    sub = subs[subId];
                    this._eventCallDepth++;
                    if (this._eventCallDepth < 300) {
                        sub.callback.call(sub.scope, value);
                    } else {
                        this.error("fire: potential stack overflow from recursive event '" + event + "' - dropping this event");
                    }
                    this._eventCallDepth--;
                }
            }
        }
    }

    /**
     * Subscribes to an event on this component.
     *
     * The callback is be called with this component as scope.
     *
     * @param {String} event The event
     * @param {Function} callback Called fired on the event
     * @param {Object} [scope=this] Scope for the callback
     * @return {String} Handle to the subscription, which may be used to unsubscribe with {@link #off}.
     */
    on(event, callback, scope) {
        if (!this._events) {
            this._events = {};
        }
        if (!this._subIdMap) {
            this._subIdMap = new Map$1$1(); // Subscription subId pool
        }
        if (!this._subIdEvents) {
            this._subIdEvents = {};
        }
        if (!this._eventSubs) {
            this._eventSubs = {};
        }
        if (!this._eventSubsNum) {
            this._eventSubsNum = {};
        }
        let subs = this._eventSubs[event];
        if (!subs) {
            subs = {};
            this._eventSubs[event] = subs;
            this._eventSubsNum[event] = 1;
        } else {
            this._eventSubsNum[event]++;
        }
        const subId = this._subIdMap.addItem(); // Create unique subId
        subs[subId] = {
            callback: callback,
            scope: scope || this
        };
        this._subIdEvents[subId] = event;
        const value = this._events[event];
        if (value !== undefined) { // A publication exists, notify callback immediately
            callback.call(scope || this, value);
        }
        return subId;
    }

    /**
     * Cancels an event subscription that was previously made with {@link Component#on} or {@link Component#once}.
     *
     * @param {String} subId Subscription ID
     */
    off(subId) {
        if (subId === undefined || subId === null) {
            return;
        }
        if (!this._subIdEvents) {
            return;
        }
        const event = this._subIdEvents[subId];
        if (event) {
            delete this._subIdEvents[subId];
            const subs = this._eventSubs[event];
            if (subs) {
                delete subs[subId];
                this._eventSubsNum[event]--;
            }
            this._subIdMap.removeItem(subId); // Release subId
        }
    }

    /**
     * Subscribes to the next occurrence of the given event, then un-subscribes as soon as the event is subIdd.
     *
     * This is equivalent to calling {@link Component#on}, and then calling {@link Component#off} inside the callback function.
     *
     * @param {String} event Data event to listen to
     * @param {Function} callback Called when fresh data is available at the event
     * @param {Object} [scope=this] Scope for the callback
     */
    once(event, callback, scope) {
        const self = this;
        const subId = this.on(event,
            function (value) {
                self.off(subId);
                callback.call(scope || this, value);
            },
            scope);
    }

    /**
     * Returns true if there are any subscribers to the given event on this component.
     *
     * @param {String} event The event
     * @return {Boolean} True if there are any subscribers to the given event on this component.
     */
    hasSubs(event) {
        return (this._eventSubsNum && (this._eventSubsNum[event] > 0));
    }

    /**
     * Logs a console debugging message for this component.
     *
     * The console message will have this format: *````[LOG] [<component type> <component id>: <message>````*
     *
     * Also fires the message as a "log" event on the parent {@link Scene}.
     *
     * @param {String} message The message to log
     */
    log(message) {
        message = "[LOG]" + this._message(message);
        window.console.log(message);
        this.scene.fire("log", message);
    }

    _message(message) {
        return " [" + this.type + " " + utils.inQuotes(this.id) + "]: " + message;
    }

    /**
     * Logs a warning for this component to the JavaScript console.
     *
     * The console message will have this format: *````[WARN] [<component type> =<component id>: <message>````*
     *
     * Also fires the message as a "warn" event on the parent {@link Scene}.
     *
     * @param {String} message The message to log
     */
    warn(message) {
        message = "[WARN]" + this._message(message);
        window.console.warn(message);
        this.scene.fire("warn", message);
    }

    /**
     * Logs an error for this component to the JavaScript console.
     *
     * The console message will have this format: *````[ERROR] [<component type> =<component id>: <message>````*
     *
     * Also fires the message as an "error" event on the parent {@link Scene}.
     *
     * @param {String} message The message to log
     */
    error(message) {
        message = "[ERROR]" + this._message(message);
        window.console.error(message);
        this.scene.fire("error", message);
    }

    /**
     * Adds a child component to this.
     *
     * When component not given, attaches the scene's default instance for the given name (if any).
     * Publishes the new child component on this component, keyed to the given name.
     *
     * @param {*} params
     * @param {String} params.name component name
     * @param {Component} [params.component] The component
     * @param {String} [params.type] Optional expected type of base type of the child; when supplied, will
     * cause an exception if the given child is not the same type or a subtype of this.
     * @param {Boolean} [params.sceneDefault=false]
     * @param {Boolean} [params.sceneSingleton=false]
     * @param {Function} [params.onAttached] Optional callback called when component attached
     * @param {Function} [params.onAttached.callback] Callback function
     * @param {Function} [params.onAttached.scope] Optional scope for callback
     * @param {Function} [params.onDetached] Optional callback called when component is detached
     * @param {Function} [params.onDetached.callback] Callback function
     * @param {Function} [params.onDetached.scope] Optional scope for callback
     * @param {{String:Function}} [params.on] Callbacks to subscribe to properties on component
     * @param {Boolean} [params.recompiles=true] When true, fires "dirty" events on this component
     * @private
     */
    _attach(params) {

        const name = params.name;

        if (!name) {
            this.error("Component 'name' expected");
            return;
        }

        let component = params.component;
        const sceneDefault = params.sceneDefault;
        const sceneSingleton = params.sceneSingleton;
        const type = params.type;
        const on = params.on;
        const recompiles = params.recompiles !== false;

        // True when child given as config object, where parent manages its instantiation and destruction
        let managingLifecycle = false;

        if (component) {

            if (utils.isNumeric(component) || utils.isString(component)) {

                // Component ID given
                // Both numeric and string IDs are supported

                const id = component;

                component = this.scene.components[id];

                if (!component) {

                    // Quote string IDs in errors

                    this.error("Component not found: " + utils.inQuotes(id));
                    return;
                }
            }
        }

        if (!component) {

            if (sceneSingleton === true) {

                // Using the first instance of the component type we find

                const instances = this.scene.types[type];
                for (const id2 in instances) {
                    if (instances.hasOwnProperty) {
                        component = instances[id2];
                        break;
                    }
                }

                if (!component) {
                    this.error("Scene has no default component for '" + name + "'");
                    return null;
                }

            } else if (sceneDefault === true) {

                // Using a default scene component

                component = this.scene[name];

                if (!component) {
                    this.error("Scene has no default component for '" + name + "'");
                    return null;
                }
            }
        }

        if (component) {

            if (component.scene.id !== this.scene.id) {
                this.error("Not in same scene: " + component.type + " " + utils.inQuotes(component.id));
                return;
            }

            if (type) {

                if (!component.isType(type)) {
                    this.error("Expected a " + type + " type or subtype: " + component.type + " " + utils.inQuotes(component.id));
                    return;
                }
            }
        }

        if (!this._attachments) {
            this._attachments = {};
        }

        const oldComponent = this._attached[name];
        let subs;
        let i;
        let len;

        if (oldComponent) {

            if (component && oldComponent.id === component.id) {

                // Reject attempt to reattach same component
                return;
            }

            const oldAttachment = this._attachments[oldComponent.id];

            // Unsubscribe from events on old component

            subs = oldAttachment.subs;

            for (i = 0, len = subs.length; i < len; i++) {
                oldComponent.off(subs[i]);
            }

            delete this._attached[name];
            delete this._attachments[oldComponent.id];

            const onDetached = oldAttachment.params.onDetached;
            if (onDetached) {
                if (utils.isFunction(onDetached)) {
                    onDetached(oldComponent);
                } else {
                    onDetached.scope ? onDetached.callback.call(onDetached.scope, oldComponent) : onDetached.callback(oldComponent);
                }
            }

            if (oldAttachment.managingLifecycle) {

                // Note that we just unsubscribed from all events fired by the child
                // component, so destroying it won't fire events back at us now.

                oldComponent.destroy();
            }
        }

        if (component) {

            // Set and publish the new component on this component

            const attachment = {
                params: params,
                component: component,
                subs: [],
                managingLifecycle: managingLifecycle
            };

            attachment.subs.push(
                component.once("destroyed",
                    function () {
                        attachment.params.component = null;
                        this._attach(attachment.params);
                    },
                    this));

            if (recompiles) {
                attachment.subs.push(
                    component.on("dirty",
                        function () {
                            this.fire("dirty", this);
                        },
                        this));
            }

            this._attached[name] = component;
            this._attachments[component.id] = attachment;

            // Bind destruct listener to new component to remove it
            // from this component when destroyed

            const onAttached = params.onAttached;
            if (onAttached) {
                if (utils.isFunction(onAttached)) {
                    onAttached(component);
                } else {
                    onAttached.scope ? onAttached.callback.call(onAttached.scope, component) : onAttached.callback(component);
                }
            }

            if (on) {

                let event;
                let subIdr;
                let callback;
                let scope;

                for (event in on) {
                    if (on.hasOwnProperty(event)) {

                        subIdr = on[event];

                        if (utils.isFunction(subIdr)) {
                            callback = subIdr;
                            scope = null;
                        } else {
                            callback = subIdr.callback;
                            scope = subIdr.scope;
                        }

                        if (!callback) {
                            continue;
                        }

                        attachment.subs.push(component.on(event, callback, scope));
                    }
                }
            }
        }

        if (recompiles) {
            this.fire("dirty", this); // FIXME: May trigger spurous mesh recompilations unless able to limit with param?
        }

        this.fire(name, component); // Component can be null

        return component;
    }

    _checkComponent(expectedType, component) {
        if (!component.isComponent) {
            if (utils.isID(component)) {
                const id = component;
                component = this.scene.components[id];
                if (!component) {
                    this.error("Component not found: " + id);
                    return;
                }
            } else {
                this.error("Expected a Component or ID");
                return;
            }
        }
        if (expectedType !== component.type) {
            this.error("Expected a " + expectedType + " Component");
            return;
        }
        if (component.scene.id !== this.scene.id) {
            this.error("Not in same scene: " + component.type);
            return;
        }
        return component;
    }

    _checkComponent2(expectedTypes, component) {
        if (!component.isComponent) {
            if (utils.isID(component)) {
                const id = component;
                component = this.scene.components[id];
                if (!component) {
                    this.error("Component not found: " + id);
                    return;
                }
            } else {
                this.error("Expected a Component or ID");
                return;
            }
        }
        if (component.scene.id !== this.scene.id) {
            this.error("Not in same scene: " + component.type);
            return;
        }
        for (var i = 0, len = expectedTypes.length; i < len; i++) {
            if (expectedTypes[i] === component.type) {
                return component;
            }
        }
        this.error("Expected component types: " + expectedTypes);
        return null;
    }

    _own(component) {
        if (!this._ownedComponents) {
            this._ownedComponents = {};
        }
        if (!this._ownedComponents[component.id]) {
            this._ownedComponents[component.id] = component;
        }
        component.once("destroyed", () => {
            delete this._ownedComponents[component.id];
        }, this);
    }

    /**
     * Protected method, called by sub-classes to queue a call to _update().
     * @protected
     * @param {Number} [priority=1]
     */
    _needUpdate(priority) {
        if (!this._updateScheduled) {
            this._updateScheduled = true;
            if (priority === 0) {
                this._doUpdate();
            } else {
                core.scheduleTask(this._doUpdate, this);
            }
        }
    }

    /**
     * @private
     */
    _doUpdate() {
        if (this._updateScheduled) {
            this._updateScheduled = false;
            if (this._update) {
                this._update();
            }
        }
    }

    /**
     * Schedule a task to perform on the next browser interval
     * @param task
     */
    scheduleTask(task) {
        core.scheduleTask(task, null);
    }

    /**
     * Protected virtual template method, optionally implemented
     * by sub-classes to perform a scheduled task.
     *
     * @protected
     */
    _update() {
    }

    /**
     * Destroys all {@link Component}s that are owned by this. These are Components that were instantiated with
     * this Component as their first constructor argument.
     */
    clear() {
        if (this._ownedComponents) {
            for (var id in this._ownedComponents) {
                if (this._ownedComponents.hasOwnProperty(id)) {
                    const component = this._ownedComponents[id];
                    component.destroy();
                    delete this._ownedComponents[id];
                }
            }
        }
    }

    /**
     * Destroys this component.
     */
    destroy() {

        if (this.destroyed) {
            return;
        }

        /**
         * Fired when this Component is destroyed.
         * @event destroyed
         */
        this.fire("destroyed", this.destroyed = true); // Must fire before we blow away subscription maps, below

        // Unsubscribe from child components and destroy then

        let id;
        let attachment;
        let component;
        let subs;
        let i;
        let len;

        if (this._attachments) {
            for (id in this._attachments) {
                if (this._attachments.hasOwnProperty(id)) {
                    attachment = this._attachments[id];
                    component = attachment.component;
                    subs = attachment.subs;
                    for (i = 0, len = subs.length; i < len; i++) {
                        component.off(subs[i]);
                    }
                    if (attachment.managingLifecycle) {
                        component.destroy();
                    }
                }
            }
        }

        if (this._ownedComponents) {
            for (id in this._ownedComponents) {
                if (this._ownedComponents.hasOwnProperty(id)) {
                    component = this._ownedComponents[id];
                    component.destroy();
                    delete this._ownedComponents[id];
                }
            }
        }

        this.scene._removeComponent(this);

        // Memory leak avoidance
        this._attached = {};
        this._attachments = null;
        this._subIdMap = null;
        this._subIdEvents = null;
        this._eventSubs = null;
        this._events = null;
        this._eventCallDepth = 0;
        this._ownedComponents = null;
        this._updateScheduled = false;
    }
}

const tempVec3a$O = math.vec3();
const tempVec3b$C = math.vec3();
const tempMat4a$s = math.mat4();

/**
 * @private
 */
class FrustumPlane {

    constructor() {
        this.normal = math.vec3();
        this.offset = 0;
        this.testVertex = math.vec3();
    }

    set(nx, ny, nz, offset) {
        const s = 1.0 / Math.sqrt(nx * nx + ny * ny + nz * nz);
        this.normal[0] = nx * s;
        this.normal[1] = ny * s;
        this.normal[2] = nz * s;
        this.offset = offset * s;
        this.testVertex[0] = (this.normal[0] >= 0.0) ? 1 : 0;
        this.testVertex[1] = (this.normal[1] >= 0.0) ? 1 : 0;
        this.testVertex[2] = (this.normal[2] >= 0.0) ? 1 : 0;
    }
}

/**
 * @private
 */
class Frustum$1 {
    constructor() {
        this.planes = [
            new FrustumPlane(), new FrustumPlane(), new FrustumPlane(),
            new FrustumPlane(), new FrustumPlane(), new FrustumPlane()
        ];
    }
}

Frustum$1.INSIDE = 0;
Frustum$1.INTERSECT = 1;
Frustum$1.OUTSIDE = 2;

/** @private */
function setFrustum(frustum, viewMat, projMat) {

    const m = math.mulMat4(projMat, viewMat, tempMat4a$s);

    const m0 = m[0];
    const m1 = m[1];
    const m2 = m[2];
    const m3 = m[3];
    const m4 = m[4];
    const m5 = m[5];
    const m6 = m[6];
    const m7 = m[7];
    const m8 = m[8];
    const m9 = m[9];
    const m10 = m[10];
    const m11 = m[11];
    const m12 = m[12];
    const m13 = m[13];
    const m14 = m[14];
    const m15 = m[15];

    frustum.planes[0].set(m3 - m0, m7 - m4, m11 - m8, m15 - m12);
    frustum.planes[1].set(m3 + m0, m7 + m4, m11 + m8, m15 + m12);
    frustum.planes[2].set(m3 - m1, m7 - m5, m11 - m9, m15 - m13);
    frustum.planes[3].set(m3 + m1, m7 + m5, m11 + m9, m15 + m13);
    frustum.planes[4].set(m3 - m2, m7 - m6, m11 - m10, m15 - m14);
    frustum.planes[5].set(m3 + m2, m7 + m6, m11 + m10, m15 + m14);
}

/** @private */
function frustumIntersectsAABB3(frustum, aabb) {

    let ret = Frustum$1.INSIDE;

    const min = tempVec3a$O;
    const max = tempVec3b$C;

    min[0] = aabb[0];
    min[1] = aabb[1];
    min[2] = aabb[2];
    max[0] = aabb[3];
    max[1] = aabb[4];
    max[2] = aabb[5];

    const bminmax = [min, max];

    for (let i = 0; i < 6; ++i) {
        const plane = frustum.planes[i];
        if (((plane.normal[0] * bminmax[plane.testVertex[0]][0]) +
            (plane.normal[1] * bminmax[plane.testVertex[1]][1]) +
            (plane.normal[2] * bminmax[plane.testVertex[2]][2]) +
            (plane.offset)) < 0.0) {
            return Frustum$1.OUTSIDE;
        }

        if (((plane.normal[0] * bminmax[1 - plane.testVertex[0]][0]) +
            (plane.normal[1] * bminmax[1 - plane.testVertex[1]][1]) +
            (plane.normal[2] * bminmax[1 - plane.testVertex[2]][2]) +
            (plane.offset)) < 0.0) {
            ret = Frustum$1.INTERSECT;
        }
    }

    return ret;
}

/**
 * Picks a {@link Viewer}'s {@link Entity}s with a canvas-space 2D marquee box.
 *
 * [<img src="https://xeokit.github.io/xeokit-sdk/assets/images/MarqueeSelect.gif">](https://xeokit.github.io/xeokit-sdk/examples/picking/#marqueePick_select)
 *
 * * [[Example 1: Select Objects with Marquee](https://xeokit.github.io/xeokit-sdk/examples/picking/#marqueePick_select)]
 * * [[Example 2: View-Fit Objects with Marquee](https://xeokit.github.io/xeokit-sdk/examples/picking/#marqueePick_viewFit)]
 *
 * # Usage
 *
 * In the example below, we
 *
 * 1. Create a {@link Viewer}, arrange the {@link Camera}
 * 2. Use an {@link XKTLoaderPlugin} to load a BIM model,
 * 3. Create a {@link ObjectsKdTree3} to automatically index the `Viewer's` {@link Entity}s for fast spatial lookup,
 * 4. Create a `MarqueePicker` to pick {@link Entity}s in the {@link Viewer}, using the {@link ObjectsKdTree3} to accelerate picking
 * 5. Create a {@link MarqueePickerMouseControl} to perform the marquee-picking with the `MarqueePicker`, using mouse input to draw the marquee box on the `Viewer's` canvas.
 *
 * When the {@link MarqueePickerMouseControl} is active:
 *
 * * Long-click, drag and release on the canvas to define a marque box that picks {@link Entity}s.
 * * Drag left-to-right to pick {@link Entity}s that intersect the box.
 * * Drag right-to-left to pick {@link Entity}s that are fully inside the box.
 * * On release, the `MarqueePicker` will fire a "picked" event with IDs of the picked {@link Entity}s, if any.
 * * Handling that event, we mark the {@link Entity}s as selected.
 * * Hold down CTRL to multi-pick.
 *
 * ````javascript
 * import {
 *         Viewer,
 *         XKTLoaderPlugin,
 *         ObjectsKdTree3,
 *         MarqueePicker,
 *         MarqueePickerMouseControl
 * } from "xeokit-sdk.es.js";
 *
 * // 1
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas"
 * });
 *
 * viewer.scene.camera.eye = [14.9, 14.3, 5.4];
 * viewer.scene.camera.look = [6.5, 8.3, -4.1];
 * viewer.scene.camera.up = [-0.28, 0.9, -0.3];
 *
 * // 2
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const sceneModel = xktLoader.load({
 *     id: "myModel",
 *     src: "../../assets/models/xkt/v8/ifc/HolterTower.ifc.xkt"
 * });
 *
 * // 3
 *
 * const objectsKdTree3 = new ObjectsKdTree3({viewer});
 *
 * // 4
 *
 * const marqueePicker = new MarqueePicker({viewer, objectsKdTree3});
 *
 * // 5
 *
 * const marqueePickerMouseControl = new MarqueePickerMouseControl({marqueePicker});
 *
 * marqueePicker.on("clear", () => {
 *     viewer.scene.setObjectsSelected(viewer.scene.selectedObjectIds, false);
 * });
 *
 * marqueePicker.on("picked", (objectIds) => {
 *     viewer.scene.setObjectsSelected(objectIds, true);
 * });
 *
 * marqueePickerMouseControl.setActive(true);
 * ````
 *
 * # Design Notes
 *
 * * The {@link ObjectsKdTree3} can be shared with any other components that want to use it to spatially search for {@link Entity}s.
 * * The {@link MarqueePickerMouseControl} can be replaced with other types of controllers (i.e. touch), or used alongside them.
 * * The `MarqueePicker` has no input handlers of its own, and provides an API through which to programmatically control marquee picking. By firing the "picked" events, `MarqueePicker` implements the *Blackboard Pattern*.
 */
class MarqueePicker extends Component {

    /**
     * Creates a MarqueePicker.
     *
     * @param {*} cfg Configuration
     * @param {Viewer} cfg.viewer The Viewer to pick Entities from.
     * @param {ObjectsKdTree3} cfg.objectsKdTree3 A k-d tree that indexes the Entities in the Viewer for fast spatial lookup.
     */
    constructor(cfg = {}) {

        if (!cfg.viewer) {
            throw "[MarqueePicker] Missing config: viewer";
        }

        if (!cfg.objectsKdTree3) {
            throw "[MarqueePicker] Missing config: objectsKdTree3";
        }

        super(cfg.viewer.scene, cfg);

        this.viewer = cfg.viewer;
        this._objectsKdTree3 = cfg.objectsKdTree3;
        this._canvasMarqueeCorner1 = math.vec2();
        this._canvasMarqueeCorner2 = math.vec2();
        this._canvasMarquee = math.AABB2();
        this._marqueeFrustum = new Frustum$1();
        this._marqueeFrustumProjMat = math.mat4();
        this._pickMode = false;

        this._marqueeElement = document.createElement('div');
        document.body.appendChild(this._marqueeElement);

        this._marqueeElement.style.position = "absolute";
        this._marqueeElement.style["z-index"] = "40000005";
        this._marqueeElement.style.width = 8 + "px";
        this._marqueeElement.style.height = 8 + "px";
        this._marqueeElement.style.visibility = "hidden";
        this._marqueeElement.style.top = 0 + "px";
        this._marqueeElement.style.left = 0 + "px";
        this._marqueeElement.style["box-shadow"] = "0 2px 5px 0 #182A3D;";
        this._marqueeElement.style["opacity"] = 1.0;
        this._marqueeElement.style["pointer-events"] = "none";
    }

    /**
     * Sets the canvas-space position of the first marquee box corner.
     *
     * @param corner1
     */
    setMarqueeCorner1(corner1) {
        this._canvasMarqueeCorner1.set(corner1);
        this._canvasMarqueeCorner2.set(corner1);
        this._updateMarquee();
    }

    /**
     * Sets the canvas-space position of the second marquee box corner.
     *
     * @param corner2
     */
    setMarqueeCorner2(corner2) {
        this._canvasMarqueeCorner2.set(corner2);
        this._updateMarquee();
    }

    /**
     * Sets both canvas-space corner positions of the marquee box.
     *
     * @param corner1
     * @param corner2
     */
    setMarquee(corner1, corner2) {
        this._canvasMarqueeCorner1.set(corner1);
        this._canvasMarqueeCorner2.set(corner2);
        this._updateMarquee();
    }

    /**
     * Sets if the marquee box is visible.
     *
     * @param {boolean} visible True if the marquee box is to be visible, else false.
     */
    setMarqueeVisible(visible) {
        this._marqueVisible = visible;
        this._marqueeElement.style.visibility = visible ? "visible" : "hidden";
    }

    /**
     * Gets if the marquee box is visible.
     *
     * @returns {boolean} True if the marquee box is visible, else false.
     */
    getMarqueeVisible() {
        return this._marqueVisible;
    }

    /**
     * Sets the pick mode.
     *
     * Supported pick modes are:
     *
     * * MarqueePicker.PICK_MODE_INSIDE - picks {@link Entity}s that are completely inside the marquee box.
     * * MarqueePicker.PICK_MODE_INTERSECTS - picks {@link Entity}s that intersect the marquee box.
     *
     * @param {number} pickMode The pick mode.
     */
    setPickMode(pickMode) {
        if (pickMode !== MarqueePicker.PICK_MODE_INSIDE && pickMode !== MarqueePicker.PICK_MODE_INTERSECTS) {
            throw "Illegal MarqueePicker pickMode: must be MarqueePicker.PICK_MODE_INSIDE or MarqueePicker.PICK_MODE_INTERSECTS";
        }
        if (pickMode !== this._pickMode) {
            this._marqueeElement.style["background-image"] =
                pickMode === MarqueePicker.PICK_MODE_INSIDE
                    /* Solid */ ? "url(\"data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='6' ry='6' stroke='%23333' stroke-width='4'/%3e%3c/svg%3e\")"
                    /* Dashed */ : "url(\"data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='6' ry='6' stroke='%23333' stroke-width='4' stroke-dasharray='6%2c 14' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e\")";
            this._pickMode = pickMode;
        }
    }

    /**
     * Gets the pick mode.
     *
     * Supported pick modes are:
     *
     * * MarqueePicker.PICK_MODE_INSIDE - picks {@link Entity}s that are completely inside the marquee box.
     * * MarqueePicker.PICK_MODE_INTERSECTS - picks {@link Entity}s that intersect the marquee box.
     *
     * @returns {number} The pick mode.
     */
    getPickMode() {
        return this._pickMode;
    }

    /**
     * Fires a "clear" event on this MarqueePicker.
     */
    clear() {
        this.fire("clear", {});
    }

    /**
     * Attempts to pick {@link Entity}s, using the current MarquePicker settings.
     *
     * Fires a "picked" event with the IDs of the {@link Entity}s that were picked, if any.
     *
     * @returns {string[]} IDs of the {@link Entity}s that were picked, if any
     */
    pick() {
        this._updateMarquee();
        this._buildMarqueeFrustum();
        const entityIds = [];
        const visitNode = (node, intersects = Frustum$1.INTERSECT) => {
            if (intersects === Frustum$1.INTERSECT) {
                intersects = frustumIntersectsAABB3(this._marqueeFrustum, node.aabb);
            }
            if (intersects === Frustum$1.OUTSIDE) {
                return;
            }
            if (node.entities) {
                const entities = node.entities;
                for (let i = 0, len = entities.length; i < len; i++) {
                    const entity = entities[i];
                    if (!entity.visible) {
                        continue;
                    }
                    const entityAABB = entity.aabb;
                    if (this._pickMode === MarqueePicker.PICK_MODE_INSIDE) {
                        // Select entities that are completely inside marquee
                        const intersection = frustumIntersectsAABB3(this._marqueeFrustum, entityAABB);
                        if (intersection === Frustum$1.INSIDE) {
                            entityIds.push(entity.id);
                        }
                    } else {
                        // Select entities that are partially inside marquee
                        const intersection = frustumIntersectsAABB3(this._marqueeFrustum, entityAABB);
                        if (intersection !== Frustum$1.OUTSIDE) {
                            entityIds.push(entity.id);
                        }
                    }
                }
            }
            if (node.left) {
                visitNode(node.left, intersects);
            }
            if (node.right) {
                visitNode(node.right, intersects);
            }
        };
        if (this._canvasMarquee[2] - this._canvasMarquee[0] > 3 || this._canvasMarquee[3] - this._canvasMarquee[1] > 3) { // Marquee pick if rectangle big enough
            visitNode(this._objectsKdTree3.root);
        }
        this.fire("picked", entityIds);
        return entityIds;
    }

    _updateMarquee() {
        this._canvasMarquee[0] = Math.min(this._canvasMarqueeCorner1[0], this._canvasMarqueeCorner2[0]);
        this._canvasMarquee[1] = Math.min(this._canvasMarqueeCorner1[1], this._canvasMarqueeCorner2[1]);
        this._canvasMarquee[2] = Math.max(this._canvasMarqueeCorner1[0], this._canvasMarqueeCorner2[0]);
        this._canvasMarquee[3] = Math.max(this._canvasMarqueeCorner1[1], this._canvasMarqueeCorner2[1]);
        this._marqueeElement.style.width = `${this._canvasMarquee[2] - this._canvasMarquee[0]}px`;
        this._marqueeElement.style.height = `${this._canvasMarquee[3] - this._canvasMarquee[1]}px`;
        this._marqueeElement.style.left = `${this._canvasMarquee[0]}px`;
        this._marqueeElement.style.top = `${this._canvasMarquee[1]}px`;
    }

    _buildMarqueeFrustum() { // https://github.com/xeokit/xeokit-sdk/issues/869#issuecomment-1165375770
        const canvas = this.viewer.scene.canvas.canvas;
        const canvasWidth = canvas.clientWidth;
        const canvasHeight = canvas.clientHeight;
        const canvasLeft = canvas.clientLeft;
        const canvasTop = canvas.clientTop;
        const xCanvasToClip = 2.0 / canvasWidth;
        const yCanvasToClip = 2.0 / canvasHeight;
        const NEAR_SCALING = 17;
        const ratio = canvas.clientHeight / canvas.clientWidth;
        const FAR_PLANE = 10000;
        const left = (this._canvasMarquee[0] - canvasLeft) * xCanvasToClip + -1;
        const right = (this._canvasMarquee[2] - canvasLeft) * xCanvasToClip + -1;
        const bottom = -(this._canvasMarquee[3] - canvasTop) * yCanvasToClip + 1;
        const top = -(this._canvasMarquee[1] - canvasTop) * yCanvasToClip + 1;
        const near = this.viewer.scene.camera.frustum.near * (NEAR_SCALING * ratio);
        const far = FAR_PLANE;
        math.frustumMat4(
            left,
            right,
            bottom * ratio,
            top * ratio,
            near,
            far,
            this._marqueeFrustumProjMat,
        );
        setFrustum(this._marqueeFrustum, this.viewer.scene.camera.viewMatrix, this._marqueeFrustumProjMat);
    }

    /**
     * Destroys this MarqueePicker.
     *
     * Does not destroy the {@link Viewer} or the {@link ObjectsKdTree3} provided to the constructor of this MarqueePicker.
     */
    destroy() {
        super.destroy();
        if (this._marqueeElement.parentElement) {
            this._marqueeElement.parentElement.removeChild(this._marqueeElement);
            this._marqueeElement = null;
            this._objectsKdTree3 = null;
        }
    }
}

/**
 * Pick mode that picks {@link Entity}s that intersect the marquee box.
 *
 * @type {number}
 */
MarqueePicker.PICK_MODE_INTERSECTS = 0;

/**
 * Pick mode that picks {@link Entity}s that are completely inside the marquee box.
 *
 * @type {number}
 */
MarqueePicker.PICK_MODE_INSIDE = 1;

/**
 * @desc Localization service for a {@link Viewer}.
 *
 * * A LocaleService is a container of string translations ("messages") for various locales.
 * * A {@link Viewer} has its own default LocaleService at {@link Viewer#localeService}.
 * * We can replace that with our own LocaleService, or a custom subclass, via the Viewer's constructor.
 * * Viewer plugins that need localized translations will attempt to them for the currently active locale from the LocaleService.
 * * Whenever we switch the LocaleService to a different locale, plugins will automatically refresh their translations for that locale.
 *
 * ## Usage
 *
 * In the example below, we'll create a {@link Viewer} that uses an {@link XKTLoaderPlugin} to load a BIM model, and a
 * {@link NavCubePlugin}, which shows a camera navigation cube in the corner of the canvas.
 *
 * We'll also configure our Viewer with our own LocaleService instance, configured with English, Māori and French
 * translations for our NavCubePlugin.
 *
 * We could instead have just used the Viewer's default LocaleService, but this example demonstrates how we might
 * configure the Viewer our own custom LocaleService subclass.
 *
 * The translations fetched by our NavCubePlugin will be:
 *
 *  * "NavCube.front"
 *  * "NavCube.back"
 *  * "NavCube.top"
 *  * "NavCube.bottom"
 *  * "NavCube.left"
 *  * "NavCube.right"
 *
 * <br>
 * These are paths that resolve to our translations for the currently active locale, and are hard-coded within
 * the NavCubePlugin.
 *
 * For example, if  the LocaleService's locale is set to "fr", then the path "NavCube.back" will drill down
 * into ````messages->fr->NavCube->front```` and fetch "Arrière".
 *
 * If we didn't provide that particular translation in our LocaleService, or any translations for that locale,
 * then the NavCubePlugin will just fall back on its own default hard-coded translation, which in this case is "BACK".
 *
 * [[Run example](https://xeokit.github.io/xeokit-sdk/examples/index.html#localization_NavCubePlugin)]
 *
 * ````javascript
 * import {Viewer, LocaleService, NavCubePlugin, XKTLoaderPlugin} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *
 *      canvasId: "myCanvas",
 *
 *      localeService: new LocaleService({
 *          messages: {
 *              "en": { // English
 *                  "NavCube": {
 *                      "front": "Front",
 *                      "back": "Back",
 *                      "top": "Top",
 *                      "bottom": "Bottom",
 *                      "left": "Left",
 *                      "right": "Right"
 *                  }
 *              },
 *              "mi": { // Māori
 *                  "NavCube": {
 *                      "front": "Mua",
 *                      "back": "Tuarā",
 *                      "top": "Runga",
 *                      "bottom": "Raro",
 *                      "left": "Mauī",
 *                      "right": "Tika"
 *                  }
 *              },
 *              "fr": { // Francais
 *                  "NavCube": {
 *                      "front": "Avant",
 *                      "back": "Arrière",
 *                      "top": "Supérieur",
 *                      "bottom": "Inférieur",
 *                      "left": "Gauche",
 *                      "right": "Droit"
 *                  }
 *              }
 *          },
 *          locale: "en"
 *      })
 *  });
 *
 * viewer.camera.eye = [-3.93, 2.85, 27.01];
 * viewer.camera.look = [4.40, 3.72, 8.89];
 * viewer.camera.up = [-0.01, 0.99, 0.03];
 *
 * const navCubePlugin = new NavCubePlugin(viewer, {
 *      canvasID: "myNavCubeCanvas"
 *  });
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const model = xktLoader.load({
 *     id: "myModel",
 *     src: "./models/xkt/Duplex.ifc.xkt",
 *     edges: true
 * });
 * ````
 *
 * We can dynamically switch our Viewer to a different locale at any time, which will update the text on the
 * faces of our NavCube:
 *
 * ````javascript
 * viewer.localeService.locale = "mi"; // Switch to Māori
 * ````
 *
 * We can load new translations at any time:
 *
 * ````javascript
 * viewer.localeService.loadMessages({
 *     "jp": { // Japanese
 *         "NavCube": {
 *             "front": "前部",
 *             "back": "裏",
 *             "top": "上",
 *             "bottom": "底",
 *             "left": "左",
 *             "right": "右"
 *         }
 *     }
 * });
 * ````
 *
 * And we can clear the translations if needed:
 *
 * ````javascript
 * viewer.localeService.clearMessages();
 * ````
 *
 * We can get an "updated" event from the LocaleService whenever we switch locales or load messages, which is useful
 * for triggering UI elements to refresh themselves with updated translations. Internally, our {@link NavCubePlugin}
 * subscribes to this event, fetching new strings for itself via {@link LocaleService#translate} each time the
 * event is fired.
 *
 * ````javascript
 * viewer.localeService.on("updated", () => {
 *     console.log( viewer.localeService.translate("NavCube.left") );
 * });
 * ````
 * @since 2.0
 */
class LocaleService {

    /**
     * Constructs a LocaleService.
     *
     * @param {*} [params={}]
     * @param {JSON} [params.messages]
     * @param {String} [params.locale]
     */
    constructor(params = {}) {

        this._eventSubIDMap = null;
        this._eventSubEvents = null;
        this._eventSubs = null;
        this._events = null;

        this._locale = "en";
        this._messages = {};
        this._locales = [];
        this._locale = "en";

        this.messages = params.messages;
        this.locale = params.locale;
    }

    /**
     * Replaces the current set of locale translations.
     *
     * * Fires an "updated" event when done.
     * * Automatically refreshes any plugins that depend on the translations.
     * * Does not change the current locale.
     *
     * ## Usage
     *
     * ````javascript
     * viewer.localeService.setMessages({
     *     messages: {
     *         "en": { // English
     *             "NavCube": {
     *                 "front": "Front",
     *                 "back": "Back",
     *                 "top": "Top",
     *                 "bottom": "Bottom",
     *                 "left": "Left",
     *                 "right": "Right"
     *             }
     *         },
     *         "mi": { // Māori
     *             "NavCube": {
     *                 "front": "Mua",
     *                 "back": "Tuarā",
     *                 "top": "Runga",
     *                 "bottom": "Raro",
     *                 "left": "Mauī",
     *                 "right": "Tika"
     *             }
     *         }
     *    }
     * });
     * ````
     *
     * @param {*} messages The new translations.
     */
    set messages(messages) {
        this._messages = messages || {};
        this._locales = Object.keys(this._messages);
        this.fire("updated", this);
    }

    /**
     * Loads a new set of locale translations, adding them to the existing translations.
     *
     * * Fires an "updated" event when done.
     * * Automatically refreshes any plugins that depend on the translations.
     * * Does not change the current locale.
     *
     * ## Usage
     *
     * ````javascript
     * viewer.localeService.loadMessages({
     *     "jp": { // Japanese
     *         "NavCube": {
     *             "front": "前部",
     *             "back": "裏",
     *             "top": "上",
     *             "bottom": "底",
     *             "left": "左",
     *             "right": "右"
     *         }
     *     }
     * });
     * ````
     *
     * @param {*} messages The new translations.
     */
    loadMessages(messages = {}) {
        for (let locale in messages) {
            this._messages[locale] = messages[locale];
        }
        this.messages = this._messages;
    }

    /**
     * Clears all locale translations.
     *
     * * Fires an "updated" event when done.
     * * Does not change the current locale.
     * * Automatically refreshes any plugins that depend on the translations, which will cause those
     * plugins to fall back on their internal hard-coded text values, since this method removes all
     * our translations.
     */
    clearMessages() {
        this.messages = {};
    }

    /**
     * Gets the list of available locales.
     *
     * These are derived from the currently configured set of translations.
     *
     * @returns {String[]} The list of available locales.
     */
    get locales() {
        return this._locales;
    }

    /**
     * Sets the current locale.
     *
     * * Fires an "updated" event when done.
     * * The given locale does not need to be in the list of available locales returned by {@link LocaleService#locales}, since
     * this method assumes that you may want to load the locales at a later point.
     * * Automatically refreshes any plugins that depend on the translations.
     * * We can then get translations for the locale, if translations have been loaded for it, via {@link LocaleService#translate} and {@link LocaleService#translatePlurals}.
     *
     * @param {String} locale The new current locale.
     */
    set locale(locale) {
        locale = locale || "de";
        if (this._locale === locale) {
            return;
        }
        this._locale = locale;
        this.fire("updated", locale);
    }

    /**
     * Gets the current locale.
     *
     * @returns {String} The current locale.
     */
    get locale() {
        return this._locale;
    }

    /**
     * Translates the given string according to the current locale.
     *
     * Returns null if no translation can be found.
     *
     * @param {String} msg String to translate.
     * @param {*} [args] Extra parameters.
     * @returns {String|null} Translated string if found, else null.
     */
    translate(msg, args) {
        const localeMessages = this._messages[this._locale];
        if (!localeMessages) {
            return null;
        }
        const localeMessage = resolvePath$1(msg, localeMessages);
        if (localeMessage) {
            if (args) {
                return vsprintf(localeMessage, args);
            }
            return localeMessage;
        }
        return null;
    }

    /**
     * Translates the given phrase according to the current locale.
     *
     * Returns null if no translation can be found.
     *
     * @param {String} msg Phrase to translate.
     * @param {Number} count The plural number.
     * @param {*} [args] Extra parameters.
     * @returns {String|null} Translated string if found, else null.
     */
    translatePlurals(msg, count, args) {
        const localeMessages = this._messages[this._locale];
        if (!localeMessages) {
            return null;
        }
        let localeMessage = resolvePath$1(msg, localeMessages);
        count = parseInt("" + count, 10);
        if (count === 0) {
            localeMessage = localeMessage.zero;
        } else {
            localeMessage = (count > 1) ? localeMessage.other : localeMessage.one;
        }
        if (!localeMessage) {
            return null;
        }
        localeMessage = vsprintf(localeMessage, [count]);
        if (args) {
            localeMessage = vsprintf(localeMessage, args);
        }
        return localeMessage;
    }

    /**
     * Fires an event on this LocaleService.
     *
     * Notifies existing subscribers to the event, optionally retains the event to give to
     * any subsequent notifications on the event as they are made.
     *
     * @param {String} event The event type name.
     * @param {Object} value The event parameters.
     * @param {Boolean} [forget=false] When true, does not retain for subsequent subscribers.
     */
    fire(event, value, forget) {
        if (!this._events) {
            this._events = {};
        }
        if (!this._eventSubs) {
            this._eventSubs = {};
        }
        if (forget !== true) {
            this._events[event] = value || true; // Save notification
        }
        const subs = this._eventSubs[event];
        if (subs) {
            for (const subId in subs) {
                if (subs.hasOwnProperty(subId)) {
                    const sub = subs[subId];
                    sub.callback(value);
                }
            }
        }
    }

    /**
     * Subscribes to an event on this LocaleService.
     *
     * @param {String} event The event
     * @param {Function} callback Callback fired on the event
     * @return {String} Handle to the subscription, which may be used to unsubscribe with {@link #off}.
     */
    on(event, callback) {
        if (!this._events) {
            this._events = {};
        }
        if (!this._eventSubIDMap) {
            this._eventSubIDMap = new Map$1$1(); // Subscription subId pool
        }
        if (!this._eventSubEvents) {
            this._eventSubEvents = {};
        }
        if (!this._eventSubs) {
            this._eventSubs = {};
        }
        let subs = this._eventSubs[event];
        if (!subs) {
            subs = {};
            this._eventSubs[event] = subs;
        }
        const subId = this._eventSubIDMap.addItem(); // Create unique subId
        subs[subId] = {
            callback: callback
        };
        this._eventSubEvents[subId] = event;
        const value = this._events[event];
        if (value !== undefined) {
            callback(value);
        }
        return subId;
    }

    /**
     * Cancels an event subscription that was previously made with {@link LocaleService#on}.
     *
     * @param {String} subId Subscription ID
     */
    off(subId) {
        if (subId === undefined || subId === null) {
            return;
        }
        if (!this._eventSubEvents) {
            return;
        }
        const event = this._eventSubEvents[subId];
        if (event) {
            delete this._eventSubEvents[subId];
            const subs = this._eventSubs[event];
            if (subs) {
                delete subs[subId];
            }
            this._eventSubIDMap.removeItem(subId); // Release subId
        }
    }
}

function resolvePath$1(key, json) {
    if (json[key]) {
        return json[key];
    }
    const parts = key.split(".");
    let obj = json;
    for (let i = 0, len = parts.length; obj && (i < len); i++) {
        const part = parts[i];
        obj = obj[part];
    }
    return obj;
}

function vsprintf(msg, args = []) {
    return msg.replace(/\{\{|\}\}|\{(\d+)\}/g, function (m, n) {
        if (m === "{{") {
            return "{";
        }
        if (m === "}}") {
            return "}";
        }
        return args[n];
    });
}

math.vec3();

const tempVec3$6 = math.vec3();
const newLook = math.vec3();
const newEye = math.vec3();
const newUp = math.vec3();
const newLookEyeVec = math.vec3();

/**
 * @desc Jumps or flies the {@link Scene}'s {@link Camera} to a given target.
 *
 * * Located at {@link Viewer#cameraFlight}
 * * Can fly or jump to its target.
 * * While flying, can be stopped, or redirected to a different target.
 * * Can also smoothly transition between ortho and perspective projections.
 *
 *
 * A CameraFlightAnimation's target can be:
 *
 * * specific ````eye````, ````look```` and ````up```` positions,
 * * an axis-aligned World-space bounding box (AABB), or
 * * an instance or ID of any {@link Component} subtype that provides a World-space AABB.
 *
 * A target can also contain a ````projection```` type to transition into. For example, if your {@link Camera#projection} is
 * currently ````"perspective"```` and you supply {@link CameraFlightAnimation#flyTo} with a ````projection```` property
 * equal to "ortho", then CameraFlightAnimation will smoothly transition the Camera into an orthographic projection.
 *
 * Configure {@link CameraFlightAnimation#fit} and {@link CameraFlightAnimation#fitFOV} to make it stop at the point
 * where the target occupies a certain amount of the field-of-view.
 *
 * ## Flying to an Entity
 *
 * Flying to an {@link Entity}:
 *
 * ````Javascript
 * var entity = new Mesh(viewer.scene);
 *
 * // Fly to the Entity's World-space AABB
 * viewer.cameraFlight.flyTo(entity);
 * ````
 * ## Flying to a Position
 *
 * Flying the CameraFlightAnimation from the previous example to specified eye, look and up positions:
 *
 * ````Javascript
 * viewer.cameraFlight.flyTo({
 *    eye: [-5,-5,-5],
 *    look: [0,0,0]
 *    up: [0,1,0],
 *    duration: 1 // Default, seconds
 * },() => {
 *      // Done
 * });
 * ````
 *
 * ## Flying to an AABB
 *
 * Flying the CameraFlightAnimation from the previous two examples explicitly to the {@link Boundary3D"}}Boundary3D's{{/crossLink}}
 * axis-aligned bounding box:
 *
 * ````Javascript
 * viewer.cameraFlight.flyTo(entity.aabb);
 * ````
 *
 * ## Transitioning Between Projections
 *
 * CameraFlightAnimation also allows us to smoothly transition between Camera projections. We can do that by itself, or
 * in addition to flying the Camera to a target.
 *
 * Let's transition the Camera to orthographic projection:
 *
 * [[Run example](/examples/index.html#camera_CameraFlightAnimation_projection)]
 *
 * ````Javascript
 * viewer.cameraFlight.flyTo({ projection: "ortho", () => {
 *      // Done
 * });
 * ````
 *
 * Now let's transition the Camera back to perspective projection:
 *
 * ````Javascript
 * viewer.cameraFlight.flyTo({ projection: "perspective"}, () => {
 *      // Done
 * });
 * ````
 *
 * Fly Camera to a position, while transitioning to orthographic projection:
 *
 * ````Javascript
 * viewer.cameraFlight.flyTo({
 *     eye: [-100,20,2],
 *     look: [0,0,-40],
 *     up: [0,1,0],
 *     projection: "ortho", () => {
 *        // Done
 * });
 * ````
 */
class CameraFlightAnimation extends Component {

    /**
     * @private
     */
    get type() {
        return "CameraFlightAnimation";
    }

    /**
     @constructor
     @private
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._look1 = math.vec3();
        this._eye1 = math.vec3();
        this._up1 = math.vec3();
        this._look2 = math.vec3();
        this._eye2 = math.vec3();
        this._up2 = math.vec3();
        this._orthoScale1 = 1;
        this._orthoScale2 = 1;
        this._flying = false;
        this._flyEyeLookUp = false;
        this._flyingEye = false;
        this._flyingLook = false;
        this._callback = null;
        this._callbackScope = null;
        this._time1 = null;
        this._time2 = null;
        this.easing = cfg.easing !== false;

        this.duration = cfg.duration;
        this.fit = cfg.fit;
        this.fitFOV = cfg.fitFOV;
        this.trail = cfg.trail;
    }

    /**
     * Flies the {@link Camera} to a target.
     *
     *  * When the target is a boundary, the {@link Camera} will fly towards the target and stop when the target fills most of the canvas.
     *  * When the target is an explicit {@link Camera} position, given as ````eye````, ````look```` and ````up````, then CameraFlightAnimation will interpolate the {@link Camera} to that target and stop there.
     *
     * @param {Object|Component} [params=Scene] Either a parameters object or a {@link Component} subtype that has
     * an AABB. Defaults to the {@link Scene}, which causes the {@link Camera} to fit the Scene in view.
     * @param {Number} [params.arc=0] Factor in range ````[0..1]```` indicating how much the {@link Camera#eye} position
     * will swing away from its {@link Camera#look} position as it flies to the target.
     * @param {Number|String|Component} [params.component] ID or instance of a component to fly to. Defaults to the entire {@link Scene}.
     * @param {Number[]} [params.aabb] World-space axis-aligned bounding box (AABB) target to fly to.
     * @param {Number[]} [params.eye] Position to fly the eye position to.
     * @param {Number[]} [params.look] Position to fly the look position to.
     * @param {Number[]} [params.up] Position to fly the up vector to.
     * @param {String} [params.projection] Projection type to transition into as we fly. Can be any of the values of {@link Camera.projection}.
     * @param {Boolean} [params.fit=true] Whether to fit the target to the view volume. Overrides {@link CameraFlightAnimation#fit}.
     * @param {Number} [params.fitFOV] How much of field-of-view, in degrees, that a target {@link Entity} or its AABB should
     * fill the canvas on arrival. Overrides {@link CameraFlightAnimation#fitFOV}.
     * @param {Number} [params.duration] Flight duration in seconds.  Overrides {@link CameraFlightAnimation#duration}.
     * @param {Number} [params.orthoScale] Animate the Camera's orthographic scale to this target value. See {@link Ortho#scale}.
     * @param {Function} [callback] Callback fired on arrival.
     * @param {Object} [scope] Optional scope for callback.
     */
    flyTo(params, callback, scope) {

        params = params || this.scene;

        if (this._flying) {
            this.stop();
        }

        this._flying = false;
        this._flyingEye = false;
        this._flyingLook = false;
        this._flyingEyeLookUp = false;

        this._callback = callback;
        this._callbackScope = scope;

        const camera = this.scene.camera;
        const flyToProjection = (!!params.projection) && (params.projection !== camera.projection);

        this._eye1[0] = camera.eye[0];
        this._eye1[1] = camera.eye[1];
        this._eye1[2] = camera.eye[2];

        this._look1[0] = camera.look[0];
        this._look1[1] = camera.look[1];
        this._look1[2] = camera.look[2];

        this._up1[0] = camera.up[0];
        this._up1[1] = camera.up[1];
        this._up1[2] = camera.up[2];

        this._orthoScale1 = camera.ortho.scale;
        this._orthoScale2 = params.orthoScale || this._orthoScale1;

        let aabb;
        let eye;
        let look;
        let up;
        let componentId;

        if (params.aabb) {
            aabb = params.aabb;

        } else if (params.length === 6) {
            aabb = params;

        } else if ((params.eye && params.look) || params.up) {
            eye = params.eye;
            look = params.look;
            up = params.up;

        } else if (params.eye) {
            eye = params.eye;

        } else if (params.look) {
            look = params.look;

        } else { // Argument must be an instance or ID of a Component (subtype)

            let component = params;

            if (utils.isNumeric(component) || utils.isString(component)) {

                componentId = component;
                component = this.scene.components[componentId];

                if (!component) {
                    this.error("Component not found: " + utils.inQuotes(componentId));
                    if (callback) {
                        if (scope) {
                            callback.call(scope);
                        } else {
                            callback();
                        }
                    }
                    return;
                }
            }
            if (!flyToProjection) {
                aabb = component.aabb || this.scene.aabb;
            }
        }

        const poi = params.poi;

        if (aabb) {

            if (aabb[3] < aabb[0] || aabb[4] < aabb[1] || aabb[5] < aabb[2]) { // Don't fly to an inverted boundary
                return;
            }

            if (aabb[3] === aabb[0] && aabb[4] === aabb[1] && aabb[5] === aabb[2]) { // Don't fly to an empty boundary
                return;
            }

            aabb = aabb.slice();
            const aabbCenter = math.getAABB3Center(aabb);

            this._look2 = poi || aabbCenter;

            const eyeLookVec = math.subVec3(this._eye1, this._look1, tempVec3$6);
            const eyeLookVecNorm = math.normalizeVec3(eyeLookVec);
            const diag = poi ? math.getAABB3DiagPoint(aabb, poi) : math.getAABB3Diag(aabb);
            const fitFOV = params.fitFOV || this._fitFOV;
            const sca = Math.abs(diag / Math.tan(fitFOV * math.DEGTORAD));

            this._orthoScale2 = diag * 1.1;

            this._eye2[0] = this._look2[0] + (eyeLookVecNorm[0] * sca);
            this._eye2[1] = this._look2[1] + (eyeLookVecNorm[1] * sca);
            this._eye2[2] = this._look2[2] + (eyeLookVecNorm[2] * sca);

            this._up2[0] = this._up1[0];
            this._up2[1] = this._up1[1];
            this._up2[2] = this._up1[2];

            this._flyingEyeLookUp = true;

        } else if (eye || look || up) {

            this._flyingEyeLookUp = !!eye && !!look && !!up;
            this._flyingEye = !!eye && !look;
            this._flyingLook = !!look && !eye;

            if (eye) {
                this._eye2[0] = eye[0];
                this._eye2[1] = eye[1];
                this._eye2[2] = eye[2];
            }

            if (look) {
                this._look2[0] = look[0];
                this._look2[1] = look[1];
                this._look2[2] = look[2];
            }

            if (up) {
                this._up2[0] = up[0];
                this._up2[1] = up[1];
                this._up2[2] = up[2];
            }
        }

        if (flyToProjection) {

            if (params.projection === "ortho" && camera.projection !== "ortho") {
                this._projection2 = "ortho";
                this._projMatrix1 = camera.projMatrix.slice();
                this._projMatrix2 = camera.ortho.matrix.slice();
                camera.projection = "customProjection";
            }

            if (params.projection === "perspective" && camera.projection !== "perspective") {
                this._projection2 = "perspective";
                this._projMatrix1 = camera.projMatrix.slice();
                this._projMatrix2 = camera.perspective.matrix.slice();
                camera.projection = "customProjection";
            }
        } else {
            this._projection2 = null;
        }

        this.fire("started", params, true);

        this._time1 = Date.now();
        this._time2 = this._time1 + (Number.isFinite(params.duration) ? params.duration * 1000 : this._duration);

        this._flying = true; // False as soon as we stop

        core.scheduleTask(this._update, this);
    }

    /**
     * Jumps the {@link Scene}'s {@link Camera} to the given target.
     *
     * * When the target is a boundary, this CameraFlightAnimation will position the {@link Camera} at where the target fills most of the canvas.
     * * When the target is an explicit {@link Camera} position, given as ````eye````, ````look```` and ````up```` vectors, then this CameraFlightAnimation will jump the {@link Camera} to that target.
     *
     * @param {*|Component} params  Either a parameters object or a {@link Component} subtype that has a World-space AABB.
     * @param {Number} [params.arc=0]  Factor in range [0..1] indicating how much the {@link Camera#eye} will swing away from its {@link Camera#look} as it flies to the target.
     * @param {Number|String|Component} [params.component] ID or instance of a component to fly to.
     * @param {Number[]} [params.aabb]  World-space axis-aligned bounding box (AABB) target to fly to.
     * @param {Number[]} [params.eye] Position to fly the eye position to.
     * @param {Number[]} [params.look]  Position to fly the look position to.
     * @param {Number[]} [params.up] Position to fly the up vector to.
     * @param {String} [params.projection] Projection type to transition into. Can be any of the values of {@link Camera.projection}.
     * @param {Number} [params.fitFOV] How much of field-of-view, in degrees, that a target {@link Entity} or its AABB should fill the canvas on arrival. Overrides {@link CameraFlightAnimation#fitFOV}.
     * @param {Boolean} [params.fit] Whether to fit the target to the view volume. Overrides {@link CameraFlightAnimation#fit}.
     */
    jumpTo(params) {
        this._jumpTo(params);
    }

    _jumpTo(params) {

        if (this._flying) {
            this.stop();
        }

        const camera = this.scene.camera;

        var aabb;
        var componentId;
        var newEye;
        var newLook;
        var newUp;

        if (params.aabb) { // Boundary3D
            aabb = params.aabb;

        } else if (params.length === 6) { // AABB
            aabb = params;

        } else if (params.eye || params.look || params.up) { // Camera pose
            newEye = params.eye;
            newLook = params.look;
            newUp = params.up;

        } else { // Argument must be an instance or ID of a Component (subtype)

            let component = params;

            if (utils.isNumeric(component) || utils.isString(component)) {
                componentId = component;
                component = this.scene.components[componentId];
                if (!component) {
                    this.error("Component not found: " + utils.inQuotes(componentId));
                    return;
                }
            }
            aabb = component.aabb || this.scene.aabb;
        }

        const poi = params.poi;

        if (aabb) {

            if (aabb[3] <= aabb[0] || aabb[4] <= aabb[1] || aabb[5] <= aabb[2]) { // Don't fly to an empty boundary
                return;
            }

            var diag = poi ? math.getAABB3DiagPoint(aabb, poi) : math.getAABB3Diag(aabb);

            newLook = poi || math.getAABB3Center(aabb, newLook);

            if (this._trail) {
                math.subVec3(camera.look, newLook, newLookEyeVec);
            } else {
                math.subVec3(camera.eye, camera.look, newLookEyeVec);
            }

            math.normalizeVec3(newLookEyeVec);
            let dist;
            const fit = (params.fit !== undefined) ? params.fit : this._fit;

            if (fit) {
                dist = Math.abs((diag) / Math.tan((params.fitFOV || this._fitFOV) * math.DEGTORAD));

            } else {
                dist = math.lenVec3(math.subVec3(camera.eye, camera.look, tempVec3$6));
            }

            math.mulVec3Scalar(newLookEyeVec, dist);

            camera.eye = math.addVec3(newLook, newLookEyeVec, tempVec3$6);
            camera.look = newLook;

            this.scene.camera.ortho.scale = diag * 1.1;

        } else if (newEye || newLook || newUp) {

            if (newEye) {
                camera.eye = newEye;
            }
            if (newLook) {
                camera.look = newLook;
            }
            if (newUp) {
                camera.up = newUp;
            }
        }

        if (params.projection) {
            camera.projection = params.projection;
        }
    }

    _update() {
        if (!this._flying) {
            return;
        }
        const time = Date.now();
        let t = (time - this._time1) / (this._time2 - this._time1);
        const stopping = (t >= 1);

        if (t > 1) {
            t = 1;
        }

        const tFlight = this.easing ? CameraFlightAnimation._ease(t, 0, 1, 1) : t;
        const camera = this.scene.camera;

        if (this._flyingEye || this._flyingLook) {

            if (this._flyingEye) {
                math.subVec3(camera.eye, camera.look, newLookEyeVec);
                camera.eye = math.lerpVec3(tFlight, 0, 1, this._eye1, this._eye2, newEye);
                camera.look = math.subVec3(newEye, newLookEyeVec, newLook);
            } else if (this._flyingLook) {
                camera.look = math.lerpVec3(tFlight, 0, 1, this._look1, this._look2, newLook);
                camera.up = math.lerpVec3(tFlight, 0, 1, this._up1, this._up2, newUp);
            }

        } else if (this._flyingEyeLookUp) {

            camera.eye = math.lerpVec3(tFlight, 0, 1, this._eye1, this._eye2, newEye);
            camera.look = math.lerpVec3(tFlight, 0, 1, this._look1, this._look2, newLook);
            camera.up = math.lerpVec3(tFlight, 0, 1, this._up1, this._up2, newUp);
        }

        if (this._projection2) {
            const tProj = (this._projection2 === "ortho") ? CameraFlightAnimation._easeOutExpo(t, 0, 1, 1) : CameraFlightAnimation._easeInCubic(t, 0, 1, 1);
            camera.customProjection.matrix = math.lerpMat4(tProj, 0, 1, this._projMatrix1, this._projMatrix2);

        } else {
            camera.ortho.scale = this._orthoScale1 + (t * (this._orthoScale2 - this._orthoScale1));
        }

        if (stopping) {
            camera.ortho.scale = this._orthoScale2;
            this.stop();
            return;
        }
        core.scheduleTask(this._update, this); // Keep flying
    }

    static _ease(t, b, c, d) { // Quadratic easing out - decelerating to zero velocity http://gizma.com/easing
        t /= d;
        return -c * t * (t - 2) + b;
    }

    static _easeInCubic(t, b, c, d) {
        t /= d;
        return c * t * t * t + b;
    }

    static _easeOutExpo(t, b, c, d) {
        return c * (-Math.pow(2, -10 * t / d) + 1) + b;
    }

    /**
     * Stops an earlier flyTo, fires arrival callback.
     */
    stop() {
        if (!this._flying) {
            return;
        }
        this._flying = false;
        this._time1 = null;
        this._time2 = null;
        if (this._projection2) {
            this.scene.camera.projection = this._projection2;
        }
        const callback = this._callback;
        if (callback) {
            this._callback = null;
            if (this._callbackScope) {
                callback.call(this._callbackScope);
            } else {
                callback();
            }
        }
        this.fire("stopped", true, true);
    }

    /**
     * Cancels an earlier flyTo without calling the arrival callback.
     */
    cancel() {
        if (!this._flying) {
            return;
        }
        this._flying = false;
        this._time1 = null;
        this._time2 = null;
        if (this._callback) {
            this._callback = null;
        }
        this.fire("canceled", true, true);
    }

    /**
     * Sets the flight duration, in seconds, when calling {@link CameraFlightAnimation#flyTo}.
     *
     * Stops any flight currently in progress.
     *
     * default value is ````0.5````.
     *
     * @param {Number} value New duration value.
     */
    set duration(value) {
        this._duration = Number.isFinite(value) ? (value * 1000.0) : 500;
        this.stop();
    }

    /**
     * Gets the flight duration, in seconds, when calling {@link CameraFlightAnimation#flyTo}.
     *
     * default value is ````0.5````.
     *
     * @returns {Number} New duration value.
     */
    get duration() {
        return this._duration / 1000.0;
    }

    /**
     * Sets if, when CameraFlightAnimation is flying to a boundary, it will always adjust the distance between the
     * {@link Camera#eye} and {@link Camera#look} so as to ensure that the target boundary is always filling the view volume.
     *
     * When false, the eye will remain at its current distance from the look position.
     *
     * Default value is ````true````.
     *
     * @param {Boolean} value Set ````true```` to activate this behaviour.
     */
    set fit(value) {
        this._fit = value !== false;
    }

    /**
     * Gets if, when CameraFlightAnimation is flying to a boundary, it will always adjust the distance between the
     * {@link Camera#eye} and {@link Camera#look} so as to ensure that the target boundary is always filling the view volume.
     *
     * When false, the eye will remain at its current distance from the look position.
     *
     * Default value is ````true````.
     *
     * @returns {Boolean} value Set ````true```` to activate this behaviour.
     */
    get fit() {
        return this._fit;
    }

    /**
     * Sets how much of the perspective field-of-view, in degrees, that a target {@link Entity#aabb} should
     * fill the canvas when calling {@link CameraFlightAnimation#flyTo} or {@link CameraFlightAnimation#jumpTo}.
     *
     * Default value is ````45````.
     *
     * @param {Number} value New FOV value.
     */
    set fitFOV(value) {
        this._fitFOV = value || 45;
    }

    /**
     * Gets how much of the perspective field-of-view, in degrees, that a target {@link Entity#aabb} should
     * fill the canvas when calling {@link CameraFlightAnimation#flyTo} or {@link CameraFlightAnimation#jumpTo}.
     *
     * Default value is ````45````.
     *
     * @returns {Number} Current FOV value.
     */
    get fitFOV() {
        return this._fitFOV;
    }

    /**
     * Sets if this CameraFlightAnimation to point the {@link Camera}
     * in the direction that it is travelling when flying to a target after calling {@link CameraFlightAnimation#flyTo}.
     *
     * Default value is ````true````.
     *
     * @param {Boolean} value Set ````true```` to activate trailing behaviour.
     */
    set trail(value) {
        this._trail = !!value;
    }

    /**
     * Gets if this CameraFlightAnimation points the {@link Camera}
     * in the direction that it is travelling when flying to a target after calling {@link CameraFlightAnimation#flyTo}.
     *
     * Default value is ````true````.
     *
     * @returns {Boolean} True if trailing behaviour is active.
     */
    get trail() {
        return this._trail;
    }

    /**
     * @private
     */
    destroy() {
        this.stop();
        super.destroy();
    }
}

/**
 * @desc Animates the {@link Scene}'s's {@link Camera} along a {@link CameraPath}.
 *
 * ## Usage
 *
 * In the example below, we'll load a model using a {@link GLTFLoaderPlugin}, then animate a {@link Camera}
 * through the frames in a {@link CameraPath}.
 *
 *  * [[Run this example](/examples/index.html#camera_CameraPathAnimation)]
 *
 * ````Javascript
 * import {Viewer, GLTFLoaderPlugin, CameraPath, CameraPathAnimation} from "xeokit-sdk.es.js";
 *
 * // Create a Viewer and arrange camera
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.camera.eye = [124.86756896972656, -93.50288391113281, 173.2632598876953];
 * viewer.camera.look = [102.14186096191406, -90.24193572998047, 173.4224395751953];
 * viewer.camera.up = [0.23516440391540527, 0.9719591736793518, -0.0016466031083837152];
 *
 * // Load model
 *
 * const gltfLoader = new GLTFLoaderPlugin(viewer);
 *
 * const model = gltfLoader.load({
 *     id: "myModel",
 *     src: "./models/gltf/modern_office/scene.gltf",
 *     edges: true,
 *     edgeThreshold: 20,
 *     xrayed: false
 * });
 *
 * // Create a CameraPath
 *
 * var cameraPath = new CameraPath(viewer.scene, {
 *     frames: [
 *         {
 *             t:    0,
 *             eye:  [124.86, -93.50, 173.26],
 *             look: [102.14, -90.24, 173.42],
 *             up:   [0.23, 0.97, -0.00]
 *         },
 *         {
 *             t:    1,
 *             eye:  [79.75, -85.98, 226.57],
 *             look: [99.24, -84.11, 238.56],
 *             up:   [-0.14, 0.98, -0.09]
 *         },
 *         // Rest of the frames omitted for brevity
 *     ]
 * });
 *
 * // Create a CameraPathAnimation to play our CameraPath
 *
 * var cameraPathAnimation = new CameraPathAnimation(viewer.scene, {
 *     cameraPath: cameraPath,
 *     playingRate: 0.2 // Playing 0.2 time units per second
 * });
 *
 * // Once model loaded, start playing after a couple of seconds delay
 *
 * model.on("loaded", function () {
 *     setTimeout(function () {
 *         cameraPathAnimation.play(0); // Play from the beginning of the CameraPath
 *     }, 2000);
 * });
 * ````
 */
class CameraPathAnimation extends Component {

    /**
     * Returns "CameraPathAnimation".
     *
     * @private
     * @returns {string} "CameraPathAnimation"
     */
    get type() {
        return "CameraPathAnimation"
    }

    /**
     * @constructor
     * @param {Component} [owner]  Owner component. When destroyed, the owner will destroy this CameraPathAnimation as well.
     * @param {*} [cfg] Configuration
     * @param {String} [cfg.id]  Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted.
     * @param {CameraPath} [cfg.eyeCurve] A {@link CameraPath} that defines the path of a {@link Camera}.
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._cameraFlightAnimation = new CameraFlightAnimation(this);
        this._t = 0;
        this.state = CameraPathAnimation.SCRUBBING;
        this._playingFromT = 0;
        this._playingToT = 0;
        this._playingRate = cfg.playingRate || 1.0;
        this._playingDir = 1.0;
        this._lastTime = null;

        this.cameraPath = cfg.cameraPath;

        this._tick = this.scene.on("tick", this._updateT, this);
    }

    _updateT() {
        const cameraPath = this._cameraPath;
        if (!cameraPath) {
            return;
        }
        let numFrames;
        let t;
        const time = performance.now();
        const elapsedSecs = (this._lastTime) ? (time - this._lastTime) * 0.001 : 0;
        this._lastTime = time;
        if (elapsedSecs === 0) {
            return;
        }
        switch (this.state) {
            case CameraPathAnimation.SCRUBBING:
                return;
            case CameraPathAnimation.PLAYING:
                this._t += this._playingRate * elapsedSecs;
                numFrames = this._cameraPath.frames.length;
                if (numFrames === 0 || (this._playingDir < 0 && this._t <= 0) || (this._playingDir > 0 && this._t >= this._cameraPath.frames[numFrames - 1].t)) {
                    this.state = CameraPathAnimation.SCRUBBING;
                    this._t = this._cameraPath.frames[numFrames - 1].t;
                    this.fire("stopped");
                    return;
                }
                cameraPath.loadFrame(this._t);
                break;
            case CameraPathAnimation.PLAYING_TO:
                t = this._t + (this._playingRate * elapsedSecs * this._playingDir);
                if ((this._playingDir < 0 && t <= this._playingToT) || (this._playingDir > 0 && t >= this._playingToT)) {
                    t = this._playingToT;
                    this.state = CameraPathAnimation.SCRUBBING;
                    this.fire("stopped");
                }
                this._t = t;
                cameraPath.loadFrame(this._t);
                break;
        }
    }

    /*
    * @private
     */
    _ease(t, b, c, d) {
        t /= d;
        return -c * t * (t - 2) + b;
    }

    /**
     * Sets the {@link CameraPath} animated by this CameraPathAnimation.
     *
     @param {CameraPath} value The new CameraPath.
     */
    set cameraPath(value) {
        this._cameraPath = value;
    }

    /**
     * Gets the {@link CameraPath} animated by this CameraPathAnimation.
     *
     @returns {CameraPath} The CameraPath.
     */
    get cameraPath() {
        return this._cameraPath;
    }

    /**
     * Sets the rate at which the CameraPathAnimation animates the {@link Camera} along the {@link CameraPath}.
     *
     *  @param {Number} value The amount of progress per second.
     */
    set rate(value) {
        this._playingRate = value;
    }

    /**
     * Gets the rate at which the CameraPathAnimation animates the {@link Camera} along the {@link CameraPath}.
     *
     * @returns {*|number} The current playing rate.
     */
    get rate() {
        return this._playingRate;
    }

    /**
     * Begins animating the {@link Camera} along CameraPathAnimation's {@link CameraPath} from the beginning.
     */
    play() {
        if (!this._cameraPath) {
            return;
        }
        this._lastTime = null;
        this.state = CameraPathAnimation.PLAYING;
    }

    /**
     * Begins animating the {@link Camera} along CameraPathAnimation's {@link CameraPath} from the given time.
     *
     * @param {Number} t Time instant.
     */
    playToT(t) {
        const cameraPath = this._cameraPath;
        if (!cameraPath) {
            return;
        }
        this._playingFromT = this._t;
        this._playingToT = t;
        this._playingDir = (this._playingToT - this._playingFromT) < 0 ? -1 : 1;
        this._lastTime = null;
        this.state = CameraPathAnimation.PLAYING_TO;
    }

    /**
     * Animates the {@link Camera} along CameraPathAnimation's {@link CameraPath} to the given frame.
     *
     * @param {Number} frameIdx Index of the frame to play to.
     */
    playToFrame(frameIdx) {
        const cameraPath = this._cameraPath;
        if (!cameraPath) {
            return;
        }
        const frame = cameraPath.frames[frameIdx];
        if (!frame) {
            this.error("playToFrame - frame index out of range: " + frameIdx);
            return;
        }
        this.playToT(frame.t);
    }

    /**
     * Flies the {@link Camera} directly to the given frame on the CameraPathAnimation's {@link CameraPath}.
     *
     * @param {Number} frameIdx Index of the frame to play to.
     * @param {Function} [ok] Callback to fire when playing is complete.
     */
    flyToFrame(frameIdx, ok) {
        const cameraPath = this._cameraPath;
        if (!cameraPath) {
            return;
        }
        const frame = cameraPath.frames[frameIdx];
        if (!frame) {
            this.error("flyToFrame - frame index out of range: " + frameIdx);
            return;
        }
        this.state = CameraPathAnimation.SCRUBBING;
        this._cameraFlightAnimation.flyTo(frame, ok);
    }

    /**
     * Scrubs the {@link Camera} to the given time on the CameraPathAnimation's {@link CameraPath}.
     *
     * @param {Number} t Time instant.
     */
    scrubToT(t) {
        const cameraPath = this._cameraPath;
        if (!cameraPath) {
            return;
        }
        const camera = this.scene.camera;
        if (!camera) {
            return;
        }
        this._t = t;
        cameraPath.loadFrame(this._t);
        this.state = CameraPathAnimation.SCRUBBING;
    }

    /**
     * Scrubs the {@link Camera} to the given frame on the CameraPathAnimation's {@link CameraPath}.
     *
     * @param {Number} frameIdx Index of the frame to scrub to.
     */
    scrubToFrame(frameIdx) {
        const cameraPath = this._cameraPath;
        if (!cameraPath) {
            return;
        }
        const camera = this.scene.camera;
        if (!camera) {
            return;
        }
        const frame = cameraPath.frames[frameIdx];
        if (!frame) {
            this.error("playToFrame - frame index out of range: " + frameIdx);
            return;
        }
        cameraPath.loadFrame(this._t);
        this.state = CameraPathAnimation.SCRUBBING;
    }

    /**
     * Stops playing this CameraPathAnimation.
     */
    stop() {
        this.state = CameraPathAnimation.SCRUBBING;
        this.fire("stopped");
    }

    destroy() {
        super.destroy();
        this.scene.off(this._tick);
    }
}

CameraPathAnimation.STOPPED = 0;
CameraPathAnimation.SCRUBBING = 1;
CameraPathAnimation.PLAYING = 2;
CameraPathAnimation.PLAYING_TO = 3;

/**
 * @desc Defines a shape for one or more {@link Mesh}es.
 *
 * * {@link ReadableGeometry} is a subclass that stores its data in both browser and GPU memory. Use ReadableGeometry when you need to keep the geometry arrays in browser memory.
 * * {@link VBOGeometry} is a subclass that stores its data solely in GPU memory. Use VBOGeometry when you need a lower memory footprint and don't need to keep the geometry data in browser memory.
 */
class Geometry extends Component {

    /** @private */
    get type() {
        return "Geometry";
    }

    /** @private */
    get isGeometry() {
        return true;
    }

    constructor(owner, cfg = {}) {
        super(owner, cfg);
        stats.memory.meshes++;
    }

    destroy() {
        super.destroy();
        stats.memory.meshes--;
    }
}

const ids$4 = new Map$1$1({});

/**
 * @desc Represents a chunk of state changes applied by the {@link Scene}'s renderer while it renders a frame.
 *
 * * Contains properties that represent the state changes.
 * * Has a unique automatically-generated numeric ID, which the renderer can use to sort these, in order to avoid applying redundant state changes for each frame.
 * * Initialize your own properties on a RenderState via its constructor.
 *
 * @private
 */
class RenderState {

    constructor(cfg) {

        /**
         The RenderState's ID, unique within the renderer.
         @property id
         @type {Number}
         @final
         */
        this.id = ids$4.addItem({});
        for (const key in cfg) {
            if (cfg.hasOwnProperty(key)) {
                this[key] = cfg[key];
            }
        }
    }

    /**
     Destroys this RenderState.
     */
    destroy() {
        ids$4.removeItem(this.id);
    }
}

/**
 * @desc Represents a WebGL ArrayBuffer.
 *
 * @private
 */
class ArrayBuf {

    constructor(gl, type, data, numItems, itemSize, usage, normalized, stride, offset) {

        this._gl = gl;
        this.type = type;
        this.allocated = false;
        this.ConstructorType = data.constructor;

        switch (data.constructor) {

            case Uint8Array:
                this.itemType = gl.UNSIGNED_BYTE;
                this.itemByteSize = 1;
                break;

            case Int8Array:
                this.itemType = gl.BYTE;
                this.itemByteSize = 1;
                break;

            case  Uint16Array:
                this.itemType = gl.UNSIGNED_SHORT;
                this.itemByteSize = 2;
                break;

            case  Int16Array:
                this.itemType = gl.SHORT;
                this.itemByteSize = 2;
                break;

            case Uint32Array:
                this.itemType = gl.UNSIGNED_INT;
                this.itemByteSize = 4;
                break;

            case Int32Array:
                this.itemType = gl.INT;
                this.itemByteSize = 4;
                break;

            default:
                this.itemType = gl.FLOAT;
                this.itemByteSize = 4;
        }

        this.usage = usage;
        this.length = 0;
        this.dataLength = numItems;
        this.numItems = 0;
        this.itemSize = itemSize;
        this.normalized = !!normalized;
        this.stride = stride || 0;
        this.offset = offset || 0;

        this._allocate(data);
    }

    _allocate(data) {
        this.allocated = false;
        this._handle = this._gl.createBuffer();
        if (!this._handle) {
            throw "Failed to allocate WebGL ArrayBuffer";
        }
        if (this._handle) {
            this._gl.bindBuffer(this.type, this._handle);
            this._gl.bufferData(this.type, data.length > this.dataLength ? data.slice(0, this.dataLength) : data, this.usage);
            this._gl.bindBuffer(this.type, null);
            this.length = data.length;
            this.numItems = this.length / this.itemSize;
            this.allocated = true;
        }
    }

    setData(data, offset) {
        if (!this.allocated) {
            return;
        }
        if (data.length + (offset || 0) > this.length) {            // Needs reallocation
            this.destroy();
            this._allocate(data);
        } else {            // No reallocation needed
            this._gl.bindBuffer(this.type, this._handle);
            if (offset || offset === 0) {
                this._gl.bufferSubData(this.type, offset * this.itemByteSize, data);
            } else {
                this._gl.bufferData(this.type, data, this.usage);
            }
            this._gl.bindBuffer(this.type, null);
        }
    }

    getData(baseIndex = 0, length = (this.numItems) - baseIndex) {
        if (!this.allocated) {
            return [];
        }

        const array = new (this.ConstructorType)(length * this.itemSize);

        this._gl.bindBuffer(this.type, this._handle);
        this._gl.getBufferSubData(
            this.type,
            baseIndex * this.itemByteSize * this.itemSize,
            array,
            0,
            length * this.itemSize
        );

        return array;
    }

    bind() {
        if (!this.allocated) {
            return;
        }
        this._gl.bindBuffer(this.type, this._handle);
    }

    unbind() {
        if (!this.allocated) {
            return;
        }
        this._gl.bindBuffer(this.type, null);
    }

    destroy() {
        if (!this.allocated) {
            return;
        }
        this._gl.deleteBuffer(this._handle);
        this._handle = null;
        this.allocated = false;
    }
}

/**
 * @private
 */
var buildEdgeIndices = (function () {

    const uniquePositions = [];
    const indicesLookup = [];
    const indicesReverseLookup = [];
    const weldedIndices = [];

// TODO: Optimize with caching, but need to cater to both compressed and uncompressed positions

    const faces = [];
    let numFaces = 0;
    const compa = new Uint16Array(3);
    const compb = new Uint16Array(3);
    const compc = new Uint16Array(3);
    const a = math.vec3();
    const b = math.vec3();
    const c = math.vec3();
    const cb = math.vec3();
    const ab = math.vec3();
    const cross = math.vec3();
    const normal = math.vec3();

    function weldVertices(positions, indices) {
        const positionsMap = {}; // Hashmap for looking up vertices by position coordinates (and making sure they are unique)
        let vx;
        let vy;
        let vz;
        let key;
        const precisionPoints = 4; // number of decimal points, e.g. 4 for epsilon of 0.0001
        const precision = Math.pow(10, precisionPoints);
        let i;
        let len;
        let lenUniquePositions = 0;
        for (i = 0, len = positions.length; i < len; i += 3) {
            vx = positions[i];
            vy = positions[i + 1];
            vz = positions[i + 2];
            key = Math.round(vx * precision) + '_' + Math.round(vy * precision) + '_' + Math.round(vz * precision);
            if (positionsMap[key] === undefined) {
                positionsMap[key] = lenUniquePositions / 3;
                uniquePositions[lenUniquePositions++] = vx;
                uniquePositions[lenUniquePositions++] = vy;
                uniquePositions[lenUniquePositions++] = vz;
            }
            indicesLookup[i / 3] = positionsMap[key];
        }
        for (i = 0, len = indices.length; i < len; i++) {
            weldedIndices[i] = indicesLookup[indices[i]];
            indicesReverseLookup[weldedIndices[i]] = indices[i];
        }
    }

    function buildFaces(numIndices, positionsDecodeMatrix) {
        numFaces = 0;
        for (let i = 0, len = numIndices; i < len; i += 3) {
            const ia = ((weldedIndices[i]) * 3);
            const ib = ((weldedIndices[i + 1]) * 3);
            const ic = ((weldedIndices[i + 2]) * 3);
            if (positionsDecodeMatrix) {
                compa[0] = uniquePositions[ia];
                compa[1] = uniquePositions[ia + 1];
                compa[2] = uniquePositions[ia + 2];
                compb[0] = uniquePositions[ib];
                compb[1] = uniquePositions[ib + 1];
                compb[2] = uniquePositions[ib + 2];
                compc[0] = uniquePositions[ic];
                compc[1] = uniquePositions[ic + 1];
                compc[2] = uniquePositions[ic + 2];
                // Decode
                math.decompressPosition(compa, positionsDecodeMatrix, a);
                math.decompressPosition(compb, positionsDecodeMatrix, b);
                math.decompressPosition(compc, positionsDecodeMatrix, c);
            } else {
                a[0] = uniquePositions[ia];
                a[1] = uniquePositions[ia + 1];
                a[2] = uniquePositions[ia + 2];
                b[0] = uniquePositions[ib];
                b[1] = uniquePositions[ib + 1];
                b[2] = uniquePositions[ib + 2];
                c[0] = uniquePositions[ic];
                c[1] = uniquePositions[ic + 1];
                c[2] = uniquePositions[ic + 2];
            }
            math.subVec3(c, b, cb);
            math.subVec3(a, b, ab);
            math.cross3Vec3(cb, ab, cross);
            math.normalizeVec3(cross, normal);
            const face = faces[numFaces] || (faces[numFaces] = {normal: math.vec3()});
            face.normal[0] = normal[0];
            face.normal[1] = normal[1];
            face.normal[2] = normal[2];
            numFaces++;
        }
    }

    return function (positions, indices, positionsDecodeMatrix, edgeThreshold) {
        weldVertices(positions, indices);
        buildFaces(indices.length, positionsDecodeMatrix);
        const edgeIndices = [];
        const thresholdDot = Math.cos(math.DEGTORAD * edgeThreshold);
        const edges = {};
        let edge1;
        let edge2;
        let index1;
        let index2;
        let key;
        let largeIndex = false;
        let edge;
        let normal1;
        let normal2;
        let dot;
        let ia;
        let ib;
        for (let i = 0, len = indices.length; i < len; i += 3) {
            const faceIndex = i / 3;
            for (let j = 0; j < 3; j++) {
                edge1 = weldedIndices[i + j];
                edge2 = weldedIndices[i + ((j + 1) % 3)];
                index1 = Math.min(edge1, edge2);
                index2 = Math.max(edge1, edge2);
                key = index1 + "," + index2;
                if (edges[key] === undefined) {
                    edges[key] = {
                        index1: index1,
                        index2: index2,
                        face1: faceIndex,
                        face2: undefined
                    };
                } else {
                    edges[key].face2 = faceIndex;
                }
            }
        }
        for (key in edges) {
            edge = edges[key];
            // an edge is only rendered if the angle (in degrees) between the face normals of the adjoining faces exceeds this value. default = 1 degree.
            if (edge.face2 !== undefined) {
                normal1 = faces[edge.face1].normal;
                normal2 = faces[edge.face2].normal;
                dot = math.dotVec3(normal1, normal2);
                if (dot > thresholdDot) {
                    continue;
                }
            }
            ia = indicesReverseLookup[edge.index1];
            ib = indicesReverseLookup[edge.index2];
            if (!largeIndex && ia > 65535 || ib > 65535) {
                largeIndex = true;
            }
            edgeIndices.push(ia);
            edgeIndices.push(ib);
        }
        return (largeIndex) ? new Uint32Array(edgeIndices) : new Uint16Array(edgeIndices);
    };
})();

/**
 * Private geometry compression and decompression utilities.
 */

/**
 * @private
 * @param array
 * @returns {{min: Float32Array, max: Float32Array}}
 */
function getPositionsBounds(array) {
    const min = new Float32Array(3);
    const max = new Float32Array(3);
    let i, j;
    for (i = 0; i < 3; i++) {
        min[i] = Number.MAX_VALUE;
        max[i] = -Number.MAX_VALUE;
    }
    for (i = 0; i < array.length; i += 3) {
        for (j = 0; j < 3; j++) {
            min[j] = Math.min(min[j], array[i + j]);
            max[j] = Math.max(max[j], array[i + j]);
        }
    }
    return {
        min: min,
        max: max
    };
}

const createPositionsDecodeMatrix$1 = (function () {
    const translate = math.mat4();
    const scale = math.mat4();
    return function (aabb, positionsDecodeMatrix) {
        positionsDecodeMatrix = positionsDecodeMatrix || math.mat4();
        const xmin = aabb[0];
        const ymin = aabb[1];
        const zmin = aabb[2];
        const xwid = aabb[3] - xmin;
        const ywid = aabb[4] - ymin;
        const zwid = aabb[5] - zmin;
        const maxInt = 65535;
        math.identityMat4(translate);
        math.translationMat4v(aabb, translate);
        math.identityMat4(scale);
        math.scalingMat4v([xwid / maxInt, ywid / maxInt, zwid / maxInt], scale);
        math.mulMat4(translate, scale, positionsDecodeMatrix);
        return positionsDecodeMatrix;
    };
})();

/**
 * @private
 */
var compressPositions = (function () { // http://cg.postech.ac.kr/research/mesh_comp_mobile/mesh_comp_mobile_conference.pdf
    const translate = math.mat4();
    const scale = math.mat4();
    return function (array, min, max) {
        const quantized = new Uint16Array(array.length);
        const multiplier = new Float32Array([
            max[0] !== min[0] ? 65535 / (max[0] - min[0]) : 0,
            max[1] !== min[1] ? 65535 / (max[1] - min[1]) : 0,
            max[2] !== min[2] ? 65535 / (max[2] - min[2]) : 0
        ]);
        let i;
        for (i = 0; i < array.length; i += 3) {
            quantized[i + 0] = Math.max(0, Math.min(65535, Math.floor((array[i + 0] - min[0]) * multiplier[0])));
            quantized[i + 1] = Math.max(0, Math.min(65535, Math.floor((array[i + 1] - min[1]) * multiplier[1])));
            quantized[i + 2] = Math.max(0, Math.min(65535, Math.floor((array[i + 2] - min[2]) * multiplier[2])));
        }
        math.identityMat4(translate);
        math.translationMat4v(min, translate);
        math.identityMat4(scale);
        math.scalingMat4v([
            (max[0] - min[0]) / 65535,
            (max[1] - min[1]) / 65535,
            (max[2] - min[2]) / 65535
        ], scale);
        const decodeMat = math.mulMat4(translate, scale, math.identityMat4());
        return {
            quantized: quantized,
            decodeMatrix: decodeMat
        };
    };
})();

function compressPosition(p, aabb, q) {
    const multiplier = new Float32Array([
        aabb[3] !== aabb[0] ? 65535 / (aabb[3] - aabb[0]) : 0,
        aabb[4] !== aabb[1] ? 65535 / (aabb[4] - aabb[1]) : 0,
        aabb[5] !== aabb[2] ? 65535 / (aabb[5] - aabb[2]) : 0
    ]);
    q[0] = Math.max(0, Math.min(65535, Math.floor((p[0] - aabb[0]) * multiplier[0])));
    q[1] = Math.max(0, Math.min(65535, Math.floor((p[1] - aabb[1]) * multiplier[1])));
    q[2] = Math.max(0, Math.min(65535, Math.floor((p[2] - aabb[2]) * multiplier[2])));
}

function decompressPosition(position, decodeMatrix, dest) {
    dest[0] = position[0] * decodeMatrix[0] + decodeMatrix[12];
    dest[1] = position[1] * decodeMatrix[5] + decodeMatrix[13];
    dest[2] = position[2] * decodeMatrix[10] + decodeMatrix[14];
    return dest;
}

function decompressAABB(aabb, decodeMatrix, dest = aabb) {
    dest[0] = aabb[0] * decodeMatrix[0] + decodeMatrix[12];
    dest[1] = aabb[1] * decodeMatrix[5] + decodeMatrix[13];
    dest[2] = aabb[2] * decodeMatrix[10] + decodeMatrix[14];
    dest[3] = aabb[3] * decodeMatrix[0] + decodeMatrix[12];
    dest[4] = aabb[4] * decodeMatrix[5] + decodeMatrix[13];
    dest[5] = aabb[5] * decodeMatrix[10] + decodeMatrix[14];
    return dest;
}

/**
 * @private
 */
function decompressPositions(positions, decodeMatrix, dest = new Float32Array(positions.length)) {
    for (let i = 0, len = positions.length; i < len; i += 3) {
        dest[i + 0] = positions[i + 0] * decodeMatrix[0] + decodeMatrix[12];
        dest[i + 1] = positions[i + 1] * decodeMatrix[5] + decodeMatrix[13];
        dest[i + 2] = positions[i + 2] * decodeMatrix[10] + decodeMatrix[14];
    }
    return dest;
}

//--------------- UVs --------------------------------------------------------------------------------------------------

/**
 * @private
 * @param array
 * @returns {{min: Float32Array, max: Float32Array}}
 */
function getUVBounds(array) {
    const min = new Float32Array(2);
    const max = new Float32Array(2);
    let i, j;
    for (i = 0; i < 2; i++) {
        min[i] = Number.MAX_VALUE;
        max[i] = -Number.MAX_VALUE;
    }
    for (i = 0; i < array.length; i += 2) {
        for (j = 0; j < 2; j++) {
            min[j] = Math.min(min[j], array[i + j]);
            max[j] = Math.max(max[j], array[i + j]);
        }
    }
    return {
        min: min,
        max: max
    };
}

/**
 * @private
 */
var compressUVs = (function () {
    const translate = math.mat3();
    const scale = math.mat3();
    return function (array, min, max) {
        const quantized = new Uint16Array(array.length);
        const multiplier = new Float32Array([
            65535 / (max[0] - min[0]),
            65535 / (max[1] - min[1])
        ]);
        let i;
        for (i = 0; i < array.length; i += 2) {
            quantized[i + 0] = Math.max(0, Math.min(65535, Math.floor((array[i + 0] - min[0]) * multiplier[0])));
            quantized[i + 1] = Math.max(0, Math.min(65535, Math.floor((array[i + 1] - min[1]) * multiplier[1])));
        }
        math.identityMat3(translate);
        math.translationMat3v(min, translate);
        math.identityMat3(scale);
        math.scalingMat3v([
            (max[0] - min[0]) / 65535,
            (max[1] - min[1]) / 65535
        ], scale);
        const decodeMat = math.mulMat3(translate, scale, math.identityMat3());
        return {
            quantized: quantized,
            decodeMatrix: decodeMat
        };
    };
})();


//--------------- Normals ----------------------------------------------------------------------------------------------

/**
 * @private
 */
function compressNormals(array) { // http://jcgt.org/published/0003/02/01/

    // Note: three elements for each encoded normal, in which the last element in each triplet is redundant.
    // This is to work around a mysterious WebGL issue where 2-element normals just wouldn't work in the shader :/

    const encoded = new Int8Array(array.length);
    let oct, dec, best, currentCos, bestCos;
    for (let i = 0; i < array.length; i += 3) {
        // Test various combinations of ceil and floor
        // to minimize rounding errors
        best = oct = octEncodeVec3$1(array, i, "floor", "floor");
        dec = octDecodeVec2$1(oct);
        currentCos = bestCos = dot$1(array, i, dec);
        oct = octEncodeVec3$1(array, i, "ceil", "floor");
        dec = octDecodeVec2$1(oct);
        currentCos = dot$1(array, i, dec);
        if (currentCos > bestCos) {
            best = oct;
            bestCos = currentCos;
        }
        oct = octEncodeVec3$1(array, i, "floor", "ceil");
        dec = octDecodeVec2$1(oct);
        currentCos = dot$1(array, i, dec);
        if (currentCos > bestCos) {
            best = oct;
            bestCos = currentCos;
        }
        oct = octEncodeVec3$1(array, i, "ceil", "ceil");
        dec = octDecodeVec2$1(oct);
        currentCos = dot$1(array, i, dec);
        if (currentCos > bestCos) {
            best = oct;
            bestCos = currentCos;
        }
        encoded[i] = best[0];
        encoded[i + 1] = best[1];
    }
    return encoded;
}

/**
 * @private
 */
function octEncodeVec3$1(array, i, xfunc, yfunc) { // Oct-encode single normal vector in 2 bytes
    let x = array[i] / (Math.abs(array[i]) + Math.abs(array[i + 1]) + Math.abs(array[i + 2]));
    let y = array[i + 1] / (Math.abs(array[i]) + Math.abs(array[i + 1]) + Math.abs(array[i + 2]));
    if (array[i + 2] < 0) {
        let tempx = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1);
        let tempy = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1);
        x = tempx;
        y = tempy;
    }
    return new Int8Array([
        Math[xfunc](x * 127.5 + (x < 0 ? -1 : 0)),
        Math[yfunc](y * 127.5 + (y < 0 ? -1 : 0))
    ]);
}

/**
 * Decode an oct-encoded normal
 */
function octDecodeVec2$1(oct) {
    let x = oct[0];
    let y = oct[1];
    x /= x < 0 ? 127 : 128;
    y /= y < 0 ? 127 : 128;
    const z = 1 - Math.abs(x) - Math.abs(y);
    if (z < 0) {
        x = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1);
        y = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1);
    }
    const length = Math.sqrt(x * x + y * y + z * z);
    return [
        x / length,
        y / length,
        z / length
    ];
}

/**
 * Dot product of a normal in an array against a candidate decoding
 * @private
 */
function dot$1(array, i, vec3) {
    return array[i] * vec3[0] + array[i + 1] * vec3[1] + array[i + 2] * vec3[2];
}

/**
 * @private
 */
function decompressUV(uv, decodeMatrix, dest) {
    dest[0] = uv[0] * decodeMatrix[0] + decodeMatrix[6];
    dest[1] = uv[1] * decodeMatrix[4] + decodeMatrix[7];
}

/**
 * @private
 */
function decompressUVs(uvs, decodeMatrix, dest = new Float32Array(uvs.length)) {
    for (let i = 0, len = uvs.length; i < len; i += 3) {
        dest[i + 0] = uvs[i + 0] * decodeMatrix[0] + decodeMatrix[6];
        dest[i + 1] = uvs[i + 1] * decodeMatrix[4] + decodeMatrix[7];
    }
    return dest;
}

/**
 * @private
 */
function decompressNormal(oct, result) {
    let x = oct[0];
    let y = oct[1];
    x = (2 * x + 1) / 255;
    y = (2 * y + 1) / 255;
    const z = 1 - Math.abs(x) - Math.abs(y);
    if (z < 0) {
        x = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1);
        y = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1);
    }
    const length = Math.sqrt(x * x + y * y + z * z);
    result[0] = x / length;
    result[1] = y / length;
    result[2] = z / length;
    return result;
}

/**
 * @private
 */
function decompressNormals(octs, result) {
    for (let i = 0, j = 0, len = octs.length; i < len; i += 2) {
        let x = octs[i + 0];
        let y = octs[i + 1];
        x = (2 * x + 1) / 255;
        y = (2 * y + 1) / 255;
        const z = 1 - Math.abs(x) - Math.abs(y);
        if (z < 0) {
            x = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1);
            y = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1);
        }
        const length = Math.sqrt(x * x + y * y + z * z);
        result[j + 0] = x / length;
        result[j + 1] = y / length;
        result[j + 2] = z / length;
        j += 3;
    }
    return result;
}

/**
 * @private
 */
const geometryCompressionUtils = {

    getPositionsBounds: getPositionsBounds,
    createPositionsDecodeMatrix: createPositionsDecodeMatrix$1,
    compressPositions: compressPositions,
    compressPosition:compressPosition,
    decompressPositions: decompressPositions,
    decompressPosition: decompressPosition,
    decompressAABB: decompressAABB,

    getUVBounds: getUVBounds,
    compressUVs: compressUVs,
    decompressUVs: decompressUVs,
    decompressUV: decompressUV,

    compressNormals: compressNormals,
    decompressNormals: decompressNormals,
    decompressNormal: decompressNormal
};

const memoryStats$1 = stats.memory;
const tempAABB$3 = math.AABB3();

/**
 * @desc A {@link Geometry} that keeps its geometry data in both browser and GPU memory.
 *
 * ReadableGeometry uses more memory than {@link VBOGeometry}, which only stores its geometry data in GPU memory.
 *
 * ## Usage
 *
 * Creating a {@link Mesh} with a ReadableGeometry that defines a single triangle, plus a {@link PhongMaterial} with diffuse {@link Texture}:
 *
 * [[Run this example](/examples/index.html#geometry_ReadableGeometry)]
 *
 * ````javascript
 * import {Viewer, Mesh, ReadableGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *         canvasId: "myCanvas"
 *     });
 *
 * const myMesh = new Mesh(viewer.scene, {
 *         geometry: new ReadableGeometry(viewer.scene, {
 *             primitive: "triangles",
 *             positions: [0.0, 3, 0.0, -3, -3, 0.0, 3, -3, 0.0],
 *             normals: [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
 *             uv: [0.0, 0.0, 0.5, 1.0, 1.0, 0.0],
 *             indices: [0, 1, 2]
 *         }),
 *         material: new PhongMaterial(viewer.scene, {
 *             diffuseMap: new Texture(viewer.scene, {
 *                 src: "textures/diffuse/uvGrid2.jpg"
 *             }),
 *             backfaces: true
 *         })
 *     });
 *
 * // Get geometry data from browser memory:
 *
 * const positions = myMesh.geometry.positions; // Flat arrays
 * const normals = myMesh.geometry.normals;
 * const uv = myMesh.geometry.uv;
 * const indices = myMesh.geometry.indices;
 *
 * ````
 */
class ReadableGeometry extends Geometry {

    /**
     @private
     */
    get type() {
        return "ReadableGeometry";
    }

    /**
     * @private
     * @returns {Boolean}
     */
    get isReadableGeometry() {
        return true;
    }

    /**
     *
     @class ReadableGeometry
     @module xeokit
     @submodule geometry
     @constructor
     @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well.
     @param {*} [cfg] Configs
     @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene},
     generated automatically when omitted.
     @param {String:Object} [cfg.meta] Optional map of user-defined metadata to attach to this Geometry.
     @param [cfg.primitive="triangles"] {String} The primitive type. Accepted values are 'points', 'lines', 'line-loop', 'line-strip', 'triangles', 'triangle-strip' and 'triangle-fan'.
     @param [cfg.positions] {Number[]} Positions array.
     @param [cfg.normals] {Number[]} Vertex normal vectors array.
     @param [cfg.uv] {Number[]} UVs array.
     @param [cfg.colors] {Number[]} Vertex colors.
     @param [cfg.indices] {Number[]} Indices array.
     @param [cfg.autoVertexNormals=false] {Boolean} Set true to automatically generate normal vectors from the positions and
     indices, if those are supplied.
     @param [cfg.compressGeometry=false] {Boolean} Stores positions, colors, normals and UVs in compressGeometry and oct-encoded formats
     for reduced memory footprint and GPU bus usage.
     @param [cfg.edgeThreshold=10] {Number} When a {@link Mesh} renders this Geometry as wireframe,
     this indicates the threshold angle (in degrees) between the face normals of adjacent triangles below which the edge is discarded.
     @extends Component
     * @param owner
     * @param cfg
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._state = new RenderState({ // Arrays for emphasis effects are got from xeokit.Geometry friend methods
            compressGeometry: !!cfg.compressGeometry,
            primitive: null, // WebGL enum
            primitiveName: null, // String
            positions: null,    // Uint16Array when compressGeometry == true, else Float32Array
            normals: null,      // Uint8Array when compressGeometry == true, else Float32Array
            colors: null,
            uv: null,           // Uint8Array when compressGeometry == true, else Float32Array
            indices: null,
            positionsDecodeMatrix: null, // Set when compressGeometry == true
            uvDecodeMatrix: null, // Set when compressGeometry == true
            positionsBuf: null,
            normalsBuf: null,
            colorsbuf: null,
            uvBuf: null,
            indicesBuf: null,
            hash: ""
        });

        this._numTriangles = 0;

        this._edgeThreshold = cfg.edgeThreshold || 10.0;

        // Lazy-generated VBOs

        this._edgeIndicesBuf = null;
        this._pickTrianglePositionsBuf = null;
        this._pickTriangleColorsBuf = null;

        // Local-space Boundary3D

        this._aabbDirty = true;

        this._boundingSphere = true;
        this._aabb = null;
        this._aabbDirty = true;

        this._obb = null;
        this._obbDirty = true;

        const state = this._state;
        const gl = this.scene.canvas.gl;

        // Primitive type

        cfg.primitive = cfg.primitive || "triangles";
        switch (cfg.primitive) {
            case "points":
                state.primitive = gl.POINTS;
                state.primitiveName = cfg.primitive;
                break;
            case "lines":
                state.primitive = gl.LINES;
                state.primitiveName = cfg.primitive;
                break;
            case "line-loop":
                state.primitive = gl.LINE_LOOP;
                state.primitiveName = cfg.primitive;
                break;
            case "line-strip":
                state.primitive = gl.LINE_STRIP;
                state.primitiveName = cfg.primitive;
                break;
            case "triangles":
                state.primitive = gl.TRIANGLES;
                state.primitiveName = cfg.primitive;
                break;
            case "triangle-strip":
                state.primitive = gl.TRIANGLE_STRIP;
                state.primitiveName = cfg.primitive;
                break;
            case "triangle-fan":
                state.primitive = gl.TRIANGLE_FAN;
                state.primitiveName = cfg.primitive;
                break;
            default:
                this.error("Unsupported value for 'primitive': '" + cfg.primitive +
                    "' - supported values are 'points', 'lines', 'line-loop', 'line-strip', 'triangles', " +
                    "'triangle-strip' and 'triangle-fan'. Defaulting to 'triangles'.");
                state.primitive = gl.TRIANGLES;
                state.primitiveName = cfg.primitive;
        }

        if (cfg.positions) {
            if (this._state.compressGeometry) {
                const bounds = geometryCompressionUtils.getPositionsBounds(cfg.positions);
                const result = geometryCompressionUtils.compressPositions(cfg.positions, bounds.min, bounds.max);
                state.positions = result.quantized;
                state.positionsDecodeMatrix = result.decodeMatrix;
            } else {
                state.positions = cfg.positions.constructor === Float32Array ? cfg.positions : new Float32Array(cfg.positions);
            }
        }
        if (cfg.colors) {
            state.colors = cfg.colors.constructor === Float32Array ? cfg.colors : new Float32Array(cfg.colors);
        }
        if (cfg.uv) {
            if (this._state.compressGeometry) {
                const bounds = geometryCompressionUtils.getUVBounds(cfg.uv);
                const result = geometryCompressionUtils.compressUVs(cfg.uv, bounds.min, bounds.max);
                state.uv = result.quantized;
                state.uvDecodeMatrix = result.decodeMatrix;
            } else {
                state.uv = cfg.uv.constructor === Float32Array ? cfg.uv : new Float32Array(cfg.uv);
            }
        }
        if (cfg.normals) {
            if (this._state.compressGeometry) {
                state.normals = geometryCompressionUtils.compressNormals(cfg.normals);
            } else {
                state.normals = cfg.normals.constructor === Float32Array ? cfg.normals : new Float32Array(cfg.normals);
            }
        }
        if (cfg.indices) {
            state.indices = (cfg.indices.constructor === Uint32Array || cfg.indices.constructor === Uint16Array) ? cfg.indices : new Uint32Array(cfg.indices);
            if (this._state.primitiveName === "triangles") {
                this._numTriangles = (cfg.indices.length / 3);
            }
        }

        this._buildHash();

        memoryStats$1.meshes++;

        this._buildVBOs();
    }

    _buildVBOs() {
        const state = this._state;
        const gl = this.scene.canvas.gl;
        if (state.indices) {
            state.indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, state.indices, state.indices.length, 1, gl.STATIC_DRAW);
            memoryStats$1.indices += state.indicesBuf.numItems;
        }
        if (state.positions) {
            state.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, state.positions, state.positions.length, 3, gl.STATIC_DRAW);
            memoryStats$1.positions += state.positionsBuf.numItems;
        }
        if (state.normals) {
            let normalized = state.compressGeometry;
            state.normalsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, state.normals, state.normals.length, 3, gl.STATIC_DRAW, normalized);
            memoryStats$1.normals += state.normalsBuf.numItems;
        }
        if (state.colors) {
            state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, state.colors, state.colors.length, 4, gl.STATIC_DRAW);
            memoryStats$1.colors += state.colorsBuf.numItems;
        }
        if (state.uv) {
            state.uvBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, state.uv, state.uv.length, 2, gl.STATIC_DRAW);
            memoryStats$1.uvs += state.uvBuf.numItems;
        }
    }

    _buildHash() {
        const state = this._state;
        const hash = ["/g"];
        hash.push("/" + state.primitive + ";");
        if (state.positions) {
            hash.push("p");
        }
        if (state.colors) {
            hash.push("c");
        }
        if (state.normals || state.autoVertexNormals) {
            hash.push("n");
        }
        if (state.uv) {
            hash.push("u");
        }
        if (state.compressGeometry) {
            hash.push("cp");
        }
        hash.push(";");
        state.hash = hash.join("");
    }

    _getEdgeIndices() {
        if (!this._edgeIndicesBuf) {
            this._buildEdgeIndices();
        }
        return this._edgeIndicesBuf;
    }

    _getPickTrianglePositions() {
        if (!this._pickTrianglePositionsBuf) {
            this._buildPickTriangleVBOs();
        }
        return this._pickTrianglePositionsBuf;
    }

    _getPickTriangleColors() {
        if (!this._pickTriangleColorsBuf) {
            this._buildPickTriangleVBOs();
        }
        return this._pickTriangleColorsBuf;
    }

    _buildEdgeIndices() { // FIXME: Does not adjust indices after other objects are deleted from vertex buffer!!
        const state = this._state;
        if (!state.positions || !state.indices) {
            return;
        }
        const gl = this.scene.canvas.gl;
        const edgeIndices = buildEdgeIndices(state.positions, state.indices, state.positionsDecodeMatrix, this._edgeThreshold);
        this._edgeIndicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, edgeIndices, edgeIndices.length, 1, gl.STATIC_DRAW);
        memoryStats$1.indices += this._edgeIndicesBuf.numItems;
    }

    _buildPickTriangleVBOs() { // Builds positions and indices arrays that allow each triangle to have a unique color
        const state = this._state;
        if (!state.positions || !state.indices) {
            return;
        }
        const gl = this.scene.canvas.gl;
        const arrays = math.buildPickTriangles(state.positions, state.indices, state.compressGeometry);
        const positions = arrays.positions;
        const colors = arrays.colors;
        this._pickTrianglePositionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, positions, positions.length, 3, gl.STATIC_DRAW);
        this._pickTriangleColorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, colors, colors.length, 4, gl.STATIC_DRAW, true);
        memoryStats$1.positions += this._pickTrianglePositionsBuf.numItems;
        memoryStats$1.colors += this._pickTriangleColorsBuf.numItems;
    }

    _buildPickVertexVBOs() {
        // var state = this._state;
        // if (!state.positions || !state.indices) {
        //     return;
        // }
        // var gl = this.scene.canvas.gl;
        // var arrays = math.buildPickVertices(state.positions, state.indices, state.compressGeometry);
        // var pickVertexPositions = arrays.positions;
        // var pickColors = arrays.colors;
        // this._pickVertexPositionsBuf = new xeokit.renderer.ArrayBuf(gl, gl.ARRAY_BUFFER, pickVertexPositions, pickVertexPositions.length, 3, gl.STATIC_DRAW);
        // this._pickVertexColorsBuf = new xeokit.renderer.ArrayBuf(gl, gl.ARRAY_BUFFER, pickColors, pickColors.length, 4, gl.STATIC_DRAW, true);
        // memoryStats.positions += this._pickVertexPositionsBuf.numItems;
        // memoryStats.colors += this._pickVertexColorsBuf.numItems;
    }

    _webglContextLost() {
        if (this._sceneVertexBufs) {
            this._sceneVertexBufs.webglContextLost();
        }
    }

    _webglContextRestored() {
        if (this._sceneVertexBufs) {
            this._sceneVertexBufs.webglContextRestored();
        }
        this._buildVBOs();
        this._edgeIndicesBuf = null;
        this._pickVertexPositionsBuf = null;
        this._pickTrianglePositionsBuf = null;
        this._pickTriangleColorsBuf = null;
        this._pickVertexPositionsBuf = null;
        this._pickVertexColorsBuf = null;
    }

    /**
     * Gets the Geometry's primitive type.

     Valid types are: 'points', 'lines', 'line-loop', 'line-strip', 'triangles', 'triangle-strip' and 'triangle-fan'.

     @property primitive
     @default "triangles"
     @type {String}
     */
    get primitive() {
        return this._state.primitiveName;
    }

    /**
     Indicates if this Geometry is quantized.

     Compression is an internally-performed optimization which stores positions, colors, normals and UVs
     in quantized and oct-encoded formats for reduced memory footprint and GPU bus usage.

     Quantized geometry may not be updated.

     @property compressGeometry
     @default false
     @type {Boolean}
     @final
     */
    get compressGeometry() {
        return this._state.compressGeometry;
    }

    /**
     The Geometry's vertex positions.

     @property positions
     @default null
     @type {Number[]}
     */
    get positions() {
        if (!this._state.positions) {
            return null;
        }
        if (!this._state.compressGeometry) {
            return this._state.positions;
        }
        if (!this._decompressedPositions) {
            this._decompressedPositions = new Float32Array(this._state.positions.length);
            geometryCompressionUtils.decompressPositions(this._state.positions, this._state.positionsDecodeMatrix, this._decompressedPositions);
        }
        return this._decompressedPositions;
    }

    set positions(newPositions) {
        const state = this._state;
        const positions = state.positions;
        if (!positions) {
            this.error("can't update geometry positions - geometry has no positions");
            return;
        }
        if (positions.length !== newPositions.length) {
            this.error("can't update geometry positions - new positions are wrong length");
            return;
        }
        if (this._state.compressGeometry) {
            const bounds = geometryCompressionUtils.getPositionsBounds(newPositions);
            const result = geometryCompressionUtils.compressPositions(newPositions, bounds.min, bounds.max);
            newPositions = result.quantized; // TODO: Copy in-place
            state.positionsDecodeMatrix = result.decodeMatrix;
        }
        positions.set(newPositions);
        if (state.positionsBuf) {
            state.positionsBuf.setData(positions);
        }
        this._setAABBDirty();
        this.glRedraw();
    }

    /**
     The Geometry's vertex normals.

     @property normals
     @default null
     @type {Number[]}
     */
    get normals() {
        if (!this._state.normals) {
            return;
        }
        if (!this._state.compressGeometry) {
            return this._state.normals;
        }
        if (!this._decompressedNormals) {
            const lenCompressed = this._state.normals.length;
            const lenDecompressed = lenCompressed + (lenCompressed / 2); // 2 -> 3
            this._decompressedNormals = new Float32Array(lenDecompressed);
            geometryCompressionUtils.decompressNormals(this._state.normals, this._decompressedNormals);
        }
        return this._decompressedNormals;
    }

    set normals(newNormals) {
        if (this._state.compressGeometry) {
            this.error("can't update geometry normals - quantized geometry is immutable"); // But will be eventually
            return;
        }
        const state = this._state;
        const normals = state.normals;
        if (!normals) {
            this.error("can't update geometry normals - geometry has no normals");
            return;
        }
        if (normals.length !== newNormals.length) {
            this.error("can't update geometry normals - new normals are wrong length");
            return;
        }
        normals.set(newNormals);
        if (state.normalsBuf) {
            state.normalsBuf.setData(normals);
        }
        this.glRedraw();
    }


    /**
     The Geometry's UV coordinates.

     @property uv
     @default null
     @type {Number[]}
     */
    get uv() {
        if (!this._state.uv) {
            return null;
        }
        if (!this._state.compressGeometry) {
            return this._state.uv;
        }
        if (!this._decompressedUV) {
            this._decompressedUV = new Float32Array(this._state.uv.length);
            geometryCompressionUtils.decompressUVs(this._state.uv, this._state.uvDecodeMatrix, this._decompressedUV);
        }
        return this._decompressedUV;
    }

    set uv(newUV) {
        if (this._state.compressGeometry) {
            this.error("can't update geometry UVs - quantized geometry is immutable"); // But will be eventually
            return;
        }
        const state = this._state;
        const uv = state.uv;
        if (!uv) {
            this.error("can't update geometry UVs - geometry has no UVs");
            return;
        }
        if (uv.length !== newUV.length) {
            this.error("can't update geometry UVs - new UVs are wrong length");
            return;
        }
        uv.set(newUV);
        if (state.uvBuf) {
            state.uvBuf.setData(uv);
        }
        this.glRedraw();
    }

    /**
     The Geometry's vertex colors.

     @property colors
     @default null
     @type {Number[]}
     */
    get colors() {
        return this._state.colors;
    }

    set colors(newColors) {
        if (this._state.compressGeometry) {
            this.error("can't update geometry colors - quantized geometry is immutable"); // But will be eventually
            return;
        }
        const state = this._state;
        const colors = state.colors;
        if (!colors) {
            this.error("can't update geometry colors - geometry has no colors");
            return;
        }
        if (colors.length !== newColors.length) {
            this.error("can't update geometry colors - new colors are wrong length");
            return;
        }
        colors.set(newColors);
        if (state.colorsBuf) {
            state.colorsBuf.setData(colors);
        }
        this.glRedraw();
    }

    /**
     The Geometry's indices.

     If ````xeokit.WEBGL_INFO.SUPPORTED_EXTENSIONS["OES_element_index_uint"]```` is true, then this can be
     a ````Uint32Array````, otherwise it needs to be a ````Uint16Array````.

     @property indices
     @default null
     @type Uint16Array | Uint32Array
     @final
     */
    get indices() {
        return this._state.indices;
    }

    /**
     * Local-space axis-aligned 3D boundary (AABB) of this geometry.
     *
     * The AABB is represented by a six-element Float64Array containing the min/max extents of the
     * axis-aligned volume, ie. ````[xmin, ymin,zmin,xmax,ymax, zmax]````.
     *
     * @property aabb
     * @final
     * @type {Number[]}
     */
    get aabb() {
        if (this._aabbDirty) {
            if (!this._aabb) {
                this._aabb = math.AABB3();
            }
            math.positions3ToAABB3(this._state.positions, this._aabb, this._state.positionsDecodeMatrix);
            this._aabbDirty = false;
        }
        return this._aabb;
    }

    /**
     * Local-space oriented 3D boundary (OBB) of this geometry.
     *
     * The OBB is represented by a 32-element Float64Array containing the eight vertices of the box,
     * where each vertex is a homogeneous coordinate having [x,y,z,w] elements.
     *
     * @property obb
     * @final
     * @type {Number[]}
     */
    get obb() {
        if (this._obbDirty) {
            if (!this._obb) {
                this._obb = math.OBB3();
            }
            math.positions3ToAABB3(this._state.positions, tempAABB$3, this._state.positionsDecodeMatrix);
            math.AABB3ToOBB3(tempAABB$3, this._obb);
            this._obbDirty = false;
        }
        return this._obb;
    }

    _getMetrics() {
        if (! ("_metrics" in this)) {
            switch (this._state.primitiveName) {
            case "solid":
            case "surface":
            case "triangles": {
                const indices = this._state.indices;
                const positions = this._state.positions;
                const getPos = (i, out) => {
                    const idx = indices[i] * 3;
                    for (let j = 0; j < 3; ++j) {
                        out[j] = positions[idx + j];
                    }                    return out;
                };
                const tmp = [ math.vec3(), math.vec3(), math.vec3(), math.vec3() ];
                let totalArea = 0;
                const centroid = math.vec3([ 0, 0, 0 ]);
                for (let i = 0; i < indices.length; i += 3) {
                    const v0 = getPos(i,   tmp[0]);
                    const v1 = getPos(i+1, tmp[1]);
                    const v2 = getPos(i+2, tmp[2]);
                    math.addVec3(v0, v1, tmp[3]);
                    math.addVec3(v2, tmp[3], tmp[3]);
                    const faceArea = math.lenVec3(
                        math.cross3Vec3(
                            math.subVec3(v1, v0, tmp[1]),
                            math.subVec3(v2, v0, tmp[2]),
                            tmp[0])) / 2;
                    totalArea += faceArea;
                    math.mulVec3Scalar(tmp[3], faceArea, tmp[3]);
                    math.addVec3(centroid, tmp[3], centroid);
                }
                this._metrics = { surfaceArea: totalArea, centroid: math.mulVec3Scalar(centroid, 1 / totalArea / 3, centroid) };
                break;
            }
            default:
                this._metrics = { surfaceArea: 0 };
                break;
            }
        }
        return this._metrics;
    }

    /**
     * Returns the surface area of this Mesh.
     * @returns {number}
     */
    get surfaceArea() {
        return this._getMetrics().surfaceArea;
    }

    /**
     * Returns the centroid of this Mesh.
     * @returns {number}
     */
    get centroid() {
        return this._getMetrics().centroid;
    }

    /**
     * Approximate number of triangles in this ReadableGeometry.
     *
     * Will be zero if {@link ReadableGeometry#primitive} is not 'triangles', 'triangle-strip' or 'triangle-fan'.
     *
     * @type {Number}
     */
    get numTriangles() {
        return this._numTriangles;
    }

    _setAABBDirty() {
        if (this._aabbDirty) {
            return;
        }
        this._aabbDirty = true;
        this._aabbDirty = true;
        this._obbDirty = true;
    }

    _getState() {
        return this._state;
    }

    /**
     * Destroys this ReadableGeometry
     */
    destroy() {
        super.destroy();
        const state = this._state;
        if (state.indicesBuf) {
            state.indicesBuf.destroy();
        }
        if (state.positionsBuf) {
            state.positionsBuf.destroy();
        }
        if (state.normalsBuf) {
            state.normalsBuf.destroy();
        }
        if (state.uvBuf) {
            state.uvBuf.destroy();
        }
        if (state.colorsBuf) {
            state.colorsBuf.destroy();
        }
        if (this._edgeIndicesBuf) {
            this._edgeIndicesBuf.destroy();
        }
        if (this._pickTrianglePositionsBuf) {
            this._pickTrianglePositionsBuf.destroy();
        }
        if (this._pickTriangleColorsBuf) {
            this._pickTriangleColorsBuf.destroy();
        }
        if (this._pickVertexPositionsBuf) {
            this._pickVertexPositionsBuf.destroy();
        }
        if (this._pickVertexColorsBuf) {
            this._pickVertexColorsBuf.destroy();
        }
        state.destroy();
        memoryStats$1.meshes--;
    }
}

const memoryStats = stats.memory;
const tempAABB$2 = math.AABB3();

/**
 * @desc A {@link Geometry} that keeps its geometry data solely in GPU memory, without retaining it in browser memory.
 *
 * VBOGeometry uses less memory than {@link ReadableGeometry}, which keeps its geometry data in both browser and GPU memory.
 *
 * ## Usage
 *
 * Creating a {@link Mesh} with a VBOGeometry that defines a single triangle, plus a {@link PhongMaterial} with diffuse {@link Texture}:
 *
 * [[Run this example](/examples/index.html#geometry_VBOGeometry)]
 *
 * ````javascript
 * import {Viewer, Mesh, VBOGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *         canvasId: "myCanvas"
 *     });
 *
 * new Mesh(viewer.scene, {
 *         geometry: new VBOGeometry(viewer.scene, {
 *             primitive: "triangles",
 *             positions: [0.0, 3, 0.0, -3, -3, 0.0, 3, -3, 0.0],
 *             normals: [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
 *             uv: [0.0, 0.0, 0.5, 1.0, 1.0, 0.0],
 *             indices: [0, 1, 2]
 *         }),
 *         material: new PhongMaterial(viewer.scene, {
 *             diffuseMap: new Texture(viewer.scene, {
 *                 src: "textures/diffuse/uvGrid2.jpg"
 *             }),
 *             backfaces: true
 *         })
 *     });
 * ````
 */
class VBOGeometry extends Geometry {

    /**
     @private
     */
    get type() {
        return "VBOGeometry";
    }

    /**
     * @private
     * @returns {Boolean}
     */
    get isVBOGeometry() {
        return true;
    }

    /**
     * @constructor
     * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well.
     * @param {*} [cfg] Configs
     * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted.
     * @param {String} [cfg.primitive="triangles"]  The primitive type. Accepted values are 'points', 'lines', 'line-loop', 'line-strip', 'triangles', 'triangle-strip' and 'triangle-fan'.
     * @param {Number[]} [cfg.positions]  Positions array.
     * @param {Number[]} [cfg.normals]  Vertex normal vectors array.
     * @param {Number[]} [cfg.uv]  UVs array.
     * @param {Number[]} [cfg.colors]  Vertex colors.
     * @param {Number[]} [cfg.indices]  Indices array.
     * @param {Number} [cfg.edgeThreshold=10]  When autogenerating edges for supporting {@link Drawable#edges}, this indicates the threshold angle (in degrees) between the face normals of adjacent triangles below which the edge is discarded.
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._state = new RenderState({ // Arrays for emphasis effects are got from xeokit.GeometryLite friend methods
            compressGeometry: true,
            primitive: null, // WebGL enum
            primitiveName: null, // String
            positionsDecodeMatrix: null, // Set when compressGeometry == true
            uvDecodeMatrix: null, // Set when compressGeometry == true
            positionsBuf: null,
            normalsBuf: null,
            colorsbuf: null,
            uvBuf: null,
            indicesBuf: null,
            hash: ""
        });

        this._numTriangles = 0;

        this._edgeThreshold = cfg.edgeThreshold || 10.0;
        this._aabb = null;
        this._obb = math.OBB3();

        const state = this._state;
        const gl = this.scene.canvas.gl;

        cfg.primitive = cfg.primitive || "triangles";
        switch (cfg.primitive) {
            case "points":
                state.primitive = gl.POINTS;
                state.primitiveName = cfg.primitive;
                break;
            case "lines":
                state.primitive = gl.LINES;
                state.primitiveName = cfg.primitive;
                break;
            case "line-loop":
                state.primitive = gl.LINE_LOOP;
                state.primitiveName = cfg.primitive;
                break;
            case "line-strip":
                state.primitive = gl.LINE_STRIP;
                state.primitiveName = cfg.primitive;
                break;
            case "triangles":
                state.primitive = gl.TRIANGLES;
                state.primitiveName = cfg.primitive;
                break;
            case "triangle-strip":
                state.primitive = gl.TRIANGLE_STRIP;
                state.primitiveName = cfg.primitive;
                break;
            case "triangle-fan":
                state.primitive = gl.TRIANGLE_FAN;
                state.primitiveName = cfg.primitive;
                break;
            default:
                this.error("Unsupported value for 'primitive': '" + cfg.primitive +
                    "' - supported values are 'points', 'lines', 'line-loop', 'line-strip', 'triangles', " +
                    "'triangle-strip' and 'triangle-fan'. Defaulting to 'triangles'.");
                state.primitive = gl.TRIANGLES;
                state.primitiveName = cfg.primitive;
        }

        if (!cfg.positions) {
            this.error("Config expected: positions");
            return; // TODO: Recover?
        }

        if (!cfg.indices) {
            this.error("Config expected: indices");
            return; // TODO: Recover?
        }

        var positions;

        {
            const positionsDecodeMatrix = cfg.positionsDecodeMatrix;

            if (positionsDecodeMatrix) ; else {

                // Uncompressed positions

                const bounds = geometryCompressionUtils.getPositionsBounds(cfg.positions);
                const result = geometryCompressionUtils.compressPositions(cfg.positions, bounds.min, bounds.max);
                positions = result.quantized;
                state.positionsDecodeMatrix = result.decodeMatrix;
                state.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, positions, positions.length, 3, gl.STATIC_DRAW);
                memoryStats.positions += state.positionsBuf.numItems;
                math.positions3ToAABB3(cfg.positions, this._aabb);
                math.positions3ToAABB3(positions, tempAABB$2, state.positionsDecodeMatrix);
                math.AABB3ToOBB3(tempAABB$2, this._obb);
            }
        }

        if (cfg.colors) {
            const colors = cfg.colors.constructor === Float32Array ? cfg.colors : new Float32Array(cfg.colors);
            state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, colors, colors.length, 4, gl.STATIC_DRAW);
            memoryStats.colors += state.colorsBuf.numItems;
        }

        if (cfg.uv) {
            const bounds = geometryCompressionUtils.getUVBounds(cfg.uv);
            const result = geometryCompressionUtils.compressUVs(cfg.uv, bounds.min, bounds.max);
            const uv = result.quantized;
            state.uvDecodeMatrix = result.decodeMatrix;
            state.uvBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, uv, uv.length, 2, gl.STATIC_DRAW);
            memoryStats.uvs += state.uvBuf.numItems;
        }

        if (cfg.normals) {
            const normals = geometryCompressionUtils.compressNormals(cfg.normals);
            let normalized = state.compressGeometry;
            state.normalsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, normals, normals.length, 3, gl.STATIC_DRAW, normalized);
            memoryStats.normals += state.normalsBuf.numItems;
        }

        {
            const indices = (cfg.indices.constructor === Uint32Array || cfg.indices.constructor === Uint16Array) ? cfg.indices : new Uint32Array(cfg.indices);
            state.indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, indices, indices.length, 1, gl.STATIC_DRAW);
            memoryStats.indices += state.indicesBuf.numItems;
            const edgeIndices = buildEdgeIndices(positions, indices, state.positionsDecodeMatrix, this._edgeThreshold);
            this._edgeIndicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, edgeIndices, edgeIndices.length, 1, gl.STATIC_DRAW);

            if (this._state.primitiveName === "triangles") {
                this._numTriangles = (cfg.indices.length / 3);
            }
        }

        this._buildHash();

        memoryStats.meshes++;
    }

    _buildHash() {
        const state = this._state;
        const hash = ["/g"];
        hash.push("/" + state.primitive + ";");
        if (state.positionsBuf) {
            hash.push("p");
        }
        if (state.colorsBuf) {
            hash.push("c");
        }
        if (state.normalsBuf || state.autoVertexNormals) {
            hash.push("n");
        }
        if (state.uvBuf) {
            hash.push("u");
        }
        hash.push("cp"); // Always compressed
        hash.push(";");
        state.hash = hash.join("");
    }

    _getEdgeIndices() {
        return this._edgeIndicesBuf;
    }

    /**
     * Gets the primitive type.
     *
     * Possible types are: 'points', 'lines', 'line-loop', 'line-strip', 'triangles', 'triangle-strip' and 'triangle-fan'.
     *
     * @type {String}
     */
    get primitive() {
        return this._state.primitiveName;
    }

    /**
     * Gets the local-space axis-aligned 3D boundary (AABB).
     *
     * The AABB is represented by a six-element Float64Array containing the min/max extents of the axis-aligned volume, ie. ````[xmin, ymin,zmin,xmax,ymax, zmax]````.
     *
     * @type {Number[]}
     */
    get aabb() {
        return this._aabb;
    }

    /**
     * Gets the local-space oriented 3D boundary (OBB).
     *
     * The OBB is represented by a 32-element Float64Array containing the eight vertices of the box, where each vertex is a homogeneous coordinate having [x,y,z,w] elements.
     *
     * @type {Number[]}
     */
    get obb() {
        return this._obb;
    }

    /**
     * Approximate number of triangles in this VBOGeometry.
     *
     * Will be zero if {@link VBOGeometry#primitive} is not 'triangles', 'triangle-strip' or 'triangle-fan'.
     *
     * @type {Number}
     */
    get numTriangles() {
        return this._numTriangles;
    }

    /** @private */
    _getState() {
        return this._state;
    }

    /**
     * Destroys this component.
     */
    destroy() {
        super.destroy();
        const state = this._state;
        if (state.indicesBuf) {
            state.indicesBuf.destroy();
        }
        if (state.positionsBuf) {
            state.positionsBuf.destroy();
        }
        if (state.normalsBuf) {
            state.normalsBuf.destroy();
        }
        if (state.uvBuf) {
            state.uvBuf.destroy();
        }
        if (state.colorsBuf) {
            state.colorsBuf.destroy();
        }
        if (this._edgeIndicesBuf) {
            this._edgeIndicesBuf.destroy();
        }
        state.destroy();
        memoryStats.meshes--;
    }
}

/**
 * @private
 */
var K3D = {};

K3D.load = function(path, resp)
{
    var request = new XMLHttpRequest();
    request.open("GET", path, true);
    request.responseType = "arraybuffer";
    request.onload = function(e){resp(e.target.response);};
    request.send();
};

K3D.save = function(buff, path)
{
    var dataURI = "data:application/octet-stream;base64," + btoa(K3D.parse._buffToStr(buff));
    window.location.href = dataURI;
};

K3D.clone = function(o)
{
    return JSON.parse(JSON.stringify(o));
};



K3D.bin = {};

K3D.bin.f  = new Float32Array(1);
K3D.bin.fb = new Uint8Array(K3D.bin.f.buffer);

K3D.bin.rf		= function(buff, off) { var f = K3D.bin.f, fb = K3D.bin.fb; for(var i=0; i<4; i++) fb[i] = buff[off+i]; return f[0]; };
K3D.bin.rsl		= function(buff, off) { return buff[off] | buff[off+1]<<8; };
K3D.bin.ril		= function(buff, off) { return buff[off] | buff[off+1]<<8 | buff[off+2]<<16 | buff[off+3]<<24; };
K3D.bin.rASCII0 = function(buff, off) { var s = ""; while(buff[off]!=0) s += String.fromCharCode(buff[off++]); return s; };


K3D.bin.wf		= function(buff, off, v) { var f=new Float32Array(buff.buffer, off, 1); f[0]=v; };
K3D.bin.wsl		= function(buff, off, v) { buff[off]=v; buff[off+1]=v>>8; };
K3D.bin.wil		= function(buff, off, v) { buff[off]=v; buff[off+1]=v>>8; buff[off+2]=v>>16; buff[off+3]>>24; };
K3D.parse = {};

K3D.parse._buffToStr = function(buff)
{
    var a = new Uint8Array(buff);
    var s = "";
    for(var i=0; i<a.length; i++) s = s.concat(String.fromCharCode(a[i]));
    return s;
};

K3D.parse._strToBuff = function(str)
{
    var buf = new ArrayBuffer(str.length);
    var bufView = new Uint8Array(buf);
    for (var i=0; i<str.length; i++) bufView[i] = str.charCodeAt(i);
    return buf;
};

K3D.parse._readLine = function(a, off)	// Uint8Array, offset
{
    var s = "";
    while(a[off] != 10) s += String.fromCharCode(a[off++]);
    return s;
};
K3D.parse.fromJSON = function(buff)
{
    var json = JSON.parse(K3D.parse._buffToStr(buff));
    return json;
};

K3D.parse.toJSON = function(object)
{
    var str = JSON.stringify(object);
    return K3D.parse._strToBuff(str);
};

K3D.parse.fromOBJ = function(buff)
{
    var res = {};
    res.groups = {};

    res.c_verts = [];
    res.c_uvt	= [];
    res.c_norms = [];

    res.i_verts = [];
    res.i_uvt   = [];
    res.i_norms = [];

    var cg = {to:0};	// current group
    var off = 0;
    var a = new Uint8Array(buff);

    while(off < a.length)
    {
        var line = K3D.parse._readLine(a, off);
        off += line.length + 1;
        line = line.replace(/ +(?= )/g,'');
        line = line.replace(/(^\s+|\s+$)/g, '');
        var cds = line.split(" ");
        if(cds[0] == "g")
        {
            cg.to = res.i_verts.length;
            if(res.groups[cds[1]] == null) res.groups[cds[1]] = {from:res.i_verts.length, to:0};
            cg = res.groups[cds[1]];
        }
        if(cds[0] == "v")
        {
            var x = parseFloat(cds[1]);
            var y = parseFloat(cds[2]);
            var z = parseFloat(cds[3]);
            res.c_verts.push(x,y,z);
        }
        if(cds[0] == "vt")
        {
            var x = parseFloat(cds[1]);
            var y = 1-parseFloat(cds[2]);
            res.c_uvt.push(x,y);
        }
        if(cds[0] == "vn")
        {
            var x = parseFloat(cds[1]);
            var y = parseFloat(cds[2]);
            var z = parseFloat(cds[3]);
            res.c_norms.push(x,y,z);
        }
        if(cds[0] == "f")
        {
            var v0a = cds[1].split("/"), v1a = cds[2].split("/"), v2a = cds[3].split("/");
            var vi0 = parseInt(v0a[0])-1, vi1 = parseInt(v1a[0])-1, vi2 = parseInt(v2a[0])-1;
            var ui0 = parseInt(v0a[1])-1, ui1 = parseInt(v1a[1])-1, ui2 = parseInt(v2a[1])-1;
            var ni0 = parseInt(v0a[2])-1, ni1 = parseInt(v1a[2])-1, ni2 = parseInt(v2a[2])-1;

            var vlen = res.c_verts.length/3, ulen = res.c_uvt.length/2, nlen = res.c_norms.length/3;
            if(vi0<0) vi0 = vlen + vi0+1; if(vi1<0) vi1 = vlen + vi1+1;	if(vi2<0) vi2 = vlen + vi2+1;
            if(ui0<0) ui0 = ulen + ui0+1; if(ui1<0) ui1 = ulen + ui1+1;	if(ui2<0) ui2 = ulen + ui2+1;
            if(ni0<0) ni0 = nlen + ni0+1; if(ni1<0) ni1 = nlen + ni1+1;	if(ni2<0) ni2 = nlen + ni2+1;

            res.i_verts.push(vi0, vi1, vi2);  //cg.i_verts.push(vi0, vi1, vi2)
            res.i_uvt  .push(ui0, ui1, ui2);  //cg.i_uvt  .push(ui0, ui1, ui2);
            res.i_norms.push(ni0, ni1, ni2);  //cg.i_norms.push(ni0, ni1, ni2);
            if(cds.length == 5)
            {
                var v3a = cds[4].split("/");
                var vi3 = parseInt(v3a[0])-1, ui3 = parseInt(v3a[1])-1, ni3 = parseInt(v3a[2])-1;
                if(vi3<0) vi3 = vlen + vi3+1;
                if(ui3<0) ui3 = ulen + ui3+1;
                if(ni3<0) ni3 = nlen + ni3+1;
                res.i_verts.push(vi0, vi2, vi3);  //cg.i_verts.push(vi0, vi2, vi3);
                res.i_uvt  .push(ui0, ui2, ui3);  //cg.i_uvt  .push(ui0, ui2, ui3);
                res.i_norms.push(ni0, ni2, ni3);  //cg.i_norms.push(ni0, ni2, ni3);
            }
        }
    }
    cg.to = res.i_verts.length;

    return res;
};


K3D.parse.fromMD2 = function(buff)
{
    buff = new Uint8Array(buff);
    var res = {};
    var head = {};
    //res.head = head;
    head.ident			= K3D.bin.ril(buff,  0);             /* magic number: "IDP2" */
    head.version		= K3D.bin.ril(buff,  4);             /* version: must be 8 */

    head.skinwidth		= K3D.bin.ril(buff,  8);             /* texture width */
    head.skinheight		= K3D.bin.ril(buff, 12);             /* texture height */

    head.framesize		= K3D.bin.ril(buff, 16);             /* size in bytes of a frame */

    head.num_skins		= K3D.bin.ril(buff, 20);             /* number of skins */
    head.num_vertices	= K3D.bin.ril(buff, 24);             /* number of vertices per frame */
    head.num_st			= K3D.bin.ril(buff, 28);             /* number of texture coordinates */
    head.num_tris		= K3D.bin.ril(buff, 32);             /* number of triangles */
    head.num_glcmds		= K3D.bin.ril(buff, 36);             /* number of opengl commands */
    head.num_frames		= K3D.bin.ril(buff, 40);             /* number of frames */

    head.offset_skins	= K3D.bin.ril(buff, 44);             /* offset skin data */
    head.offset_st		= K3D.bin.ril(buff, 48);             /* offset texture coordinate data */
    head.offset_tris	= K3D.bin.ril(buff, 52);             /* offset triangle data */
    head.offset_frames	= K3D.bin.ril(buff, 56);             /* offset frame data */
    head.offset_glcmds	= K3D.bin.ril(buff, 60);             /* offset OpenGL command data */
    head.offset_end		= K3D.bin.ril(buff, 64);             /* offset end of file */

    var off = head.offset_st;
    res.c_uvt = [];
    for(var i=0; i<head.num_st; i++)
    {
        var x = K3D.bin.rsl(buff, off  )/head.skinwidth;
        var y = K3D.bin.rsl(buff, off+2)/head.skinheight;
        res.c_uvt.push(x,y);  off += 4;
    }

    var off = head.offset_tris;
    var vi = [], ti = [];
    res.i_verts = vi;
    res.i_uvt = ti;
    //res.tris = {i_verts : vi, i_uvt : ti};
    for(var i=0; i<head.num_tris; i++)
    {
        vi.push(K3D.bin.rsl(buff, off  ), K3D.bin.rsl(buff, off+2), K3D.bin.rsl(buff, off+4 ));
        ti.push(K3D.bin.rsl(buff, off+6), K3D.bin.rsl(buff, off+8), K3D.bin.rsl(buff, off+10));
        off += 12;
    }

    var off = head.offset_skins;
    res.skins = [];
    for(var i=0; i<head.num_skins; i++)
    {
        res.skins.push(K3D.bin.rASCII0(buff, off));
        off += 64;
    }

    var off = head.offset_frames;
    res.frames = [];
    var nms = K3D.parse.fromMD2._normals;
    for(var i=0; i<head.num_frames; i++)
    {
        var fr = {};
        var sx = K3D.bin.rf(buff, off), sy = K3D.bin.rf(buff, off+4), sz = K3D.bin.rf(buff, off+8);  off += 12;
        var tx = K3D.bin.rf(buff, off), ty = K3D.bin.rf(buff, off+4), tz = K3D.bin.rf(buff, off+8);  off += 12;
        fr.name		 = K3D.bin.rASCII0(buff, off); off += 16;
        fr.verts	 = [];
        fr.norms	 = [];

        for(var j=0; j<head.num_vertices; j++)
        {
            fr.verts.push(buff[off]*sx+tx, buff[off+1]*sy+ty, buff[off+2]*sz+tz);
            fr.norms.push(nms[3*buff[off+3]], nms[3*buff[off+3]+1], nms[3*buff[off+3]+2]);
            off += 4;
        }
        res.frames.push(fr);
    }
    return res;
};



/*
 static MD2 normals
 */

K3D.parse.fromMD2._normals =
    [
        -0.525731,  0.000000,  0.850651,
        -0.442863,  0.238856,  0.864188,
        -0.295242,  0.000000,  0.955423,
        -0.309017,  0.500000,  0.809017,
        -0.16246,  0.262866,  0.951056,
        0.000000,  0.000000,  1.000000,
        0.000000,  0.850651,  0.525731,
        -0.147621,  0.716567,  0.681718,
        0.147621,  0.716567,  0.681718,
        0.000000,  0.525731,  0.850651,
        0.309017,  0.500000,  0.809017,
        0.525731,  0.000000,  0.850651,
        0.295242,  0.000000,  0.955423,
        0.442863,  0.238856,  0.864188,
        0.162460,  0.262866,  0.951056,
        -0.681718,  0.147621,  0.716567,
        -0.809017,  0.309017,  0.500000,
        -0.587785,  0.425325,  0.688191,
        -0.850651,  0.525731,  0.000000,
        -0.864188,  0.442863,  0.238856,
        -0.716567,  0.681718,  0.147621,
        -0.688191,  0.587785,  0.425325,
        -0.5,  0.809017,  0.309017,
        -0.238856,  0.864188,  0.442863,
        -0.425325,  0.688191,  0.587785,
        -0.716567,  0.681718, -0.147621,
        -0.5,  0.809017, -0.309017,
        -0.525731,  0.850651,  0.000000,
        0.000000,  0.850651, -0.525731,
        -0.238856,  0.864188, -0.442863,
        0.000000,  0.955423, -0.295242,
        -0.262866,  0.951056, -0.16246,
        0.000000,  1.000000,  0.000000,
        0.000000,  0.955423,  0.295242,
        -0.262866,  0.951056,  0.162460,
        0.238856,  0.864188,  0.442863,
        0.262866,  0.951056,  0.162460,
        0.500000,  0.809017,  0.309017,
        0.238856,  0.864188, -0.442863,
        0.262866,  0.951056, -0.16246,
        0.500000,  0.809017, -0.309017,
        0.850651,  0.525731,  0.000000,
        0.716567,  0.681718,  0.147621,
        0.716567,  0.681718, -0.147621,
        0.525731,  0.850651,  0.000000,
        0.425325,  0.688191,  0.587785,
        0.864188,  0.442863,  0.238856,
        0.688191,  0.587785,  0.425325,
        0.809017,  0.309017,  0.500000,
        0.681718,  0.147621,  0.716567,
        0.587785,  0.425325,  0.688191,
        0.955423,  0.295242,  0.000000,
        1.000000,  0.000000,  0.000000,
        0.951056,  0.162460,  0.262866,
        0.850651, -0.525731,  0.000000,
        0.955423, -0.295242,  0.000000,
        0.864188, -0.442863,  0.238856,
        0.951056, -0.16246,  0.262866,
        0.809017, -0.309017,  0.500000,
        0.681718, -0.147621,  0.716567,
        0.850651,  0.000000,  0.525731,
        0.864188,  0.442863, -0.238856,
        0.809017,  0.309017, -0.5,
        0.951056,  0.162460, -0.262866,
        0.525731,  0.000000, -0.850651,
        0.681718,  0.147621, -0.716567,
        0.681718, -0.147621, -0.716567,
        0.850651,  0.000000, -0.525731,
        0.809017, -0.309017, -0.5,
        0.864188, -0.442863, -0.238856,
        0.951056, -0.16246, -0.262866,
        0.147621,  0.716567, -0.681718,
        0.309017,  0.500000, -0.809017,
        0.425325,  0.688191, -0.587785,
        0.442863,  0.238856, -0.864188,
        0.587785,  0.425325, -0.688191,
        0.688191,  0.587785, -0.425325,
        -0.147621,  0.716567, -0.681718,
        -0.309017,  0.500000, -0.809017,
        0.000000,  0.525731, -0.850651,
        -0.525731,  0.000000, -0.850651,
        -0.442863,  0.238856, -0.864188,
        -0.295242,  0.000000, -0.955423,
        -0.16246,  0.262866, -0.951056,
        0.000000,  0.000000, -1,
        0.295242,  0.000000, -0.955423,
        0.162460,  0.262866, -0.951056,
        -0.442863, -0.238856, -0.864188,
        -0.309017, -0.5, -0.809017,
        -0.16246, -0.262866, -0.951056,
        0.000000, -0.850651, -0.525731,
        -0.147621, -0.716567, -0.681718,
        0.147621, -0.716567, -0.681718,
        0.000000, -0.525731, -0.850651,
        0.309017, -0.5, -0.809017,
        0.442863, -0.238856, -0.864188,
        0.162460, -0.262866, -0.951056,
        0.238856, -0.864188, -0.442863,
        0.500000, -0.809017, -0.309017,
        0.425325, -0.688191, -0.587785,
        0.716567, -0.681718, -0.147621,
        0.688191, -0.587785, -0.425325,
        0.587785, -0.425325, -0.688191,
        0.000000, -0.955423, -0.295242,
        0.000000, -1,  0.000000,
        0.262866, -0.951056, -0.16246,
        0.000000, -0.850651,  0.525731,
        0.000000, -0.955423,  0.295242,
        0.238856, -0.864188,  0.442863,
        0.262866, -0.951056,  0.162460,
        0.500000, -0.809017,  0.309017,
        0.716567, -0.681718,  0.147621,
        0.525731, -0.850651,  0.000000,
        -0.238856, -0.864188, -0.442863,
        -0.5, -0.809017, -0.309017,
        -0.262866, -0.951056, -0.16246,
        -0.850651, -0.525731,  0.000000,
        -0.716567, -0.681718, -0.147621,
        -0.716567, -0.681718,  0.147621,
        -0.525731, -0.850651,  0.000000,
        -0.5, -0.809017,  0.309017,
        -0.238856, -0.864188,  0.442863,
        -0.262866, -0.951056,  0.162460,
        -0.864188, -0.442863,  0.238856,
        -0.809017, -0.309017,  0.500000,
        -0.688191, -0.587785,  0.425325,
        -0.681718, -0.147621,  0.716567,
        -0.442863, -0.238856,  0.864188,
        -0.587785, -0.425325,  0.688191,
        -0.309017, -0.5,  0.809017,
        -0.147621, -0.716567,  0.681718,
        -0.425325, -0.688191,  0.587785,
        -0.16246, -0.262866,  0.951056,
        0.442863, -0.238856,  0.864188,
        0.162460, -0.262866,  0.951056,
        0.309017, -0.5,  0.809017,
        0.147621, -0.716567,  0.681718,
        0.000000, -0.525731,  0.850651,
        0.425325, -0.688191,  0.587785,
        0.587785, -0.425325,  0.688191,
        0.688191, -0.587785,  0.425325,
        -0.955423,  0.295242,  0.000000,
        -0.951056,  0.162460,  0.262866,
        -1,  0.000000,  0.000000,
        -0.850651,  0.000000,  0.525731,
        -0.955423, -0.295242,  0.000000,
        -0.951056, -0.16246,  0.262866,
        -0.864188,  0.442863, -0.238856,
        -0.951056,  0.162460, -0.262866,
        -0.809017,  0.309017, -0.5,
        -0.864188, -0.442863, -0.238856,
        -0.951056, -0.16246, -0.262866,
        -0.809017, -0.309017, -0.5,
        -0.681718,  0.147621, -0.716567,
        -0.681718, -0.147621, -0.716567,
        -0.850651,  0.000000, -0.525731,
        -0.688191,  0.587785, -0.425325,
        -0.587785,  0.425325, -0.688191,
        -0.425325,  0.688191, -0.587785,
        -0.425325, -0.688191, -0.587785,
        -0.587785, -0.425325, -0.688191,
        -0.688191, -0.587785, -0.425325
    ];

K3D.parse.fromCollada = function(buff)
{
    var str = K3D.parse._buffToStr(buff);
    var xml = new DOMParser().parseFromString(str,"text/xml");
    xml = xml.childNodes[0];
    var resp = {};

    //console.log(xml);

    var ass = xml.getElementsByTagName("asset"             )[0];
    var geo = xml.getElementsByTagName("library_geometries")[0];
    var ima = xml.getElementsByTagName("library_images"    )[0];
    var mat = xml.getElementsByTagName("library_materials" )[0];
    var eff = xml.getElementsByTagName("library_effects"   )[0];

    //console.log(xml);
    if(ass) resp.asset 		= K3D.parse.fromCollada._asset        (ass);
    if(geo) resp.geometries = K3D.parse.fromCollada._libGeometries(geo);
    if(ima) resp.images     = K3D.parse.fromCollada._libImages    (ima);
    if(mat) resp.materials  = K3D.parse.fromCollada._libMaterials (mat);
    if(eff) resp.effects    = K3D.parse.fromCollada._libEffects   (eff);
    return resp;
};

K3D.parse.fromCollada._asset = function(xml)
{
    //console.log(xml);
    return {
        created : xml.getElementsByTagName("created" )[0].textContent,
        modified: xml.getElementsByTagName("modified")[0].textContent,
        up_axis : xml.getElementsByTagName("up_axis" )[0].textContent
    };
};

K3D.parse.fromCollada._libGeometries = function(xml)
{
    xml = xml.getElementsByTagName("geometry");
    var res = [];
    for(var i=0; i<xml.length; i++)
    {
        var g = xml[i];
        var o = K3D.parse.fromCollada._getMesh(g.getElementsByTagName("mesh")[0]);
        res.push(o);
    }
    return res;
};

K3D.parse.fromCollada._getMesh = function(mesh)
{
    //console.log(mesh);
    var res = {};
    var ss = mesh.getElementsByTagName("source");
    var sources = res.sources = {};
    for(var i=0; i<ss.length; i++)
    {
        var farr = ss[i].getElementsByTagName("float_array")[0].textContent.split(" ");
        var fl = farr.length - (farr[farr.length-1] == "" ? 1 : 0);
        var arr = new Array(fl);
        for(var j=0; j<fl; j++) arr[j] = parseFloat(farr[j]);
        sources[ss[i].getAttribute("id")] = arr;
    }

    res.triangles = [];
    var tgs = mesh.getElementsByTagName("triangles");
    if(tgs == null) return res;
    for(var i=0; i<tgs.length; i++)
    {
        var t = {};
        var tnode = tgs[i];
        t.material = tnode.getAttribute("material");
        var inputs = tnode.getElementsByTagName("input");
        var inds = [];
        for(var j=0; j<inputs.length; j++)
        {
            var inp = inputs[j], arr = [];
            inds[parseInt(inp.getAttribute("offset"))] = arr;
            var par = inp.getAttribute("semantic");
            t["s_"+par] = (par == "VERTEX") ?
                mesh.getElementsByTagName("vertices")[0].getElementsByTagName("input")[0].getAttribute("source").substring(1)
                : inp.getAttribute("source").substring(1);

            t["i_"+par] = arr;
            sources[t["s_"+par]];
        }
        var indices = tnode.getElementsByTagName("p")[0].textContent.split(" ");
        var inum = 3*Math.floor(indices.length/3);
        for(var j=0; j<inum; j++) inds[j%inputs.length].push(parseInt(indices[j]));

        /*
         if(t.s_VERTEX  ) t.u_VERTEX   = K3D.edit.unwrap(t.i_VERTEX  , sources[t.s_VERTEX  ], 3);
         if(t.s_TEXCOORD) t.u_TEXCOORD = K3D.edit.unwrap(t.i_TEXCOORD, sources[t.s_TEXCOORD], 2);
         if(t.s_NORMAL  ) t.u_NORMAL   = K3D.edit.unwrap(t.i_NORMAL  , sources[t.s_NORMAL  ], 3);
         //*/
        //if(t.s_TEXCOORD) for(var j=1; j<t.u_TEXCOORD.length; j+=2) t.u_TEXCOORD[j] = 1 - t.u_TEXCOORD[j];

        /*
         t.u_INDEX = new Array(t.i_VERTEX.length);
         for(var j=0; j<t.i_VERTEX.length; j++) t.u_INDEX[j] = j;
         */

        res.triangles.push(t);
    }
    return res;
};

K3D.parse.fromCollada._libImages = function(xml)
{
    xml = xml.getElementsByTagName("image");
    var res = {};
    for(var i=0; i<xml.length; i++)
    {
        res[xml[i].getAttribute("id")] = xml[i].getElementsByTagName("init_from")[0].textContent;
    }
    return res;
};

K3D.parse.fromCollada._libMaterials = function(xml)
{
    xml = xml.getElementsByTagName("material");
    var res = {};
    for(var i=0; i<xml.length; i++)
    {
        res[xml[i].getAttribute("name")] = xml[i].getElementsByTagName("instance_effect")[0].getAttribute("url").substring(1);
    }
    return res;
};

K3D.parse.fromCollada._libEffects = function(xml)
{
    xml = xml.getElementsByTagName("effect");
    var res = {};
    for(var i=0; i<xml.length; i++)
    {
        var eff = {};
        var params = xml[i].getElementsByTagName("newparam");
        for(var j=0; j<params.length; j++)
        {
            var srf = params[j].getElementsByTagName("surface")[0];
            if(srf) eff.surface = srf.getElementsByTagName("init_from")[0].textContent;
        }
        res[xml[i].getAttribute("id")] = eff;
    }
    return res;
};





K3D.parse.from3DS = function(buff)
{
    buff = new Uint8Array(buff);
    var res = {};
    if(K3D.bin.rsl(buff, 0) != 0x4d4d) return null;
    var lim = K3D.bin.ril(buff, 2);

    var off = 6;
    while(off < lim)
    {
        var cid = K3D.bin.rsl(buff, off);
        var lng = K3D.bin.ril(buff, off+2);
        //console.log(cid.toString(16), lng);

        if(cid == 0x3d3d) res.edit = K3D.parse.from3DS._edit3ds(buff, off, lng);
        if(cid == 0xb000) res.keyf = K3D.parse.from3DS._keyf3ds(buff, off, lng);

        off += lng;
    }
    return res;
};

K3D.parse.from3DS._edit3ds = function(buff, coff, clng)	// buffer, chunk offset, length
{
    var res = {};
    var off = coff+6;
    while(off < coff+clng)
    {
        var cid = K3D.bin.rsl(buff, off);
        var lng = K3D.bin.ril(buff, off+2);
        //console.log("\t", cid.toString(16), lng);

        if(cid == 0x4000) { if(res.objects==null) res.objects = []; res.objects.push(K3D.parse.from3DS._edit_object(buff, off, lng)); }
        //if(cid == 0xb000) res.KEYF3DS = K3D.parse.from3DS._keyf3ds(buff, off, lng);

        off += lng;
    }
    return res;
};

K3D.parse.from3DS._keyf3ds = function(buff, coff, clng)
{
    var res = {};
    var off = coff+6;
    while(off < coff+clng)
    {
        var cid = K3D.bin.rsl(buff, off);
        var lng = K3D.bin.ril(buff, off+2);
        //console.log("\t\t", cid.toString(16), lng);

        //if(cid == 0x4000) { res.objects.push(K3D.parse.from3DS._edit_object(buff, off, lng)); }
        if(cid == 0xb002) { if(res.desc==null) res.desc = []; res.desc.push(K3D.parse.from3DS._keyf_objdes(buff, off, lng)); }

        off += lng;
    }
    return res;
};

K3D.parse.from3DS._keyf_objdes = function(buff, coff, clng)
{
    var res = {};
    var off = coff+6;
    while(off < coff+clng)
    {
        var cid = K3D.bin.rsl(buff, off);
        var lng = K3D.bin.ril(buff, off+2);
        //console.log("\t\t\t", cid.toString(16), lng);

        if(cid == 0xb010) res.hierarchy = K3D.parse.from3DS._keyf_objhierarch(buff, off, lng);
        if(cid == 0xb011) res.dummy_name = K3D.bin.rASCII0(buff, off+6);
        off += lng;
    }
    return res;
};

K3D.parse.from3DS._keyf_objhierarch = function(buff, coff, clng)
{
    var res = {};
    var off = coff+6;
    res.name = K3D.bin.rASCII0(buff, off);  off += res.name.length+1;
    res.hierarchy = K3D.bin.rsl(buff, off+4);
    return res;
};

K3D.parse.from3DS._edit_object = function(buff, coff, clng)	// buffer, chunk offset, length
{
    var res = {};
    var off = coff+6;
    res.name = K3D.bin.rASCII0(buff, off);  off += res.name.length+1;
    //console.log(res.name);
    while(off < coff+clng)
    {
        var cid = K3D.bin.rsl(buff, off);
        var lng = K3D.bin.ril(buff, off+2);
        //console.log("\t\t", cid.toString(16), lng);

        if(cid == 0x4100) res.mesh = K3D.parse.from3DS._obj_trimesh(buff, off, lng);
        //if(cid == 0xb000) res.KEYF3DS = K3D.parse.from3DS._keyf3ds(buff, off, lng);

        off += lng;
    }
    return res;
};

K3D.parse.from3DS._obj_trimesh = function(buff, coff, clng)	// buffer, chunk offset, length
{
    var res = {};
    var off = coff+6;

    while(off < coff+clng)
    {
        var cid = K3D.bin.rsl(buff, off);
        var lng = K3D.bin.ril(buff, off+2);
        //console.log("\t\t\t", cid.toString(16), lng);

        if(cid == 0x4110) res.vertices      = K3D.parse.from3DS._tri_vertexl     (buff, off, lng);
        if(cid == 0x4120) res.indices       = K3D.parse.from3DS._tri_facel1      (buff, off, lng);
        if(cid == 0x4140) res.uvt			= K3D.parse.from3DS._tri_mappingcoors(buff, off, lng);
        if(cid == 0x4160) res.local		    = K3D.parse.from3DS._tri_local       (buff, off, lng);
        off += lng;
    }
    return res;
};

K3D.parse.from3DS._tri_vertexl = function(buff, coff, clng)	// buffer, chunk offset, length
{
    var res = [];
    var off = coff+6;
    var n = K3D.bin.rsl(buff, off);  off += 2;
    for(var i=0; i<n; i++)
    {
        res.push(K3D.bin.rf(buff, off  ));	res.push(K3D.bin.rf(buff, off+4));	res.push(K3D.bin.rf(buff, off+8));
        off += 12;
    }
    return res;
};

K3D.parse.from3DS._tri_facel1 = function(buff, coff, clng)	// buffer, chunk offset, length
{
    var res = [];
    var off = coff+6;
    var n = K3D.bin.rsl(buff, off);  off += 2;
    for(var i=0; i<n; i++)
    {
        res.push(K3D.bin.rsl(buff, off  ));
        res.push(K3D.bin.rsl(buff, off+2));
        res.push(K3D.bin.rsl(buff, off+4));
        off += 8;
    }
    return res;
};

K3D.parse.from3DS._tri_mappingcoors = function(buff, coff, clng)	// buffer, chunk offset, length
{
    var res = [];
    var off = coff+6;
    var n = K3D.bin.rsl(buff, off);  off += 2;
    for(var i=0; i<n; i++)
    {
        res.push(  K3D.bin.rf(buff, off  ));
        res.push(1-K3D.bin.rf(buff, off+4));
        off += 8;
    }
    return res;
};

K3D.parse.from3DS._tri_local = function(buff, coff, clng)	// buffer, chunk offset, length
{
    var res = {};
    var off = coff+6;
    res.X = [K3D.bin.rf(buff, off), K3D.bin.rf(buff, off+4), K3D.bin.rf(buff, off+8)];  off += 12;
    res.Y = [K3D.bin.rf(buff, off), K3D.bin.rf(buff, off+4), K3D.bin.rf(buff, off+8)];  off += 12;
    res.Z = [K3D.bin.rf(buff, off), K3D.bin.rf(buff, off+4), K3D.bin.rf(buff, off+8)];  off += 12;
    res.C = [K3D.bin.rf(buff, off), K3D.bin.rf(buff, off+4), K3D.bin.rf(buff, off+8)];  off += 12;
    return res;
};

K3D.parse.fromBIV = function(buff)
{
    buff = new Uint8Array(buff);
    var res = {};

    var head = {};

    head.id		= K3D.bin.ril(buff,  0);

    head.verS	= K3D.bin.ril(buff,  4);
    head.texS	= K3D.bin.ril(buff,  8);
    head.indS	= K3D.bin.ril(buff, 12);

    head.verO	= K3D.bin.ril(buff, 16);
    head.verL	= K3D.bin.ril(buff, 20);
    head.texO	= K3D.bin.ril(buff, 24);
    head.texL	= K3D.bin.ril(buff, 28);
    head.indO	= K3D.bin.ril(buff, 32);
    head.indL	= K3D.bin.ril(buff, 36);

    if(head.verO != 0) res.vertices = K3D.parse.fromBIV._readFloats(buff, head.verO, head.verL);
    if(head.texO != 0) res.uvt      = K3D.parse.fromBIV._readFloats(buff, head.texO, head.texL);
    if(head.indO != 0) res.indices  = K3D.parse.fromBIV._readInts  (buff, head.indO, head.indL, head.indS);

    return res;
};

K3D.parse.toBIV = function(obj)
{
    var maxi = 0;
    for(var i=0; i<obj.indices.length; i++)	maxi = Math.max(maxi, obj.indices[i]);

    var indS = 32;
    if(maxi<=0xffff) indS = 16;

    var len = 40;
    if(obj.vertices) len+=obj.vertices.length*4;
    if(obj.uvt     ) len+=obj.uvt     .length*4;
    if(obj.indices ) len+=obj.indices .length*indS/8;


    var buff = new Uint8Array(len);

    K3D.bin.wil(buff,  0, 0x6976616e);

    K3D.bin.wil(buff,  4, 32);
    K3D.bin.wil(buff,  8, 32);
    K3D.bin.wil(buff, 12, indS);

    var off = 40;
    if(obj.vertices)
    {
        K3D.bin.wil(buff, 16, off);
        K3D.bin.wil(buff, 20, 4*obj.vertices.length);
        K3D.parse.fromBIV._writeFloats(buff, off, obj.vertices);
        off += 4*obj.vertices.length;
    }
    if(obj.uvt)
    {
        K3D.bin.wil(buff, 24, off);
        K3D.bin.wil(buff, 28, 4*obj.uvt.length);
        K3D.parse.fromBIV._writeFloats(buff, off, obj.uvt);
        off += 4*obj.uvt.length;
    }
    if(obj.indices)
    {
        K3D.bin.wil(buff, 32, off);
        K3D.bin.wil(buff, 36, 4*obj.indices.length);
        K3D.parse.fromBIV._writeInts  (buff, off, obj.indices, indS);
    }
    return buff.buffer;
};

K3D.parse.fromBIV._readFloats = function(buff, off, len)
{
    var arr = [];
    for(var i=0; i<len/4; i++) arr.push( K3D.bin.rf(buff, off+4*i));
    return arr;
};

K3D.parse.fromBIV._writeFloats = function(buff, off, arr)
{
    for(var i=0; i<arr.length; i++) K3D.bin.wf(buff, off+4*i, arr[i]);
};

K3D.parse.fromBIV._readInts   = function(buff, off, len, cs)
{
    var arr = [];
    for(var i=0; i<len/4; i++)
    {
        if(cs==16) arr.push( K3D.bin.rsl(buff, off+2*i));
        if(cs==32) arr.push( K3D.bin.ril(buff, off+4*i));
    }
    return arr;
};

K3D.parse.fromBIV._writeInts   = function(buff, off, arr, cs)
{
    for(var i=0; i<arr.length; i++)
    {
        if(cs==16) K3D.bin.wsl(buff, off+2*i, arr[i]);
        if(cs==32) K3D.bin.wil(buff, off+4*i, arr[i]);
    }
};
K3D.gen = {};

K3D.gen.Plane = function(sw, sh, tsw, tsh)
{
    if(!tsw) tsw = 1;
    if(!tsh) tsh = 1;
    var r = {verts:[], inds:[], uvt:[]};
    var ssw = sw+1, ssh = sh+1;
    for(var i=0; i<ssh; i++)
    {
        for(var j=0; j<ssw; j++)
        {
            var x = -1 + j*(2/sw);
            var y = -1 + i*(2/sh);
            r.verts.push(x, y, 0);
            r.uvt.push(tsw*j/sw, tsh*i/sh);
            if(i<sh && j<sw)
                r.inds.push(i*ssw+j, i*ssw+j+1, (i+1)*ssw+j,   i*ssw+j+1, (i+1)*ssw+j, (i+1)*ssw+j+1);
        }
    }
    return r;
};
K3D.gen.Cube = function()
{
    var r = {
        verts:[	-1, 1,-1,   1, 1,-1,  -1,-1,-1,   1,-1,-1, // front
            -1, 1, 1,   1, 1, 1,  -1,-1, 1,   1,-1, 1, // back

            -1, 1, 1,  -1, 1,-1,  -1,-1, 1,  -1,-1,-1, // left
            1, 1, 1,   1, 1,-1,   1,-1, 1,   1,-1,-1, // right

            -1, 1,-1,   1, 1,-1,  -1, 1, 1,   1, 1, 1, // top
            -1,-1,-1,   1,-1,-1,  -1,-1, 1,   1,-1, 1  // bottom
        ],
        inds:[	0,1,2, 1,2,3, 4,5,6, 5,6,7,
            8,9,10, 9,10,11, 12,13,14, 13,14,15,
            16,17,18, 17,18,19, 20,21,22, 21,22,23
        ],
        uvt:[
            1/4,1/4,  2/4,1/4,  1/4,2/4,  2/4,2/4, // front
            4/4,1/4,  3/4,1/4,  4/4,2/4,  3/4,2/4, // back

            0/4,1/4,  1/4,1/4,  0/4,2/4,  1/4,2/4, // left
            3/4,1/4,  2/4,1/4,  3/4,2/4,  2/4,2/4, // right

            1/4,1/4,  2/4,1/4,  1/4,0/4,  2/4,0/4, // top
            1/4,2/4,  2/4,2/4,  1/4,3/4,  2/4,3/4, // bottom
        ]
    };
    return r;
};

K3D.gen.Sphere = function(sx, sy){
    var r = {verts:[], inds:[], uvt:[]};
    var nx = sx+1, ny = sy+1;
    for(var i=0; i<ny; i++)	// rows
    {
        for(var j=0; j<nx; j++) // cols
        {
            var lat = -Math.PI/2 + i*Math.PI/sy;
            var lon =  j*2*Math.PI/sx;
            var x = Math.cos(lat) * Math.cos(lon);
            var y = Math.sin(lat);
            var z = Math.cos(lat) * Math.sin(lon);

            r.verts.push(x,y,z);
            r.uvt.push(j/sx, i/sy);
            if(i<sy && j<sx)          // 6 indices for 2 triangles
                r.inds.push(nx*i+j, nx*i+j+1, nx*(i+1)+j, nx*i+j+1, nx*(i+1)+j, nx*(i+1)+j+1);
        }
    }
    return r;
};

K3D.mat = {};

K3D.mat.scale = function(x,y,z){
    return [
        x,0,0,0,
        0,y,0,0,
        0,0,z,0,
        0,0,0,1
    ];
};

K3D.mat.translate = function(x,y,z){
    return [
        1,0,0,0,
        0,1,0,0,
        0,0,1,0,
        x,y,z,1
    ];
};

K3D.mat.rotateDeg = function(x,y,z){
    var r = Math.PI/180;
    return K3D.mat.rotate(x*r, y*r, z*r);
};

K3D.mat.rotate = function(x,y,z){
    var m = [
        1,0,0,0,
        0,1,0,0,
        0,0,1,0,
        0,0,0,1
    ];
    var a =  x;	// alpha
    var b =  y;	// beta
    var g =  z;	// gama

    var ca = Math.cos(a), cb = Math.cos(b), cg = Math.cos(g);
    var sa = Math.sin(a), sb = Math.sin(b), sg = Math.sin(g);

    m[0] = cb*cg;				m[1] = -cb*sg;					m[2 ] = sb;
    m[4] = (ca*sg+sa*sb*cg);	m[5] = (ca*cg-sa*sb*sg);		m[6 ] = -sa*cb;
    m[8] = (sa*sg-ca*sb*cg);	m[9] = (sa*cg+ca*sb*sg);		m[10] = ca*cb;

    return m;
};


K3D.edit = {};

K3D.edit.interpolate = function(a, b, d, t){
    for(var i=0; i<a.length; i++) d[i] = a[i] + t*(b[i] - a[i]);
};


K3D.edit.transform = function(a, m){
    for(var i=0; i<a.length; i+=3)    {
        var x = a[i], y = a[i+1], z = a[i+2];
        a[i+0] = m[0]*x + m[4]*y + m[8 ]*z + m[12];
        a[i+1] = m[1]*x + m[5]*y + m[9 ]*z + m[13];
        a[i+2] = m[2]*x + m[6]*y + m[10]*z + m[14];
    }
};

// starting indices, starting coordinates, coordinates per index

K3D.edit.unwrap = function(ind, crd, cpi){
    var ncrd = new Array(Math.floor(ind.length/3)*cpi);
    for(var i=0; i<ind.length; i++)
    {
        for(var j=0; j<cpi; j++)
        {
            ncrd[i*cpi+j] = crd[ind[i]*cpi+j];
        }
    }
    return ncrd;
};

// current indices, new indices, current array, coordinates per vertex

K3D.edit.remap = function(ind, nind, arr, cpi){
    var ncrd = new Array(arr.length);
    for(var i=0; i<ind.length; i++)
    {
        for(var j=0; j<cpi; j++)
        {
            ncrd[nind[i]*cpi+j] = arr[ind[i]*cpi+j];
        }
    }
    return ncrd;
};

K3D.utils = {};

K3D.utils.getAABB = function(vts)
{
    var minx, miny, minz, maxx, maxy, maxz;
    minx = miny = minz = 999999999;
    maxx = maxy = maxz = -minx;

    for(var i=0; i<vts.length; i+=3)
    {
        var vx = vts[i+0];
        var vy = vts[i+1];
        var vz = vts[i+2];
        if(vx<minx) minx = vx;  if(vx>maxx) maxx = vx;
        if(vy<miny) miny = vy;  if(vy>maxy) maxy = vy;
        if(vz<minz) minz = vz;  if(vy>maxz) maxz = vz;
    }
    return {min:{x:minx, y:miny, z:minz}, max:{x:maxx, y:maxy, z:maxz}};
};

/**
 * @desc Creates box-shaped {@link Geometry}.
 *
 * ## Usage
 *
 * In the example below we'll create a {@link Mesh} with a box-shaped {@link ReadableGeometry}.
 *
 * [[Run this example](/examples/index.html#geometry_builders_buildBoxGeometry)]
 *
 * ````javascript
 * import {Viewer, Mesh, buildBoxGeometry, ReadableGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *         canvasId: "myCanvas"
 * });
 *
 * viewer.scene.camera.eye = [0, 0, 5];
 * viewer.scene.camera.look = [0, 0, 0];
 * viewer.scene.camera.up = [0, 1, 0];
 *
 * new Mesh(viewer.scene, {
 *      geometry: new ReadableGeometry(viewer.scene, buildBoxGeometry({
 *         center: [0,0,0],
 *         xSize: 1,  // Half-size on each axis
 *         ySize: 1,
 *         zSize: 1
 *      }),
 *      material: new PhongMaterial(viewer.scene, {
 *         diffuseMap: new Texture(viewer.scene, {
 *             src: "textures/diffuse/uvGrid2.jpg"
 *         })
 *      })
 * });
 * ````
 *
 * @function buildBoxGeometry
 * @param {*} [cfg] Configs
 * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted.
 * @param {Number[]} [cfg.center]  3D point indicating the center position.
 * @param {Number} [cfg.xSize=1.0]  Half-size on the X-axis.
 * @param {Number} [cfg.ySize=1.0]  Half-size on the Y-axis.
 * @param {Number} [cfg.zSize=1.0]  Half-size on the Z-axis.
 * @returns {Object} Configuration for a {@link Geometry} subtype.
 */
function buildBoxGeometry(cfg = {}) {

    let xSize = cfg.xSize || 1;
    if (xSize < 0) {
        console.error("negative xSize not allowed - will invert");
        xSize *= -1;
    }

    let ySize = cfg.ySize || 1;
    if (ySize < 0) {
        console.error("negative ySize not allowed - will invert");
        ySize *= -1;
    }

    let zSize = cfg.zSize || 1;
    if (zSize < 0) {
        console.error("negative zSize not allowed - will invert");
        zSize *= -1;
    }

    const center = cfg.center;
    const centerX = center ? center[0] : 0;
    const centerY = center ? center[1] : 0;
    const centerZ = center ? center[2] : 0;

    const xmin = -xSize + centerX;
    const ymin = -ySize + centerY;
    const zmin = -zSize + centerZ;
    const xmax = xSize + centerX;
    const ymax = ySize + centerY;
    const zmax = zSize + centerZ;

    return utils.apply(cfg, {

        // The vertices - eight for our cube, each
        // one spanning three array elements for X,Y and Z
        positions: [

            // v0-v1-v2-v3 front
            xmax, ymax, zmax,
            xmin, ymax, zmax,
            xmin, ymin, zmax,
            xmax, ymin, zmax,

            // v0-v3-v4-v1 right
            xmax, ymax, zmax,
            xmax, ymin, zmax,
            xmax, ymin, zmin,
            xmax, ymax, zmin,

            // v0-v1-v6-v1 top
            xmax, ymax, zmax,
            xmax, ymax, zmin,
            xmin, ymax, zmin,
            xmin, ymax, zmax,

            // v1-v6-v7-v2 left
            xmin, ymax, zmax,
            xmin, ymax, zmin,
            xmin, ymin, zmin,
            xmin, ymin, zmax,

            // v7-v4-v3-v2 bottom
            xmin, ymin, zmin,
            xmax, ymin, zmin,
            xmax, ymin, zmax,
            xmin, ymin, zmax,

            // v4-v7-v6-v1 back
            xmax, ymin, zmin,
            xmin, ymin, zmin,
            xmin, ymax, zmin,
            xmax, ymax, zmin
        ],

        // Normal vectors, one for each vertex
        normals: [

            // v0-v1-v2-v3 front
            0, 0, 1,
            0, 0, 1,
            0, 0, 1,
            0, 0, 1,

            // v0-v3-v4-v5 right
            1, 0, 0,
            1, 0, 0,
            1, 0, 0,
            1, 0, 0,

            // v0-v5-v6-v1 top
            0, 1, 0,
            0, 1, 0,
            0, 1, 0,
            0, 1, 0,

            // v1-v6-v7-v2 left
            -1, 0, 0,
            -1, 0, 0,
            -1, 0, 0,
            -1, 0, 0,

            // v7-v4-v3-v2 bottom
            0, -1, 0,
            0, -1, 0,
            0, -1, 0,
            0, -1, 0,

            // v4-v7-v6-v5 back
            0, 0, -1,
            0, 0, -1,
            0, 0, -1,
            0, 0, -1
        ],

        // UV coords
        uv: [

            // v0-v1-v2-v3 front
            1, 0,
            0, 0,
            0, 1,
            1, 1,

            // v0-v3-v4-v1 right
            0, 0,
            0, 1,
            1, 1,
            1, 0,

            // v0-v1-v6-v1 top
            1, 1,
            1, 0,
            0, 0,
            0, 1,

            // v1-v6-v7-v2 left
            1, 0,
            0, 0,
            0, 1,
            1, 1,

            // v7-v4-v3-v2 bottom
            0, 1,
            1, 1,
            1, 0,
            0, 0,

            // v4-v7-v6-v1 back
            0, 1,
            1, 1,
            1, 0,
            0, 0
        ],

        // Indices - these organise the
        // positions and uv texture coordinates
        // into geometric primitives in accordance
        // with the "primitive" parameter,
        // in this case a set of three indices
        // for each triangle.
        //
        // Note that each triangle is specified
        // in counter-clockwise winding order.
        //
        // You can specify them in clockwise
        // order if you configure the Modes
        // node's frontFace flag as "cw", instead of
        // the default "ccw".
        indices: [
            0, 1, 2,
            0, 2, 3,
            // front
            4, 5, 6,
            4, 6, 7,
            // right
            8, 9, 10,
            8, 10, 11,
            // top
            12, 13, 14,
            12, 14, 15,
            // left
            16, 17, 18,
            16, 18, 19,
            // bottom
            20, 21, 22,
            20, 22, 23
        ]
    });
}

/**
 * @desc Creates a cylinder-shaped {@link Geometry}.
 *
 * ## Usage
 *
 * Creating a {@link Mesh} with a cylinder-shaped {@link ReadableGeometry} :
 *
 * [[Run this example](/examples/index.html#geometry_builders_buildCylinderGeometry)]
 *
 * ````javascript
 *
 * import {Viewer, Mesh, buildCylinderGeometry, ReadableGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *      canvasId: "myCanvas"
 *  });
 *
 * viewer.camera.eye = [0, 0, 5];
 * viewer.camera.look = [0, 0, 0];
 * viewer.camera.up = [0, 1, 0];
 *
 * new Mesh(viewer.scene, {
 *      geometry: new ReadableGeometry(viewer.scene, buildCylinderGeometry({
 *          center: [0,0,0],
 *          radiusTop: 2.0,
 *          radiusBottom: 2.0,
 *          height: 5.0,
 *          radialSegments: 20,
 *          heightSegments: 1,
 *          openEnded: false
 *      }),
 *      material: new PhongMaterial(viewer.scene, {
 *         diffuseMap: new Texture(viewer.scene, {
 *             src: "textures/diffuse/uvGrid2.jpg"
 *         })
 *      })
 * });
 * ````
 *
 * @function buildCylinderGeometry
 * @param {*} [cfg] Configs
 * @param {String} [cfg.id] Optional ID for the {@link Geometry}, unique among all components in the parent {@link Scene}, generated automatically when omitted.
 * @param {Number[]} [cfg.center]  3D point indicating the center position.
 * @param {Number} [cfg.radiusTop=1]  Radius of top.
 * @param {Number} [cfg.radiusBottom=1]  Radius of bottom.
 * @param {Number} [cfg.height=1] Height.
 * @param {Number} [cfg.radialSegments=60]  Number of horizontal segments.
 * @param {Number} [cfg.heightSegments=1]  Number of vertical segments.
 * @param {Boolean} [cfg.openEnded=false]  Whether or not the cylinder has solid caps on the ends.
 * @returns {Object} Configuration for a {@link Geometry} subtype.
 */
function buildCylinderGeometry(cfg = {}) {

    let radiusTop = cfg.radiusTop || 1;
    if (radiusTop < 0) {
        console.error("negative radiusTop not allowed - will invert");
        radiusTop *= -1;
    }

    let radiusBottom = cfg.radiusBottom || 1;
    if (radiusBottom < 0) {
        console.error("negative radiusBottom not allowed - will invert");
        radiusBottom *= -1;
    }

    let height = cfg.height || 1;
    if (height < 0) {
        console.error("negative height not allowed - will invert");
        height *= -1;
    }

    let radialSegments = cfg.radialSegments || 32;
    if (radialSegments < 0) {
        console.error("negative radialSegments not allowed - will invert");
        radialSegments *= -1;
    }
    if (radialSegments < 3) {
        radialSegments = 3;
    }

    let heightSegments = cfg.heightSegments || 1;
    if (heightSegments < 0) {
        console.error("negative heightSegments not allowed - will invert");
        heightSegments *= -1;
    }
    if (heightSegments < 1) {
        heightSegments = 1;
    }

    const openEnded = !!cfg.openEnded;

    let center = cfg.center;
    const centerX = center ? center[0] : 0;
    const centerY = center ? center[1] : 0;
    const centerZ = center ? center[2] : 0;

    const heightHalf = height / 2;
    const heightLength = height / heightSegments;
    const radialAngle = (2.0 * Math.PI / radialSegments);
    const radialLength = 1.0 / radialSegments;
    //var nextRadius = this._radiusBottom;
    const radiusChange = (radiusTop - radiusBottom) / heightSegments;

    const positions = [];
    const normals = [];
    const uvs = [];
    const indices = [];

    let h;
    let i;

    let x;
    let z;

    let currentRadius;
    let currentHeight;

    let first;
    let second;

    let startIndex;
    let tu;
    let tv;

    // create vertices
    const normalY = (90.0 - (Math.atan(height / (radiusBottom - radiusTop))) * 180 / Math.PI) / 90.0;

    for (h = 0; h <= heightSegments; h++) {
        currentRadius = radiusTop - h * radiusChange;
        currentHeight = heightHalf - h * heightLength;

        for (i = 0; i <= radialSegments; i++) {
            x = Math.sin(i * radialAngle);
            z = Math.cos(i * radialAngle);

            normals.push(currentRadius * x);
            normals.push(normalY); //todo
            normals.push(currentRadius * z);

            uvs.push((i * radialLength));
            uvs.push(h * 1 / heightSegments);

            positions.push((currentRadius * x) + centerX);
            positions.push((currentHeight) + centerY);
            positions.push((currentRadius * z) + centerZ);
        }
    }

    // create faces
    for (h = 0; h < heightSegments; h++) {
        for (i = 0; i <= radialSegments; i++) {

            first = h * (radialSegments + 1) + i;
            second = first + radialSegments;

            indices.push(first);
            indices.push(second);
            indices.push(second + 1);

            indices.push(first);
            indices.push(second + 1);
            indices.push(first + 1);
        }
    }

    // create top cap
    if (!openEnded && radiusTop > 0) {
        startIndex = (positions.length / 3);

        // top center
        normals.push(0.0);
        normals.push(1.0);
        normals.push(0.0);

        uvs.push(0.5);
        uvs.push(0.5);

        positions.push(0 + centerX);
        positions.push(heightHalf + centerY);
        positions.push(0 + centerZ);

        // top triangle fan
        for (i = 0; i <= radialSegments; i++) {
            x = Math.sin(i * radialAngle);
            z = Math.cos(i * radialAngle);
            tu = (0.5 * Math.sin(i * radialAngle)) + 0.5;
            tv = (0.5 * Math.cos(i * radialAngle)) + 0.5;

            normals.push(radiusTop * x);
            normals.push(1.0);
            normals.push(radiusTop * z);

            uvs.push(tu);
            uvs.push(tv);

            positions.push((radiusTop * x) + centerX);
            positions.push((heightHalf) + centerY);
            positions.push((radiusTop * z) + centerZ);
        }

        for (i = 0; i < radialSegments; i++) {
            center = startIndex;
            first = startIndex + 1 + i;

            indices.push(first);
            indices.push(first + 1);
            indices.push(center);
        }
    }

    // create bottom cap
    if (!openEnded && radiusBottom > 0) {

        startIndex = (positions.length / 3);

        // top center
        normals.push(0.0);
        normals.push(-1);
        normals.push(0.0);

        uvs.push(0.5);
        uvs.push(0.5);

        positions.push(0 + centerX);
        positions.push(0 - heightHalf + centerY);
        positions.push(0 + centerZ);

        // top triangle fan
        for (i = 0; i <= radialSegments; i++) {

            x = Math.sin(i * radialAngle);
            z = Math.cos(i * radialAngle);

            tu = (0.5 * Math.sin(i * radialAngle)) + 0.5;
            tv = (0.5 * Math.cos(i * radialAngle)) + 0.5;

            normals.push(radiusBottom * x);
            normals.push(-1);
            normals.push(radiusBottom * z);

            uvs.push(tu);
            uvs.push(tv);

            positions.push((radiusBottom * x) + centerX);
            positions.push((0 - heightHalf) + centerY);
            positions.push((radiusBottom * z) + centerZ);
        }

        for (i = 0; i < radialSegments; i++) {

            center = startIndex;
            first = startIndex + 1 + i;

            indices.push(center);
            indices.push(first + 1);
            indices.push(first);
        }
    }

    return utils.apply(cfg, {
        positions: positions,
        normals: normals,
        uv: uvs,
        indices: indices
    });
}

/**
 * @desc Creates a plane-shaped {@link Geometry}.
 *
 * ## Usage
 *
 * Creating a {@link Mesh} with a PlaneGeometry and a {@link PhongMaterial} with diffuse {@link Texture}:
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#buildPlaneGeometry)]
 *
 * ````javascript
 * import {Viewer, Mesh, buildPlaneGeometry, ReadableGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *      canvasId: "myCanvas"
 * });
 *
 * viewer.camera.eye = [0, 0, 5];
 * viewer.camera.look = [0, 0, 0];
 * viewer.camera.up = [0, 1, 0];

 * new Mesh(viewer.scene, {
 *      geometry: new ReadableGeometry(viewer.scene, buildPlaneGeometry({
 *          center: [0,0,0],
 *          xSize: 2,
 *          zSize: 2,
 *          xSegments: 10,
 *          zSegments: 10
 *      }),
 *      material: new PhongMaterial(viewer.scene, {
 *          diffuseMap: new Texture(viewer.scene, {
 *              src: "textures/diffuse/uvGrid2.jpg"
 *          })
 *      })
 *  });
 * ````
 *
 * @function buildPlaneGeometry
 * @param {*} [cfg] Configs
 * @param {Number[]} [cfg.center]  3D point indicating the center position.
 * @param {String} [cfg.id] Optional ID for the {@link Geometry}, unique among all components in the parent {@link Scene}, generated automatically when omitted.
 * @param {Number} [cfg.xSize=1] Dimension on the X-axis.
 * @param {Number} [cfg.zSize=1] Dimension on the Z-axis.
 * @param {Number} [cfg.xSegments=1] Number of segments on the X-axis.
 * @param {Number} [cfg.zSegments=1] Number of segments on the Z-axis.
 * @returns {Object} Configuration for a {@link Geometry} subtype.
 */
function buildPlaneGeometry(cfg = {}) {

    let xSize = cfg.xSize || 1;
    if (xSize < 0) {
        console.error("negative xSize not allowed - will invert");
        xSize *= -1;
    }

    let zSize = cfg.zSize || 1;
    if (zSize < 0) {
        console.error("negative zSize not allowed - will invert");
        zSize *= -1;
    }

    let xSegments = cfg.xSegments || 1;
    if (xSegments < 0) {
        console.error("negative xSegments not allowed - will invert");
        xSegments *= -1;
    }
    if (xSegments < 1) {
        xSegments = 1;
    }

    let zSegments = cfg.xSegments || 1;
    if (zSegments < 0) {
        console.error("negative zSegments not allowed - will invert");
        zSegments *= -1;
    }
    if (zSegments < 1) {
        zSegments = 1;
    }

    const center = cfg.center;
    const centerX = center ? center[0] : 0;
    const centerY = center ? center[1] : 0;
    const centerZ = center ? center[2] : 0;

    const halfWidth = xSize / 2;
    const halfHeight = zSize / 2;

    const planeX = Math.floor(xSegments) || 1;
    const planeZ = Math.floor(zSegments) || 1;

    const planeX1 = planeX + 1;
    const planeZ1 = planeZ + 1;

    const segmentWidth = xSize / planeX;
    const segmentHeight = zSize / planeZ;

    const positions = new Float32Array(planeX1 * planeZ1 * 3);
    const normals = new Float32Array(planeX1 * planeZ1 * 3);
    const uvs = new Float32Array(planeX1 * planeZ1 * 2);

    let offset = 0;
    let offset2 = 0;

    let iz;
    let ix;
    let x;
    let a;
    let b;
    let c;
    let d;

    for (iz = 0; iz < planeZ1; iz++) {

        const z = iz * segmentHeight - halfHeight;

        for (ix = 0; ix < planeX1; ix++) {

            x = ix * segmentWidth - halfWidth;

            positions[offset] = x + centerX;
            positions[offset + 1] = centerY;
            positions[offset + 2] = -z + centerZ;

            normals[offset + 1] = 1;

            uvs[offset2] = (ix) / planeX;
            uvs[offset2 + 1] = ((planeZ - iz) / planeZ);

            offset += 3;
            offset2 += 2;
        }
    }

    offset = 0;

    const indices = new ((positions.length / 3) > 65535 ? Uint32Array : Uint16Array)(planeX * planeZ * 6);

    for (iz = 0; iz < planeZ; iz++) {

        for (ix = 0; ix < planeX; ix++) {

            a = ix + planeX1 * iz;
            b = ix + planeX1 * (iz + 1);
            c = (ix + 1) + planeX1 * (iz + 1);
            d = (ix + 1) + planeX1 * iz;

            indices[offset] = d;
            indices[offset + 1] = b;
            indices[offset + 2] = a;

            indices[offset + 3] = d;
            indices[offset + 4] = c;
            indices[offset + 5] = b;

            offset += 6;
        }
    }

    return utils.apply(cfg, {
        positions: positions,
        normals: normals,
        uv: uvs,
        indices: indices
    });
}

/**
 * @desc Creates a sphere-shaped {@link Geometry}.
 *
 * ## Usage
 *
 * Creating a {@link Mesh} with a sphere-shaped {@link ReadableGeometry} :
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#buildSphereGeometry)]
 *
 * ````javascript
 * import {Viewer, Mesh, buildSphereGeometry, ReadableGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas"
 * });
 *
 * viewer.camera.eye = [0, 0, 5];
 * viewer.camera.look = [0, 0, 0];
 * viewer.camera.up = [0, 1, 0];
 *
 * new Mesh(viewer.scene, {
 *      geometry: new ReadableGeometry(viewer.scene, buildSphereGeometry({
 *          center: [0,0,0],
 *          radius: 1.5,
 *          heightSegments: 60,
 *          widthSegments: 60
 *      }),
 *      material: new PhongMaterial(viewer.scene, {
 *         diffuseMap: new Texture(viewer.scene, {
 *             src: "textures/diffuse/uvGrid2.jpg"
 *         })
 *      })
 * });
 * ````
 *
 * @function buildSphereGeometry
 * @param {*} [cfg] Configs
 * @param {String} [cfg.id] Optional ID for the {@link Geometry}, unique among all components in the parent {@link Scene}, generated automatically when omitted.
 * @param {Number[]} [cfg.center]  3D point indicating the center position.
 * @param {Number} [cfg.radius=1]  Radius.
 * @param {Number} [cfg.heightSegments=24] Number of latitudinal bands.
 * @param  {Number} [cfg.widthSegments=18] Number of longitudinal bands.
 * @returns {Object} Configuration for a {@link Geometry} subtype.
 */
function buildSphereGeometry(cfg = {}) {

    const lod = cfg.lod || 1;

    const centerX = cfg.center ? cfg.center[0] : 0;
    const centerY = cfg.center ? cfg.center[1] : 0;
    const centerZ = cfg.center ? cfg.center[2] : 0;

    let radius = cfg.radius || 1;
    if (radius < 0) {
        console.error("negative radius not allowed - will invert");
        radius *= -1;
    }

    let heightSegments = cfg.heightSegments || 18;
    if (heightSegments < 0) {
        console.error("negative heightSegments not allowed - will invert");
        heightSegments *= -1;
    }
    heightSegments = Math.floor(lod * heightSegments);
    if (heightSegments < 18) {
        heightSegments = 18;
    }

    let widthSegments = cfg.widthSegments || 18;
    if (widthSegments < 0) {
        console.error("negative widthSegments not allowed - will invert");
        widthSegments *= -1;
    }
    widthSegments = Math.floor(lod * widthSegments);
    if (widthSegments < 18) {
        widthSegments = 18;
    }

    const positions = [];
    const normals = [];
    const uvs = [];
    const indices = [];

    let i;
    let j;

    let theta;
    let sinTheta;
    let cosTheta;

    let phi;
    let sinPhi;
    let cosPhi;

    let x;
    let y;
    let z;

    let u;
    let v;

    let first;
    let second;

    for (i = 0; i <= heightSegments; i++) {

        theta = i * Math.PI / heightSegments;
        sinTheta = Math.sin(theta);
        cosTheta = Math.cos(theta);

        for (j = 0; j <= widthSegments; j++) {

            phi = j * 2 * Math.PI / widthSegments;
            sinPhi = Math.sin(phi);
            cosPhi = Math.cos(phi);

            x = cosPhi * sinTheta;
            y = cosTheta;
            z = sinPhi * sinTheta;
            u = 1.0 - j / widthSegments;
            v = i / heightSegments;

            normals.push(x);
            normals.push(y);
            normals.push(z);

            uvs.push(u);
            uvs.push(v);

            positions.push(centerX + radius * x);
            positions.push(centerY + radius * y);
            positions.push(centerZ + radius * z);
        }
    }

    for (i = 0; i < heightSegments; i++) {
        for (j = 0; j < widthSegments; j++) {

            first = (i * (widthSegments + 1)) + j;
            second = first + widthSegments + 1;

            indices.push(first + 1);
            indices.push(second + 1);
            indices.push(second);
            indices.push(first + 1);
            indices.push(second);
            indices.push(first);
        }
    }

    return utils.apply(cfg, {
        positions: positions,
        normals: normals,
        uv: uvs,
        indices: indices
    });
}

/**
 * @desc Creates a torus-shaped {@link Geometry}.
 *
 * ## Usage
 * Creating a {@link Mesh} with a torus-shaped {@link ReadableGeometry} :
 *
 * [[Run this example](/examples/index.html#geometry_builders_buildTorusGeometry)]
 * 
 * ````javascript
 * import {Viewer, Mesh, buildTorusGeometry, ReadableGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *      canvasId: "myCanvas"
 * });
 *
 * viewer.camera.eye = [0, 0, 5];
 * viewer.camera.look = [0, 0, 0];
 * viewer.camera.up = [0, 1, 0];
 *
 * new Mesh(viewer.scene, {
 *      geometry: new ReadableGeometry(viewer.scene, buildTorusGeometry({
 *          center: [0,0,0],
 *          radius: 1.0,
 *          tube: 0.5,
 *          radialSegments: 32,
 *          tubeSegments: 24,
 *          arc: Math.PI * 2.0
 *      }),
 *      material: new PhongMaterial(viewer.scene, {
 *         diffuseMap: new Texture(viewer.scene, {
 *             src: "textures/diffuse/uvGrid2.jpg"
 *         })
 *      })
 * });
 * ````
 *
 * @function buildTorusGeometry
 * @param {*} [cfg] Configs
 * @param {String} [cfg.id] Optional ID for the {@link Geometry}, unique among all components in the parent {@link Scene}, generated automatically when omitted.
 * @param {Number[]} [cfg.center] 3D point indicating the center position.
 * @param {Number} [cfg.radius=1] The overall radius.
 * @param {Number} [cfg.tube=0.3] The tube radius.
 * @param {Number} [cfg.radialSegments=32] The number of radial segments.
 * @param {Number} [cfg.tubeSegments=24] The number of tubular segments.
 * @param {Number} [cfg.arc=Math.PI*0.5] The length of the arc in radians, where Math.PI*2 is a closed torus.
 * @returns {Object} Configuration for a {@link Geometry} subtype.
 */
function buildTorusGeometry(cfg = {}) {

    let radius = cfg.radius || 1;
    if (radius < 0) {
        console.error("negative radius not allowed - will invert");
        radius *= -1;
    }
    radius *= 0.5;

    let tube = cfg.tube || 0.3;
    if (tube < 0) {
        console.error("negative tube not allowed - will invert");
        tube *= -1;
    }

    let radialSegments = cfg.radialSegments || 32;
    if (radialSegments < 0) {
        console.error("negative radialSegments not allowed - will invert");
        radialSegments *= -1;
    }
    if (radialSegments < 4) {
        radialSegments = 4;
    }

    let tubeSegments = cfg.tubeSegments || 24;
    if (tubeSegments < 0) {
        console.error("negative tubeSegments not allowed - will invert");
        tubeSegments *= -1;
    }
    if (tubeSegments < 4) {
        tubeSegments = 4;
    }

    let arc = cfg.arc || Math.PI * 2;
    if (arc < 0) {
        console.warn("negative arc not allowed - will invert");
        arc *= -1;
    }
    if (arc > 360) {
        arc = 360;
    }

    const center = cfg.center;
    let centerX = center ? center[0] : 0;
    let centerY = center ? center[1] : 0;
    const centerZ = center ? center[2] : 0;

    const positions = [];
    const normals = [];
    const uvs = [];
    const indices = [];

    let u;
    let v;
    let x;
    let y;
    let z;
    let vec;

    let i;
    let j;

    for (j = 0; j <= tubeSegments; j++) {
        for (i = 0; i <= radialSegments; i++) {

            u = i / radialSegments * arc;
            v = 0.785398 + (j / tubeSegments * Math.PI * 2);

            centerX = radius * Math.cos(u);
            centerY = radius * Math.sin(u);

            x = (radius + tube * Math.cos(v)) * Math.cos(u);
            y = (radius + tube * Math.cos(v)) * Math.sin(u);
            z = tube * Math.sin(v);

            positions.push(x + centerX);
            positions.push(y + centerY);
            positions.push(z + centerZ);

            uvs.push(1 - (i / radialSegments));
            uvs.push((j / tubeSegments));

            vec = math.normalizeVec3(math.subVec3([x, y, z], [centerX, centerY, centerZ], []), []);

            normals.push(vec[0]);
            normals.push(vec[1]);
            normals.push(vec[2]);
        }
    }

    let a;
    let b;
    let c;
    let d;

    for (j = 1; j <= tubeSegments; j++) {
        for (i = 1; i <= radialSegments; i++) {

            a = (radialSegments + 1) * j + i - 1;
            b = (radialSegments + 1) * (j - 1) + i - 1;
            c = (radialSegments + 1) * (j - 1) + i;
            d = (radialSegments + 1) * j + i;

            indices.push(a);
            indices.push(b);
            indices.push(c);

            indices.push(c);
            indices.push(d);
            indices.push(a);
        }
    }

    return utils.apply(cfg, {
        positions: positions,
        normals: normals,
        uv: uvs,
        indices: indices
    });
}

const letters = {
    ' ': {width: 16, points: []},
    '!': {
        width: 10, points: [
            [5, 21],
            [5, 7],
            [-1, -1],
            [5, 2],
            [4, 1],
            [5, 0],
            [6, 1],
            [5, 2]
        ]
    },
    '"': {
        width: 16, points: [
            [4, 21],
            [4, 14],
            [-1, -1],
            [12, 21],
            [12, 14]
        ]
    },
    '#': {
        width: 21, points: [
            [11, 25],
            [4, -7],
            [-1, -1],
            [17, 25],
            [10, -7],
            [-1, -1],
            [4, 12],
            [18, 12],
            [-1, -1],
            [3, 6],
            [17, 6]
        ]
    },
    '$': {
        width: 20, points: [
            [8, 25],
            [8, -4],
            [-1, -1],
            [12, 25],
            [12, -4],
            [-1, -1],
            [17, 18],
            [15, 20],
            [12, 21],
            [8, 21],
            [5, 20],
            [3, 18],
            [3, 16],
            [4, 14],
            [5, 13],
            [7, 12],
            [13, 10],
            [15, 9],
            [16, 8],
            [17, 6],
            [17, 3],
            [15, 1],
            [12, 0],
            [8, 0],
            [5, 1],
            [3, 3]
        ]
    },
    '%': {
        width: 24, points: [
            [21, 21],
            [3, 0],
            [-1, -1],
            [8, 21],
            [10, 19],
            [10, 17],
            [9, 15],
            [7, 14],
            [5, 14],
            [3, 16],
            [3, 18],
            [4, 20],
            [6, 21],
            [8, 21],
            [10, 20],
            [13, 19],
            [16, 19],
            [19, 20],
            [21, 21],
            [-1, -1],
            [17, 7],
            [15, 6],
            [14, 4],
            [14, 2],
            [16, 0],
            [18, 0],
            [20, 1],
            [21, 3],
            [21, 5],
            [19, 7],
            [17, 7]
        ]
    },
    '&': {
        width: 26, points: [
            [23, 12],
            [23, 13],
            [22, 14],
            [21, 14],
            [20, 13],
            [19, 11],
            [17, 6],
            [15, 3],
            [13, 1],
            [11, 0],
            [7, 0],
            [5, 1],
            [4, 2],
            [3, 4],
            [3, 6],
            [4, 8],
            [5, 9],
            [12, 13],
            [13, 14],
            [14, 16],
            [14, 18],
            [13, 20],
            [11, 21],
            [9, 20],
            [8, 18],
            [8, 16],
            [9, 13],
            [11, 10],
            [16, 3],
            [18, 1],
            [20, 0],
            [22, 0],
            [23, 1],
            [23, 2]
        ]
    },
    '\'': {
        width: 10, points: [
            [5, 19],
            [4, 20],
            [5, 21],
            [6, 20],
            [6, 18],
            [5, 16],
            [4, 15]
        ]
    },
    '(': {
        width: 14, points: [
            [11, 25],
            [9, 23],
            [7, 20],
            [5, 16],
            [4, 11],
            [4, 7],
            [5, 2],
            [7, -2],
            [9, -5],
            [11, -7]
        ]
    },
    ')': {
        width: 14, points: [
            [3, 25],
            [5, 23],
            [7, 20],
            [9, 16],
            [10, 11],
            [10, 7],
            [9, 2],
            [7, -2],
            [5, -5],
            [3, -7]
        ]
    },
    '*': {
        width: 16, points: [
            [8, 21],
            [8, 9],
            [-1, -1],
            [3, 18],
            [13, 12],
            [-1, -1],
            [13, 18],
            [3, 12]
        ]
    },
    '+': {
        width: 26, points: [
            [13, 18],
            [13, 0],
            [-1, -1],
            [4, 9],
            [22, 9]
        ]
    },
    ',': {
        width: 10, points: [
            [6, 1],
            [5, 0],
            [4, 1],
            [5, 2],
            [6, 1],
            [6, -1],
            [5, -3],
            [4, -4]
        ]
    },
    '-': {
        width: 26, points: [
            [4, 9],
            [22, 9]
        ]
    },
    '.': {
        width: 10, points: [
            [5, 2],
            [4, 1],
            [5, 0],
            [6, 1],
            [5, 2]
        ]
    },
    '/': {
        width: 22, points: [
            [20, 25],
            [2, -7]
        ]
    },
    '0': {
        width: 20, points: [
            [9, 21],
            [6, 20],
            [4, 17],
            [3, 12],
            [3, 9],
            [4, 4],
            [6, 1],
            [9, 0],
            [11, 0],
            [14, 1],
            [16, 4],
            [17, 9],
            [17, 12],
            [16, 17],
            [14, 20],
            [11, 21],
            [9, 21]
        ]
    },
    '1': {
        width: 20, points: [
            [6, 17],
            [8, 18],
            [11, 21],
            [11, 0]
        ]
    },
    '2': {
        width: 20, points: [
            [4, 16],
            [4, 17],
            [5, 19],
            [6, 20],
            [8, 21],
            [12, 21],
            [14, 20],
            [15, 19],
            [16, 17],
            [16, 15],
            [15, 13],
            [13, 10],
            [3, 0],
            [17, 0]
        ]
    },
    '3': {
        width: 20, points: [
            [5, 21],
            [16, 21],
            [10, 13],
            [13, 13],
            [15, 12],
            [16, 11],
            [17, 8],
            [17, 6],
            [16, 3],
            [14, 1],
            [11, 0],
            [8, 0],
            [5, 1],
            [4, 2],
            [3, 4]
        ]
    },
    '4': {
        width: 20, points: [
            [13, 21],
            [3, 7],
            [18, 7],
            [-1, -1],
            [13, 21],
            [13, 0]
        ]
    },
    '5': {
        width: 20, points: [
            [15, 21],
            [5, 21],
            [4, 12],
            [5, 13],
            [8, 14],
            [11, 14],
            [14, 13],
            [16, 11],
            [17, 8],
            [17, 6],
            [16, 3],
            [14, 1],
            [11, 0],
            [8, 0],
            [5, 1],
            [4, 2],
            [3, 4]
        ]
    },
    '6': {
        width: 20, points: [
            [16, 18],
            [15, 20],
            [12, 21],
            [10, 21],
            [7, 20],
            [5, 17],
            [4, 12],
            [4, 7],
            [5, 3],
            [7, 1],
            [10, 0],
            [11, 0],
            [14, 1],
            [16, 3],
            [17, 6],
            [17, 7],
            [16, 10],
            [14, 12],
            [11, 13],
            [10, 13],
            [7, 12],
            [5, 10],
            [4, 7]
        ]
    },
    '7': {
        width: 20, points: [
            [17, 21],
            [7, 0],
            [-1, -1],
            [3, 21],
            [17, 21]
        ]
    },
    '8': {
        width: 20, points: [
            [8, 21],
            [5, 20],
            [4, 18],
            [4, 16],
            [5, 14],
            [7, 13],
            [11, 12],
            [14, 11],
            [16, 9],
            [17, 7],
            [17, 4],
            [16, 2],
            [15, 1],
            [12, 0],
            [8, 0],
            [5, 1],
            [4, 2],
            [3, 4],
            [3, 7],
            [4, 9],
            [6, 11],
            [9, 12],
            [13, 13],
            [15, 14],
            [16, 16],
            [16, 18],
            [15, 20],
            [12, 21],
            [8, 21]
        ]
    },
    '9': {
        width: 20, points: [
            [16, 14],
            [15, 11],
            [13, 9],
            [10, 8],
            [9, 8],
            [6, 9],
            [4, 11],
            [3, 14],
            [3, 15],
            [4, 18],
            [6, 20],
            [9, 21],
            [10, 21],
            [13, 20],
            [15, 18],
            [16, 14],
            [16, 9],
            [15, 4],
            [13, 1],
            [10, 0],
            [8, 0],
            [5, 1],
            [4, 3]
        ]
    },
    ':': {
        width: 10, points: [
            [5, 14],
            [4, 13],
            [5, 12],
            [6, 13],
            [5, 14],
            [-1, -1],
            [5, 2],
            [4, 1],
            [5, 0],
            [6, 1],
            [5, 2]
        ]
    },
    ';': {
        width: 10, points: [
            [5, 14],
            [4, 13],
            [5, 12],
            [6, 13],
            [5, 14],
            [-1, -1],
            [6, 1],
            [5, 0],
            [4, 1],
            [5, 2],
            [6, 1],
            [6, -1],
            [5, -3],
            [4, -4]
        ]
    },
    '<': {
        width: 24, points: [
            [20, 18],
            [4, 9],
            [20, 0]
        ]
    },
    '=': {
        width: 26, points: [
            [4, 12],
            [22, 12],
            [-1, -1],
            [4, 6],
            [22, 6]
        ]
    },
    '>': {
        width: 24, points: [
            [4, 18],
            [20, 9],
            [4, 0]
        ]
    },
    '?': {
        width: 18, points: [
            [3, 16],
            [3, 17],
            [4, 19],
            [5, 20],
            [7, 21],
            [11, 21],
            [13, 20],
            [14, 19],
            [15, 17],
            [15, 15],
            [14, 13],
            [13, 12],
            [9, 10],
            [9, 7],
            [-1, -1],
            [9, 2],
            [8, 1],
            [9, 0],
            [10, 1],
            [9, 2]
        ]
    },
    '@': {
        width: 27, points: [
            [18, 13],
            [17, 15],
            [15, 16],
            [12, 16],
            [10, 15],
            [9, 14],
            [8, 11],
            [8, 8],
            [9, 6],
            [11, 5],
            [14, 5],
            [16, 6],
            [17, 8],
            [-1, -1],
            [12, 16],
            [10, 14],
            [9, 11],
            [9, 8],
            [10, 6],
            [11, 5],
            [-1, -1],
            [18, 16],
            [17, 8],
            [17, 6],
            [19, 5],
            [21, 5],
            [23, 7],
            [24, 10],
            [24, 12],
            [23, 15],
            [22, 17],
            [20, 19],
            [18, 20],
            [15, 21],
            [12, 21],
            [9, 20],
            [7, 19],
            [5, 17],
            [4, 15],
            [3, 12],
            [3, 9],
            [4, 6],
            [5, 4],
            [7, 2],
            [9, 1],
            [12, 0],
            [15, 0],
            [18, 1],
            [20, 2],
            [21, 3],
            [-1, -1],
            [19, 16],
            [18, 8],
            [18, 6],
            [19, 5]
        ]
    },
    'A': {
        width: 18, points: [
            [9, 21],
            [1, 0],
            [-1, -1],
            [9, 21],
            [17, 0],
            [-1, -1],
            [4, 7],
            [14, 7]
        ]
    },
    'B': {
        width: 21, points: [
            [4, 21],
            [4, 0],
            [-1, -1],
            [4, 21],
            [13, 21],
            [16, 20],
            [17, 19],
            [18, 17],
            [18, 15],
            [17, 13],
            [16, 12],
            [13, 11],
            [-1, -1],
            [4, 11],
            [13, 11],
            [16, 10],
            [17, 9],
            [18, 7],
            [18, 4],
            [17, 2],
            [16, 1],
            [13, 0],
            [4, 0]
        ]
    },
    'C': {
        width: 21, points: [
            [18, 16],
            [17, 18],
            [15, 20],
            [13, 21],
            [9, 21],
            [7, 20],
            [5, 18],
            [4, 16],
            [3, 13],
            [3, 8],
            [4, 5],
            [5, 3],
            [7, 1],
            [9, 0],
            [13, 0],
            [15, 1],
            [17, 3],
            [18, 5]
        ]
    },
    'D': {
        width: 21, points: [
            [4, 21],
            [4, 0],
            [-1, -1],
            [4, 21],
            [11, 21],
            [14, 20],
            [16, 18],
            [17, 16],
            [18, 13],
            [18, 8],
            [17, 5],
            [16, 3],
            [14, 1],
            [11, 0],
            [4, 0]
        ]
    },
    'E': {
        width: 19, points: [
            [4, 21],
            [4, 0],
            [-1, -1],
            [4, 21],
            [17, 21],
            [-1, -1],
            [4, 11],
            [12, 11],
            [-1, -1],
            [4, 0],
            [17, 0]
        ]
    },
    'F': {
        width: 18, points: [
            [4, 21],
            [4, 0],
            [-1, -1],
            [4, 21],
            [17, 21],
            [-1, -1],
            [4, 11],
            [12, 11]
        ]
    },
    'G': {
        width: 21, points: [
            [18, 16],
            [17, 18],
            [15, 20],
            [13, 21],
            [9, 21],
            [7, 20],
            [5, 18],
            [4, 16],
            [3, 13],
            [3, 8],
            [4, 5],
            [5, 3],
            [7, 1],
            [9, 0],
            [13, 0],
            [15, 1],
            [17, 3],
            [18, 5],
            [18, 8],
            [-1, -1],
            [13, 8],
            [18, 8]
        ]
    },
    'H': {
        width: 22, points: [
            [4, 21],
            [4, 0],
            [-1, -1],
            [18, 21],
            [18, 0],
            [-1, -1],
            [4, 11],
            [18, 11]
        ]
    },
    'I': {
        width: 8, points: [
            [4, 21],
            [4, 0]
        ]
    },
    'J': {
        width: 16, points: [
            [12, 21],
            [12, 5],
            [11, 2],
            [10, 1],
            [8, 0],
            [6, 0],
            [4, 1],
            [3, 2],
            [2, 5],
            [2, 7]
        ]
    },
    'K': {
        width: 21, points: [
            [4, 21],
            [4, 0],
            [-1, -1],
            [18, 21],
            [4, 7],
            [-1, -1],
            [9, 12],
            [18, 0]
        ]
    },
    'L': {
        width: 17, points: [
            [4, 21],
            [4, 0],
            [-1, -1],
            [4, 0],
            [16, 0]
        ]
    },
    'M': {
        width: 24, points: [
            [4, 21],
            [4, 0],
            [-1, -1],
            [4, 21],
            [12, 0],
            [-1, -1],
            [20, 21],
            [12, 0],
            [-1, -1],
            [20, 21],
            [20, 0]
        ]
    },
    'N': {
        width: 22, points: [
            [4, 21],
            [4, 0],
            [-1, -1],
            [4, 21],
            [18, 0],
            [-1, -1],
            [18, 21],
            [18, 0]
        ]
    },
    'O': {
        width: 22, points: [
            [9, 21],
            [7, 20],
            [5, 18],
            [4, 16],
            [3, 13],
            [3, 8],
            [4, 5],
            [5, 3],
            [7, 1],
            [9, 0],
            [13, 0],
            [15, 1],
            [17, 3],
            [18, 5],
            [19, 8],
            [19, 13],
            [18, 16],
            [17, 18],
            [15, 20],
            [13, 21],
            [9, 21]
        ]
    },
    'P': {
        width: 21, points: [
            [4, 21],
            [4, 0],
            [-1, -1],
            [4, 21],
            [13, 21],
            [16, 20],
            [17, 19],
            [18, 17],
            [18, 14],
            [17, 12],
            [16, 11],
            [13, 10],
            [4, 10]
        ]
    },
    'Q': {
        width: 22, points: [
            [9, 21],
            [7, 20],
            [5, 18],
            [4, 16],
            [3, 13],
            [3, 8],
            [4, 5],
            [5, 3],
            [7, 1],
            [9, 0],
            [13, 0],
            [15, 1],
            [17, 3],
            [18, 5],
            [19, 8],
            [19, 13],
            [18, 16],
            [17, 18],
            [15, 20],
            [13, 21],
            [9, 21],
            [-1, -1],
            [12, 4],
            [18, -2]
        ]
    },
    'R': {
        width: 21, points: [
            [4, 21],
            [4, 0],
            [-1, -1],
            [4, 21],
            [13, 21],
            [16, 20],
            [17, 19],
            [18, 17],
            [18, 15],
            [17, 13],
            [16, 12],
            [13, 11],
            [4, 11],
            [-1, -1],
            [11, 11],
            [18, 0]
        ]
    },
    'S': {
        width: 20, points: [
            [17, 18],
            [15, 20],
            [12, 21],
            [8, 21],
            [5, 20],
            [3, 18],
            [3, 16],
            [4, 14],
            [5, 13],
            [7, 12],
            [13, 10],
            [15, 9],
            [16, 8],
            [17, 6],
            [17, 3],
            [15, 1],
            [12, 0],
            [8, 0],
            [5, 1],
            [3, 3]
        ]
    },
    'T': {
        width: 16, points: [
            [8, 21],
            [8, 0],
            [-1, -1],
            [1, 21],
            [15, 21]
        ]
    },
    'U': {
        width: 22, points: [
            [4, 21],
            [4, 6],
            [5, 3],
            [7, 1],
            [10, 0],
            [12, 0],
            [15, 1],
            [17, 3],
            [18, 6],
            [18, 21]
        ]
    },
    'V': {
        width: 18, points: [
            [1, 21],
            [9, 0],
            [-1, -1],
            [17, 21],
            [9, 0]
        ]
    },
    'W': {
        width: 24, points: [
            [2, 21],
            [7, 0],
            [-1, -1],
            [12, 21],
            [7, 0],
            [-1, -1],
            [12, 21],
            [17, 0],
            [-1, -1],
            [22, 21],
            [17, 0]
        ]
    },
    'X': {
        width: 20, points: [
            [3, 21],
            [17, 0],
            [-1, -1],
            [17, 21],
            [3, 0]
        ]
    },
    'Y': {
        width: 18, points: [
            [1, 21],
            [9, 11],
            [9, 0],
            [-1, -1],
            [17, 21],
            [9, 11]
        ]
    },
    'Z': {
        width: 20, points: [
            [17, 21],
            [3, 0],
            [-1, -1],
            [3, 21],
            [17, 21],
            [-1, -1],
            [3, 0],
            [17, 0]
        ]
    },
    '[': {
        width: 14, points: [
            [4, 25],
            [4, -7],
            [-1, -1],
            [5, 25],
            [5, -7],
            [-1, -1],
            [4, 25],
            [11, 25],
            [-1, -1],
            [4, -7],
            [11, -7]
        ]
    },
    '\\': {
        width: 14, points: [
            [0, 21],
            [14, -3]
        ]
    },
    ']': {
        width: 14, points: [
            [9, 25],
            [9, -7],
            [-1, -1],
            [10, 25],
            [10, -7],
            [-1, -1],
            [3, 25],
            [10, 25],
            [-1, -1],
            [3, -7],
            [10, -7]
        ]
    },
    '^': {
        width: 16, points: [
            [6, 15],
            [8, 18],
            [10, 15],
            [-1, -1],
            [3, 12],
            [8, 17],
            [13, 12],
            [-1, -1],
            [8, 17],
            [8, 0]
        ]
    },
    '_': {
        width: 16, points: [
            [0, -2],
            [16, -2]
        ]
    },
    '`': {
        width: 10, points: [
            [6, 21],
            [5, 20],
            [4, 18],
            [4, 16],
            [5, 15],
            [6, 16],
            [5, 17]
        ]
    },
    'a': {
        width: 19, points: [
            [15, 14],
            [15, 0],
            [-1, -1],
            [15, 11],
            [13, 13],
            [11, 14],
            [8, 14],
            [6, 13],
            [4, 11],
            [3, 8],
            [3, 6],
            [4, 3],
            [6, 1],
            [8, 0],
            [11, 0],
            [13, 1],
            [15, 3]
        ]
    },
    'b': {
        width: 19, points: [
            [4, 21],
            [4, 0],
            [-1, -1],
            [4, 11],
            [6, 13],
            [8, 14],
            [11, 14],
            [13, 13],
            [15, 11],
            [16, 8],
            [16, 6],
            [15, 3],
            [13, 1],
            [11, 0],
            [8, 0],
            [6, 1],
            [4, 3]
        ]
    },
    'c': {
        width: 18, points: [
            [15, 11],
            [13, 13],
            [11, 14],
            [8, 14],
            [6, 13],
            [4, 11],
            [3, 8],
            [3, 6],
            [4, 3],
            [6, 1],
            [8, 0],
            [11, 0],
            [13, 1],
            [15, 3]
        ]
    },
    'd': {
        width: 19, points: [
            [15, 21],
            [15, 0],
            [-1, -1],
            [15, 11],
            [13, 13],
            [11, 14],
            [8, 14],
            [6, 13],
            [4, 11],
            [3, 8],
            [3, 6],
            [4, 3],
            [6, 1],
            [8, 0],
            [11, 0],
            [13, 1],
            [15, 3]
        ]
    },
    'e': {
        width: 18, points: [
            [3, 8],
            [15, 8],
            [15, 10],
            [14, 12],
            [13, 13],
            [11, 14],
            [8, 14],
            [6, 13],
            [4, 11],
            [3, 8],
            [3, 6],
            [4, 3],
            [6, 1],
            [8, 0],
            [11, 0],
            [13, 1],
            [15, 3]
        ]
    },
    'f': {
        width: 12, points: [
            [10, 21],
            [8, 21],
            [6, 20],
            [5, 17],
            [5, 0],
            [-1, -1],
            [2, 14],
            [9, 14]
        ]
    },
    'g': {
        width: 19, points: [
            [15, 14],
            [15, -2],
            [14, -5],
            [13, -6],
            [11, -7],
            [8, -7],
            [6, -6],
            [-1, -1],
            [15, 11],
            [13, 13],
            [11, 14],
            [8, 14],
            [6, 13],
            [4, 11],
            [3, 8],
            [3, 6],
            [4, 3],
            [6, 1],
            [8, 0],
            [11, 0],
            [13, 1],
            [15, 3]
        ]
    },
    'h': {
        width: 19, points: [
            [4, 21],
            [4, 0],
            [-1, -1],
            [4, 10],
            [7, 13],
            [9, 14],
            [12, 14],
            [14, 13],
            [15, 10],
            [15, 0]
        ]
    },
    'i': {
        width: 8, points: [
            [3, 21],
            [4, 20],
            [5, 21],
            [4, 22],
            [3, 21],
            [-1, -1],
            [4, 14],
            [4, 0]
        ]
    },
    'j': {
        width: 10, points: [
            [5, 21],
            [6, 20],
            [7, 21],
            [6, 22],
            [5, 21],
            [-1, -1],
            [6, 14],
            [6, -3],
            [5, -6],
            [3, -7],
            [1, -7]
        ]
    },
    'k': {
        width: 17, points: [
            [4, 21],
            [4, 0],
            [-1, -1],
            [14, 14],
            [4, 4],
            [-1, -1],
            [8, 8],
            [15, 0]
        ]
    },
    'l': {
        width: 8, points: [
            [4, 21],
            [4, 0]
        ]
    },
    'm': {
        width: 30, points: [
            [4, 14],
            [4, 0],
            [-1, -1],
            [4, 10],
            [7, 13],
            [9, 14],
            [12, 14],
            [14, 13],
            [15, 10],
            [15, 0],
            [-1, -1],
            [15, 10],
            [18, 13],
            [20, 14],
            [23, 14],
            [25, 13],
            [26, 10],
            [26, 0]
        ]
    },
    'n': {
        width: 19, points: [
            [4, 14],
            [4, 0],
            [-1, -1],
            [4, 10],
            [7, 13],
            [9, 14],
            [12, 14],
            [14, 13],
            [15, 10],
            [15, 0]
        ]
    },
    'o': {
        width: 19, points: [
            [8, 14],
            [6, 13],
            [4, 11],
            [3, 8],
            [3, 6],
            [4, 3],
            [6, 1],
            [8, 0],
            [11, 0],
            [13, 1],
            [15, 3],
            [16, 6],
            [16, 8],
            [15, 11],
            [13, 13],
            [11, 14],
            [8, 14]
        ]
    },
    'p': {
        width: 19, points: [
            [4, 14],
            [4, -7],
            [-1, -1],
            [4, 11],
            [6, 13],
            [8, 14],
            [11, 14],
            [13, 13],
            [15, 11],
            [16, 8],
            [16, 6],
            [15, 3],
            [13, 1],
            [11, 0],
            [8, 0],
            [6, 1],
            [4, 3]
        ]
    },
    'q': {
        width: 19, points: [
            [15, 14],
            [15, -7],
            [-1, -1],
            [15, 11],
            [13, 13],
            [11, 14],
            [8, 14],
            [6, 13],
            [4, 11],
            [3, 8],
            [3, 6],
            [4, 3],
            [6, 1],
            [8, 0],
            [11, 0],
            [13, 1],
            [15, 3]
        ]
    },
    'r': {
        width: 13, points: [
            [4, 14],
            [4, 0],
            [-1, -1],
            [4, 8],
            [5, 11],
            [7, 13],
            [9, 14],
            [12, 14]
        ]
    },
    's': {
        width: 17, points: [
            [14, 11],
            [13, 13],
            [10, 14],
            [7, 14],
            [4, 13],
            [3, 11],
            [4, 9],
            [6, 8],
            [11, 7],
            [13, 6],
            [14, 4],
            [14, 3],
            [13, 1],
            [10, 0],
            [7, 0],
            [4, 1],
            [3, 3]
        ]
    },
    't': {
        width: 12, points: [
            [5, 21],
            [5, 4],
            [6, 1],
            [8, 0],
            [10, 0],
            [-1, -1],
            [2, 14],
            [9, 14]
        ]
    },
    'u': {
        width: 19, points: [
            [4, 14],
            [4, 4],
            [5, 1],
            [7, 0],
            [10, 0],
            [12, 1],
            [15, 4],
            [-1, -1],
            [15, 14],
            [15, 0]
        ]
    },
    'v': {
        width: 16, points: [
            [2, 14],
            [8, 0],
            [-1, -1],
            [14, 14],
            [8, 0]
        ]
    },
    'w': {
        width: 22, points: [
            [3, 14],
            [7, 0],
            [-1, -1],
            [11, 14],
            [7, 0],
            [-1, -1],
            [11, 14],
            [15, 0],
            [-1, -1],
            [19, 14],
            [15, 0]
        ]
    },
    'x': {
        width: 17, points: [
            [3, 14],
            [14, 0],
            [-1, -1],
            [14, 14],
            [3, 0]
        ]
    },
    'y': {
        width: 16, points: [
            [2, 14],
            [8, 0],
            [-1, -1],
            [14, 14],
            [8, 0],
            [6, -4],
            [4, -6],
            [2, -7],
            [1, -7]
        ]
    },
    'z': {
        width: 17, points: [
            [14, 14],
            [3, 0],
            [-1, -1],
            [3, 14],
            [14, 14],
            [-1, -1],
            [3, 0],
            [14, 0]
        ]
    },
    '{': {
        width: 14, points: [
            [9, 25],
            [7, 24],
            [6, 23],
            [5, 21],
            [5, 19],
            [6, 17],
            [7, 16],
            [8, 14],
            [8, 12],
            [6, 10],
            [-1, -1],
            [7, 24],
            [6, 22],
            [6, 20],
            [7, 18],
            [8, 17],
            [9, 15],
            [9, 13],
            [8, 11],
            [4, 9],
            [8, 7],
            [9, 5],
            [9, 3],
            [8, 1],
            [7, 0],
            [6, -2],
            [6, -4],
            [7, -6],
            [-1, -1],
            [6, 8],
            [8, 6],
            [8, 4],
            [7, 2],
            [6, 1],
            [5, -1],
            [5, -3],
            [6, -5],
            [7, -6],
            [9, -7]
        ]
    },
    '|': {
        width: 8, points: [
            [4, 25],
            [4, -7]
        ]
    },
    '}': {
        width: 14, points: [
            [5, 25],
            [7, 24],
            [8, 23],
            [9, 21],
            [9, 19],
            [8, 17],
            [7, 16],
            [6, 14],
            [6, 12],
            [8, 10],
            [-1, -1],
            [7, 24],
            [8, 22],
            [8, 20],
            [7, 18],
            [6, 17],
            [5, 15],
            [5, 13],
            [6, 11],
            [10, 9],
            [6, 7],
            [5, 5],
            [5, 3],
            [6, 1],
            [7, 0],
            [8, -2],
            [8, -4],
            [7, -6],
            [-1, -1],
            [8, 8],
            [6, 6],
            [6, 4],
            [7, 2],
            [8, 1],
            [9, -1],
            [9, -3],
            [8, -5],
            [7, -6],
            [5, -7]
        ]
    },
    '~': {
        width: 24, points: [
            [3, 6],
            [3, 8],
            [4, 11],
            [6, 12],
            [8, 12],
            [10, 11],
            [14, 8],
            [16, 7],
            [18, 7],
            [20, 8],
            [21, 10],
            [-1, -1],
            [3, 8],
            [4, 10],
            [6, 11],
            [8, 11],
            [10, 10],
            [14, 7],
            [16, 6],
            [18, 6],
            [20, 7],
            [21, 10],
            [21, 12]
        ]
    }
};

/**
 * @desc Creates wireframe vector text {@link Geometry}.
 *
 * ## Usage
 *
 * Creating a {@link Mesh} with vector text {@link ReadableGeometry} :
 *
 * [[Run this example](/examples/index.html#geometry_builders_buildVectorTextGeometry)]
 *
 * ````javascript
 *
 * import {Viewer, Mesh, buildVectorTextGeometry, ReadableGeometry, PhongMaterial} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *      canvasId: "myCanvas"
 * });
 *
 * viewer.camera.eye = [0, 0, 100];
 * viewer.camera.look = [0, 0, 0];
 * viewer.camera.up = [0, 1, 0];
 *
 * new Mesh(viewer.scene, {
 *      geometry: new ReadableGeometry(viewer.scene, buildVectorTextGeometry({
 *          origin: [0,0,0],
 *          text: "On the other side of the screen, it all looked so easy"
 *      }),
 *      material: new PhongMaterial(viewer.scene, {
 *         diffuseMap: new Texture(viewer.scene, {
 *             src: "textures/diffuse/uvGrid2.jpg"
 *         })
 *      })
 * });
 * ````
 *
 * @function buildVectorTextGeometry
 * @param {*} [cfg] Configs
 * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted.
 * @param {Number[]} [cfg.center]  3D point indicating the center position.
 * @param {Number[]} [cfg.origin] 3D point indicating the top left corner.
 * @param {Number} [cfg.size=1] Size of each character.
 * @param {String} [cfg.text=""] The text.
 * @returns {Object} Configuration for a {@link Geometry} subtype.
 */
function buildVectorTextGeometry(cfg = {}) {

    var origin = cfg.origin || [0, 0, 0];
    var xOrigin = origin[0];
    var yOrigin = origin[1];
    var zOrigin = origin[2];
    var size = cfg.size || 1;

    var positions = [];
    var indices = [];
    var text = cfg.text;
    if (utils.isNumeric(text)) {
        text = "" + text;
    }
    var lines = (text || "").split("\n");
    var countVerts = 0;
    var y = 0;
    var x;
    var str;
    var len;
    var c;
    var mag = 1.0 / 25.0;
    var penUp;
    var p1;
    var p2;
    var pointsLen;
    var a;

    for (var iLine = 0; iLine < lines.length; iLine++) {

        x = 0;
        str = lines[iLine];
        len = str.length;

        for (var i = 0; i < len; i++) {

            c = letters[str.charAt(i)];

            if (!c) {
                continue;
            }

            penUp = 1;
            p1 = -1;
            p2 = -1;

            pointsLen = c.points.length;

            for (var j = 0; j < pointsLen; j++) {
                a = c.points[j];

                if (a[0] === -1 && a[1] === -1) {
                    penUp = 1;
                    continue;
                }

                positions.push((x + (a[0] * size) * mag) + xOrigin);
                positions.push((y + (a[1] * size) * mag) + yOrigin);
                positions.push(0 + zOrigin);

                if (p1 === -1) {
                    p1 = countVerts;
                } else if (p2 === -1) {
                    p2 = countVerts;
                } else {
                    p1 = p2;
                    p2 = countVerts;
                }
                countVerts++;

                if (penUp) {
                    penUp = false;

                } else {
                    indices.push(p1);
                    indices.push(p2);
                }
            }
            x += c.width * mag * size;

        }
        y -= 35 * mag * size;
    }

    return utils.apply(cfg, {
        primitive: "lines",
        positions: positions,
        indices: indices
    });
}

/**
 * @desc Creates a 3D line {@link Geometry}.
 *
 * ## Usage
 *
 * In the example below we'll create a {@link Mesh} with a line {@link ReadableGeometry}.
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#buildLineGeometry)]
 *
 * ````javascript
 * //------------------------------------------------------------------------------------------------------------------
 * // Import the modules we need for this example
 * //------------------------------------------------------------------------------------------------------------------
 *
 * import {buildLineGeometry, Viewer, Mesh, ReadableGeometry, PhongMaterial} from "../../dist/xeokit-sdk.min.es.js";
 *
 * //------------------------------------------------------------------------------------------------------------------
 * // Create a Viewer and arrange the camera
 * //------------------------------------------------------------------------------------------------------------------
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas"
 * });
 *
 * viewer.camera.eye = [0, 0, 8];
 * viewer.camera.look = [0, 0, 0];
 * viewer.camera.up = [0, 1, 0];
 *
 * //------------------------------------------------------------------------------------------------------------------
 * // Create a mesh with simple 2d line shape
 * //------------------------------------------------------------------------------------------------------------------
 *
 * new Mesh(viewer.scene, {
 *     geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({
 *         startPoint: [-5,-2,0],
 *         endPoint: [-5,2,0],
 *     })),
 *     material: new PhongMaterial(viewer.scene, {
 *         emissive: [0, 1,]
 *     })
 * });
 *
 * //------------------------------------------------------------------------------------------------------------------
 * // Create a mesh with simple 2d line shape with black color
 * //------------------------------------------------------------------------------------------------------------------
 *
 * new Mesh(viewer.scene, {
 *     geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({
 *         startPoint: [-4,-2,0],
 *         endPoint: [-4,2,0],
 *     })),
 *     material: new PhongMaterial(viewer.scene, {
 *         emissive: [0, 0, 0]
 *     })
 * });
 *
 * //------------------------------------------------------------------------------------------------------------------
 * // Create a mesh with simple 2d line shape with black color and simple pattern
 * //------------------------------------------------------------------------------------------------------------------
 *
 * new Mesh(viewer.scene, {
 *     geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({
 *         startPoint: [-3,-2,0],
 *         endPoint: [-3,2,0],
 *         pattern: [0.10],
 *     })),
 *     material: new PhongMaterial(viewer.scene, {
 *         emissive: [0, 0, 0]
 *     })
 * });
 *
 * //------------------------------------------------------------------------------------------------------------------
 * // Create a mesh with simple 2d line shape with blue color and simple pattern extended to end
 * //------------------------------------------------------------------------------------------------------------------
 *
 * new Mesh(viewer.scene, {
 *     geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({
 *         startPoint: [-2,-2,0],
 *         endPoint: [-2,2,0],
 *         pattern: [0.10],
 *         extendToEnd: true,
 *     })),
 *     material: new PhongMaterial(viewer.scene, {
 *         emissive: [0, 0, 1]
 *     })
 * });
 *
 * //------------------------------------------------------------------------------------------------------------------
 * // Create a mesh with simple 2d line shape with black color and more complex pattern
 * //------------------------------------------------------------------------------------------------------------------
 *
 * new Mesh(viewer.scene, {
 *     geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({
 *         startPoint: [-1,-2,0],
 *         endPoint: [-1,2,0],
 *         pattern: [0.15, 0.05],
 *     })),
 *     material: new PhongMaterial(viewer.scene, {
 *         emissive: [0, 0, 0]
 *     })
 * });
 *
 * //------------------------------------------------------------------------------------------------------------------
 * // Create a mesh with simple 2d line shape with blue color and more complex pattern extended to end
 * //------------------------------------------------------------------------------------------------------------------
 *
 * new Mesh(viewer.scene, {
 *     geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({
 *         startPoint: [0,-2,0],
 *         endPoint: [0,2,0],
 *         pattern: [0.15, 0.05],
 *         extendToEnd: true,
 *     })),
 *     material: new PhongMaterial(viewer.scene, {
 *         emissive: [0, 0, 1]
 *     })
 * });
 *
 * //------------------------------------------------------------------------------------------------------------------
 * // Create a mesh with simple 2d line shape with black color and complex pattern
 * //------------------------------------------------------------------------------------------------------------------
 *
 * new Mesh(viewer.scene, {
 *     geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({
 *         startPoint: [1,-2,0],
 *         endPoint: [1,2,0],
 *         pattern: [0.15, 0.05, 0.50],
 *     })),
 *     material: new PhongMaterial(viewer.scene, {
 *         emissive: [0, 0, 0]
 *     })
 * });
 *
 * //------------------------------------------------------------------------------------------------------------------
 * // Create a mesh with simple 2d line shape with blue color and complex pattern extended to end
 * //------------------------------------------------------------------------------------------------------------------
 *
 * new Mesh(viewer.scene, {
 *     geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({
 *         startPoint: [2,-2,0],
 *         endPoint: [2,2,0],
 *         pattern: [0.15, 0.05, 0.50],
 *         extendToEnd: true,
 *     })),
 *     material: new PhongMaterial(viewer.scene, {
 *         emissive: [0, 0, 1]
 *     })
 * });
 *
 * //------------------------------------------------------------------------------------------------------------------
 * // Create a mesh with simple 3d line shape with white color and simple pattern
 * //------------------------------------------------------------------------------------------------------------------
 *
 * new Mesh(viewer.scene, {
 *     geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({
 *         startPoint: [3,-2,-1],
 *         endPoint: [5,2,1],
 *         pattern: [0.10],
 *     })),
 *     material: new PhongMaterial(viewer.scene, {
 *         emissive: [1, 1, 1]
 *     })
 * });
 *
 * //------------------------------------------------------------------------------------------------------------------
 * // Create a mesh with simple 3d line shape with black color and simple dot pattern
 * //------------------------------------------------------------------------------------------------------------------
 *
 * new Mesh(viewer.scene, {
 *     geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({
 *         startPoint: [5,-2,-1],
 *         endPoint: [7,2,1],
 *         pattern: [0.03],
 *     })),
 *     material: new PhongMaterial(viewer.scene, {
 *         emissive: [0, 0, 0]
 *     })
 * });
 * ````
 *
 * @function buildLineGeometry
 * @param {*} [cfg] Configs
 * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted.
 * @param {Number[]} [cfg.startPoint]  3D start point (x0, y0, z0).
 * @param {Number[]} [cfg.endPoint]  3D end point (x1, y1, z1).
 * @param {Number[]} [cfg.pattern] Lengths of segments that describe a pattern.
 * @param {Bool} [cfg.extendToEnd] If true: it will try to make sure the line doesn't end up with a gap, as it will
 * extend the last segment.
 * @returns {Object} Configuration for a {@link Geometry} subtype.
 */
function buildLineGeometry(cfg = {}) {

    if (cfg.startPoint.length !== 3) {
        throw "Start point should contain 3 elements in array: x, y and z";
    }
    if (cfg.endPoint.length !== 3) {
        throw "End point should contain 3 elements in array: x, y and z";
    }
    let indices = [];
    let points = [];
    let x0 = cfg.startPoint[0]; let y0 = cfg.startPoint[1]; let z0 = cfg.startPoint[2];
    let x1 = cfg.endPoint[0]; let y1 = cfg.endPoint[1]; let z1 = cfg.endPoint[2];
    let lineLength = Math.sqrt((x1- x0)**2 + (y1 - y0)**2 + (z1 - z0)**2);
    let normalizedDirectionVectorOfLine = [(x1-x0)/lineLength, (y1-y0)/lineLength, (z1-z0)/lineLength];

    if (!cfg.pattern) {
        indices.push(0);
        indices.push(1);
        points.push(x0, y0, z0, x1, y1, z1);
    }
    else {
        let patternsNumber = cfg.pattern.length;
        let gap = false;
        let segmentFilled = 0.0;
        let idOfCurrentPatternLength = 0;
        let pointIndicesCounter = 0;
        let currentStartPoint = [x0, y0, z0];
        let currentPatternLength = cfg.pattern[idOfCurrentPatternLength];
        points.push(currentStartPoint[0], currentStartPoint[1], currentStartPoint[2]);

        while (currentPatternLength <= (lineLength - segmentFilled)) {
            let vectorFromCurrentStartPointToCurrentEndPoint = [
                normalizedDirectionVectorOfLine[0] * currentPatternLength,
                normalizedDirectionVectorOfLine[1] * currentPatternLength,
                normalizedDirectionVectorOfLine[2] * currentPatternLength,
            ];
            let currentEndPoint = [
                currentStartPoint[0] + vectorFromCurrentStartPointToCurrentEndPoint[0],
                currentStartPoint[1] + vectorFromCurrentStartPointToCurrentEndPoint[1],
                currentStartPoint[2] + vectorFromCurrentStartPointToCurrentEndPoint[2],
            ];

            points.push(currentEndPoint[0], currentEndPoint[1], currentEndPoint[2]);

            if (!gap) {
                indices.push(pointIndicesCounter);
                indices.push(pointIndicesCounter + 1);
            }
            gap = !gap;

            pointIndicesCounter += 1;
            currentStartPoint = currentEndPoint;
            idOfCurrentPatternLength += 1;
            if (idOfCurrentPatternLength >= patternsNumber) {
                idOfCurrentPatternLength = 0;
            }
            segmentFilled += currentPatternLength;
            currentPatternLength = cfg.pattern[idOfCurrentPatternLength];
        }

        if (cfg.extendToEnd) {
            points.push(x1, y1, z1);
            indices.push(indices.length - 2);
            indices.push(indices.length - 1);
        }
    }

    return utils.apply(cfg, {
        primitive: "lines",
        positions: points,
        indices: indices,
    });
}

const angleAxis$2 = math.vec4(4);
const q1$2 = math.vec4();
const q2$2 = math.vec4();
const xAxis$2 = math.vec3([1, 0, 0]);
const yAxis$2 = math.vec3([0, 1, 0]);
const zAxis$2 = math.vec3([0, 0, 1]);

const veca$1 = math.vec3(3);
const vecb$1 = math.vec3(3);

const identityMat$2 = math.identityMat4();

/**
 * @desc An {@link Entity} that is a scene graph node that can have child Nodes and {@link Mesh}es.
 *
 * ## Usage
 *
 * The example below is the same as the one given for {@link Mesh}, since the two classes work together. In this example,
 * we'll create a scene graph in which a root Node represents a group and the {@link Mesh}s are leaves. Since Node
 * implements {@link Entity}, we can designate the root Node as a model, causing it to be registered by its ID in {@link Scene#models}.
 *
 * Since {@link Mesh} also implements {@link Entity}, we can designate the leaf {@link Mesh}es as objects, causing them to
 * be registered by their IDs in {@link Scene#objects}.
 *
 * We can then find those {@link Entity} types in {@link Scene#models} and {@link Scene#objects}.
 *
 * We can also update properties of our object-Meshes via calls to {@link Scene#setObjectsHighlighted} etc.
 *
 * [[Run this example](/examples/index.html#sceneRepresentation_SceneGraph)]
 *
 * ````javascript
 * import {Viewer, Mesh, Node, PhongMaterial} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas"
 * });
 *
 * viewer.scene.camera.eye = [-21.80, 4.01, 6.56];
 * viewer.scene.camera.look = [0, -5.75, 0];
 * viewer.scene.camera.up = [0.37, 0.91, -0.11];
 *
 * new Node(viewer.scene, {
 *      id: "table",
 *      isModel: true, // <---------- Node represents a model, so is registered by ID in viewer.scene.models
 *      rotation: [0, 50, 0],
 *      position: [0, 0, 0],
 *      scale: [1, 1, 1],
 *
 *      children: [
 *
 *          new Mesh(viewer.scene, { // Red table leg
 *              id: "redLeg",
 *              isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects
 *              position: [-4, -6, -4],
 *              scale: [1, 3, 1],
 *              rotation: [0, 0, 0],
 *              material: new PhongMaterial(viewer.scene, {
 *                  diffuse: [1, 0.3, 0.3]
 *              })
 *          }),
 *
 *          new Mesh(viewer.scene, { // Green table leg
 *              id: "greenLeg",
 *              isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects
 *              position: [4, -6, -4],
 *              scale: [1, 3, 1],
 *              rotation: [0, 0, 0],
 *              material: new PhongMaterial(viewer.scene, {
 *                  diffuse: [0.3, 1.0, 0.3]
 *              })
 *          }),
 *
 *          new Mesh(viewer.scene, {// Blue table leg
 *              id: "blueLeg",
 *              isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects
 *              position: [4, -6, 4],
 *              scale: [1, 3, 1],
 *              rotation: [0, 0, 0],
 *              material: new PhongMaterial(viewer.scene, {
 *                  diffuse: [0.3, 0.3, 1.0]
 *              })
 *          }),
 *
 *          new Mesh(viewer.scene, {  // Yellow table leg
 *              id: "yellowLeg",
 *              isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects
 *              position: [-4, -6, 4],
 *              scale: [1, 3, 1],
 *              rotation: [0, 0, 0],
 *              material: new PhongMaterial(viewer.scene, {
 *                   diffuse: [1.0, 1.0, 0.0]
 *              })
 *          }),
 *
 *          new Mesh(viewer.scene, { // Purple table top
 *              id: "tableTop",
 *              isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects
 *              position: [0, -3, 0],
 *              scale: [6, 0.5, 6],
 *              rotation: [0, 0, 0],
 *              material: new PhongMaterial(viewer.scene, {
 *                  diffuse: [1.0, 0.3, 1.0]
 *              })
 *          })
 *      ]
 *  });
 *
 * // Find Nodes and Meshes by their IDs
 *
 * var table = viewer.scene.models["table"];                // Since table Node has isModel == true
 *
 * var redLeg = viewer.scene.objects["redLeg"];             // Since the Meshes have isObject == true
 * var greenLeg = viewer.scene.objects["greenLeg"];
 * var blueLeg = viewer.scene.objects["blueLeg"];
 *
 * // Highlight one of the table leg Meshes
 *
 * viewer.scene.setObjectsHighlighted(["redLeg"], true);    // Since the Meshes have isObject == true
 *
 * // Periodically update transforms on our Nodes and Meshes
 *
 * viewer.scene.on("tick", function () {
 *
 *       // Rotate legs
 *       redLeg.rotateY(0.5);
 *       greenLeg.rotateY(0.5);
 *       blueLeg.rotateY(0.5);
 *
 *       // Rotate table
 *       table.rotateY(0.5);
 *       table.rotateX(0.3);
 *   });
 * ````
 *
 * ## Metadata
 *
 * As mentioned, we can also associate {@link MetaModel}s and {@link MetaObject}s with our Nodes and {@link Mesh}es,
 * within a {@link MetaScene}. See {@link MetaScene} for an example.
 *
 * @implements {Entity}
 */
class Node$1 extends Component {

    /**
     * @constructor
     * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well.
     * @param {*} [cfg] Configs
     * @param {String} [cfg.id] Optional ID, unique among all components in the parent scene, generated automatically when omitted.
     * @param {Boolean} [cfg.isModel] Specify ````true```` if this Mesh represents a model, in which case the Mesh will be registered by {@link Mesh#id} in {@link Scene#models} and may also have a corresponding {@link MetaModel} with matching {@link MetaModel#id}, registered by that ID in {@link MetaScene#metaModels}.
     * @param {Boolean} [cfg.isObject] Specify ````true```` if this Mesh represents an object, in which case the Mesh will be registered by {@link Mesh#id} in {@link Scene#objects} and may also have a corresponding {@link MetaObject} with matching {@link MetaObject#id}, registered by that ID in {@link MetaScene#metaObjects}.
     * @param {Node} [cfg.parent] The parent Node.
     * @param {Number[]} [cfg.origin] World-space origin for this Node.
     * @param {Number[]} [cfg.rtcCenter] Deprecated - renamed to ````origin````.
     * @param {Number[]} [cfg.position=[0,0,0]] Local 3D position.
     * @param {Number[]} [cfg.scale=[1,1,1]] Local scale.
     * @param {Number[]} [cfg.rotation=[0,0,0]] Local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis.
     * @param {Number[]} [cfg.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1] Local modelling transform matrix. Overrides the position, scale and rotation parameters.
     * @param {Number[]} [cfg.offset=[0,0,0]] World-space 3D translation offset. Translates the Node in World space, after modelling transforms.
     * @param {Boolean} [cfg.visible=true] Indicates if the Node is initially visible.
     * @param {Boolean} [cfg.culled=false] Indicates if the Node is initially culled from view.
     * @param {Boolean} [cfg.pickable=true] Indicates if the Node is initially pickable.
     * @param {Boolean} [cfg.clippable=true] Indicates if the Node is initially clippable.
     * @param {Boolean} [cfg.collidable=true] Indicates if the Node is initially included in boundary calculations.
     * @param {Boolean} [cfg.castsShadow=true] Indicates if the Node initially casts shadows.
     * @param {Boolean} [cfg.receivesShadow=true]  Indicates if the Node initially receives shadows.
     * @param {Boolean} [cfg.xrayed=false] Indicates if the Node is initially xrayed.
     * @param {Boolean} [cfg.highlighted=false] Indicates if the Node is initially highlighted.
     * @param {Boolean} [cfg.selected=false] Indicates if the Mesh is initially selected.
     * @param {Boolean} [cfg.edges=false] Indicates if the Node's edges are initially emphasized.
     * @param {Number[]} [cfg.colorize=[1.0,1.0,1.0]] Node's initial RGB colorize color, multiplies by the rendered fragment colors.
     * @param {Number} [cfg.opacity=1.0] Node's initial opacity factor, multiplies by the rendered fragment alpha.
     * @param {Array} [cfg.children] Child Nodes or {@link Mesh}es to add initially. Children must be in the same {@link Scene} and will be removed first from whatever parents they may already have.
     * @param {Boolean} [cfg.inheritStates=true] Indicates if children given to this constructor should inherit rendering state from this parent as they are added. Rendering state includes {@link Node#visible}, {@link Node#culled}, {@link Node#pickable}, {@link Node#clippable}, {@link Node#castsShadow}, {@link Node#receivesShadow}, {@link Node#selected}, {@link Node#highlighted}, {@link Node#colorize} and {@link Node#opacity}.
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._parentNode = null;
        this._children = [];

        this._aabb = null;
        this._aabbDirty = true;

        this.scene._aabbDirty = true;

        this._numTriangles = 0;

        this._scale = math.vec3();
        this._quaternion = math.identityQuaternion();
        this._rotation = math.vec3();
        this._position = math.vec3();
        this._offset = math.vec3();

        this._localMatrix = math.identityMat4();
        this._worldMatrix = math.identityMat4();

        this._localMatrixDirty = true;
        this._worldMatrixDirty = true;

        if (cfg.matrix) {
            this.matrix = cfg.matrix;
        } else {
            this.scale = cfg.scale;
            this.position = cfg.position;
            if (cfg.quaternion) ; else {
                this.rotation = cfg.rotation;
            }
        }

        this._isModel = cfg.isModel;
        if (this._isModel) {
            this.scene._registerModel(this);
        }

        this._isObject = cfg.isObject;
        if (this._isObject) {
            this.scene._registerObject(this);
        }

        this.origin = cfg.origin;
        this.visible = cfg.visible;
        this.culled = cfg.culled;
        this.pickable = cfg.pickable;
        this.clippable = cfg.clippable;
        this.collidable = cfg.collidable;
        this.castsShadow = cfg.castsShadow;
        this.receivesShadow = cfg.receivesShadow;
        this.xrayed = cfg.xrayed;
        this.highlighted = cfg.highlighted;
        this.selected = cfg.selected;
        this.edges = cfg.edges;
        this.colorize = cfg.colorize;
        this.opacity = cfg.opacity;
        this.offset = cfg.offset;

        // Add children, which inherit state from this Node

        if (cfg.children) {
            const children = cfg.children;
            for (let i = 0, len = children.length; i < len; i++) {
                this.addChild(children[i], cfg.inheritStates);
            }
        }

        if (cfg.parentId) {
            const parentNode = this.scene.components[cfg.parentId];
            if (!parentNode) {
                this.error("Parent not found: '" + cfg.parentId + "'");
            } else if (!parentNode.isNode) {
                this.error("Parent is not a Node: '" + cfg.parentId + "'");
            } else {
                parentNode.addChild(this);
            }
        } else if (cfg.parent) {
            if (!cfg.parent.isNode) {
                this.error("Parent is not a Node");
            }
            cfg.parent.addChild(this);
        }
    }

    //------------------------------------------------------------------------------------------------------------------
    // Entity members
    //------------------------------------------------------------------------------------------------------------------

    /**
     * Returns true to indicate that this Component is an Entity.
     * @type {Boolean}
     */
    get isEntity() {
        return true;
    }

    /**
     * Returns ````true```` if this Mesh represents a model.
     *
     * When this returns ````true````, the Mesh will be registered by {@link Mesh#id} in {@link Scene#models} and
     * may also have a corresponding {@link MetaModel}.
     *
     * @type {Boolean}
     */
    get isModel() {
        return this._isModel;
    }

    /**
     * Returns ````true```` if this Node represents an object.
     *
     * When ````true```` the Node will be registered by {@link Node#id} in
     * {@link Scene#objects} and may also have a {@link MetaObject} with matching {@link MetaObject#id}.
     *
     * @type {Boolean}
     * @abstract
     */
    get isObject() {
        return this._isObject;
    }

    /**
     * Gets the Node's World-space 3D axis-aligned bounding box.
     *
     * Represented by a six-element Float64Array containing the min/max extents of the
     * axis-aligned volume, ie. ````[xmin, ymin,zmin,xmax,ymax, zmax]````.
     *
     * @type {Number[]}
     */
    get aabb() {
        if (this._aabbDirty) {
            this._updateAABB();
        }
        return this._aabb;
    }

    /**
     * Sets the World-space origin for this Node.
     *
     * @type {Float64Array}
     */
    set origin(origin) {
        if (origin) {
            if (!this._origin) {
                this._origin = math.vec3();
            }
            this._origin.set(origin);
        } else {
            if (this._origin) {
                this._origin = null;
            }
        }
        for (let i = 0, len = this._children.length; i < len; i++) {
            this._children[i].origin = origin;
        }
        this.glRedraw();
    }

    /**
     *  Gets the World-space origin for this Node.
     *
     * @type {Float64Array}
     */
    get origin() {
        return this._origin;
    }

    /**
     * Sets the World-space origin for this Node.
     *
     * Deprecated and replaced by {@link Node#origin}.
     *
     * @deprecated
     * @type {Float64Array}
     */
    set rtcCenter(rtcCenter) {
        this.origin = rtcCenter;
    }

    /**
     * Gets the World-space origin for this Node.
     *
     * Deprecated and replaced by {@link Node#origin}.
     *
     * @deprecated
     * @type {Float64Array}
     */
    get rtcCenter() {
        return this.origin;
    }

    /**
     * The number of triangles in this Node.
     *
     * @type {Number}
     */
    get numTriangles() {
        return this._numTriangles;
    }

    /**
     * Sets if this Node and all child Nodes and {@link Mesh}es are visible.
     *
     * Only rendered both {@link Node#visible} is ````true```` and {@link Node#culled} is ````false````.
     *
     * When {@link Node#isObject} and {@link Node#visible} are both ````true```` the Node will be
     * registered by {@link Node#id} in {@link Scene#visibleObjects}.
     *
     * @type {Boolean}
     */
    set visible(visible) {
        visible = visible !== false;
        this._visible = visible;
        for (let i = 0, len = this._children.length; i < len; i++) {
            this._children[i].visible = visible;
        }
        if (this._isObject) {
            this.scene._objectVisibilityUpdated(this, visible);
        }
    }

    /**
     * Gets if this Node is visible.
     *
     * Child Nodes and {@link Mesh}es may have different values for this property.
     *
     * When {@link Node#isObject} and {@link Node#visible} are both ````true```` the Node will be
     * registered by {@link Node#id} in {@link Scene#visibleObjects}.
     *
     * @type {Boolean}
     */
    get visible() {
        return this._visible;
    }

    /**
     * Sets if this Node and all child Nodes and {@link Mesh}es are xrayed.
     *
     * When {@link Node#isObject} and {@link Node#xrayed} are both ````true```` the Node will be
     * registered by {@link Node#id} in {@link Scene#xrayedObjects}.
     *
     * @type {Boolean}
     */
    set xrayed(xrayed) {
        xrayed = !!xrayed;
        this._xrayed = xrayed;
        for (let i = 0, len = this._children.length; i < len; i++) {
            this._children[i].xrayed = xrayed;
        }
        if (this._isObject) {
            this.scene._objectXRayedUpdated(this, xrayed);
        }
    }

    /**
     * Gets if this Node is xrayed.
     *
     * When {@link Node#isObject} and {@link Node#xrayed} are both ````true```` the Node will be
     * registered by {@link Node#id} in {@link Scene#xrayedObjects}.
     *
     * Child Nodes and {@link Mesh}es may have different values for this property.
     *
     * @type {Boolean}
     */
    get xrayed() {
        return this._xrayed;
    }

    /**
     * Sets if this Node and all child Nodes and {@link Mesh}es are highlighted.
     *
     * When {@link Node#isObject} and {@link Node#highlighted} are both ````true```` the Node will be
     * registered by {@link Node#id} in {@link Scene#highlightedObjects}.
     *
     * @type {Boolean}
     */
    set highlighted(highlighted) {
        highlighted = !!highlighted;
        this._highlighted = highlighted;
        for (let i = 0, len = this._children.length; i < len; i++) {
            this._children[i].highlighted = highlighted;
        }
        if (this._isObject) {
            this.scene._objectHighlightedUpdated(this, highlighted);
        }
    }

    /**
     * Gets if this Node is highlighted.
     *
     * When {@link Node#isObject} and {@link Node#highlighted} are both ````true```` the Node will be
     * registered by {@link Node#id} in {@link Scene#highlightedObjects}.
     *
     * Child Nodes and {@link Mesh}es may have different values for this property.
     *
     * @type {Boolean}
     */
    get highlighted() {
        return this._highlighted;
    }

    /**
     * Sets if this Node and all child Nodes and {@link Mesh}es are selected.
     *
     * When {@link Node#isObject} and {@link Node#selected} are both ````true```` the Node will be
     * registered by {@link Node#id} in {@link Scene#selectedObjects}.
     *
     * @type {Boolean}
     */
    set selected(selected) {
        selected = !!selected;
        this._selected = selected;
        for (let i = 0, len = this._children.length; i < len; i++) {
            this._children[i].selected = selected;
        }
        if (this._isObject) {
            this.scene._objectSelectedUpdated(this, selected);
        }
    }

    /**
     * Gets if this Node is selected.
     *
     * When {@link Node#isObject} and {@link Node#selected} are both ````true```` the Node will be
     * registered by {@link Node#id} in {@link Scene#selectedObjects}.
     *
     * Child Nodes and {@link Mesh}es may have different values for this property.
     *
     * @type {Boolean}
     */
    get selected() {
        return this._selected;
    }

    /**
     * Sets if this Node and all child Nodes and {@link Mesh}es are edge-enhanced.
     *
     * @type {Boolean}
     */
    set edges(edges) {
        edges = !!edges;
        this._edges = edges;
        for (let i = 0, len = this._children.length; i < len; i++) {
            this._children[i].edges = edges;
        }
    }

    /**
     * Gets if this Node's edges are enhanced.
     *
     * Child Nodes and {@link Mesh}es may have different values for this property.
     *
     * @type {Boolean}
     */
    get edges() {
        return this._edges;
    }

    /**
     * Sets if this Node and all child Nodes and {@link Mesh}es are culled.
     *
     * @type {Boolean}
     */
    set culled(culled) {
        culled = !!culled;
        this._culled = culled;
        for (let i = 0, len = this._children.length; i < len; i++) {
            this._children[i].culled = culled;
        }
    }

    /**
     * Gets if this Node is culled.
     *
     * @type {Boolean}
     */
    get culled() {
        return this._culled;
    }

    /**
     * Sets if this Node and all child Nodes and {@link Mesh}es are clippable.
     *
     * Clipping is done by the {@link SectionPlane}s in {@link Scene#clips}.
     *
     * @type {Boolean}
     */
    set clippable(clippable) {
        clippable = clippable !== false;
        this._clippable = clippable;
        for (let i = 0, len = this._children.length; i < len; i++) {
            this._children[i].clippable = clippable;
        }
    }

    /**
     * Gets if this Node is clippable.
     *
     * Clipping is done by the {@link SectionPlane}s in {@link Scene#clips}.
     *
     * Child Nodes and {@link Mesh}es may have different values for this property.
     *
     * @type {Boolean}
     */
    get clippable() {
        return this._clippable;
    }

    /**
     * Sets if this Node and all child Nodes and {@link Mesh}es are included in boundary calculations.
     *
     * @type {Boolean}
     */
    set collidable(collidable) {
        collidable = collidable !== false;
        this._collidable = collidable;
        for (let i = 0, len = this._children.length; i < len; i++) {
            this._children[i].collidable = collidable;
        }
    }

    /**
     * Gets if this Node is included in boundary calculations.
     *
     * Child Nodes and {@link Mesh}es may have different values for this property.
     *
     * @type {Boolean}
     */
    get collidable() {
        return this._collidable;
    }

    /**
     * Sets if this Node and all child Nodes and {@link Mesh}es are pickable.
     *
     * Picking is done via calls to {@link Scene#pick}.
     *
     * @type {Boolean}
     */
    set pickable(pickable) {
        pickable = pickable !== false;
        this._pickable = pickable;
        for (let i = 0, len = this._children.length; i < len; i++) {
            this._children[i].pickable = pickable;
        }
    }

    /**
     * Gets if to this Node is pickable.
     *
     * Picking is done via calls to {@link Scene#pick}.
     *
     * Child Nodes and {@link Mesh}es may have different values for this property.
     *
     * @type {Boolean}
     */
    get pickable() {
        return this._pickable;
    }

    /**
     * Sets the RGB colorize color for this Node and all child Nodes and {@link Mesh}es}.
     *
     * Multiplies by rendered fragment colors.
     *
     * Each element of the color is in range ````[0..1]````.
     *
     * @type {Number[]}
     */
    set colorize(rgb) {
        let colorize = this._colorize;
        if (!colorize) {
            colorize = this._colorize = new Float32Array(4);
            colorize[3] = 1.0;
        }
        if (rgb) {
            colorize[0] = rgb[0];
            colorize[1] = rgb[1];
            colorize[2] = rgb[2];
        } else {
            colorize[0] = 1;
            colorize[1] = 1;
            colorize[2] = 1;
        }
        for (let i = 0, len = this._children.length; i < len; i++) {
            this._children[i].colorize = colorize;
        }
        if (this._isObject) {
            const colorized = (!!rgb);
            this.scene._objectColorizeUpdated(this, colorized);
        }
    }

    /**
     * Gets the RGB colorize color for this Node.
     *
     * Each element of the color is in range ````[0..1]````.
     *
     * Child Nodes and {@link Mesh}es may have different values for this property.
     *
     * @type {Number[]}
     */
    get colorize() {
        return this._colorize.slice(0, 3);
    }

    /**
     * Sets the opacity factor for this Node and all child Nodes and {@link Mesh}es.
     *
     * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas.
     *
     * @type {Number}
     */
    set opacity(opacity) {
        let colorize = this._colorize;
        if (!colorize) {
            colorize = this._colorize = new Float32Array(4);
            colorize[0] = 1;
            colorize[1] = 1;
            colorize[2] = 1;
        }
        colorize[3] = opacity !== null && opacity !== undefined ? opacity : 1.0;
        for (let i = 0, len = this._children.length; i < len; i++) {
            this._children[i].opacity = opacity;
        }
        if (this._isObject) {
            const opacityUpdated = (opacity !== null && opacity !== undefined);
            this.scene._objectOpacityUpdated(this, opacityUpdated);
        }
    }

    /**
     * Gets this Node's opacity factor.
     *
     * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas.
     *
     * Child Nodes and {@link Mesh}es may have different values for this property.
     *
     * @type {Number}
     */
    get opacity() {
        return this._colorize[3];
    }

    /**
     * Sets if this Node and all child Nodes and {@link Mesh}es cast shadows.
     *
     * @type {Boolean}
     */
    set castsShadow(castsShadow) {
        castsShadow = !!castsShadow;
        this._castsShadow = castsShadow;
        for (let i = 0, len = this._children.length; i < len; i++) {
            this._children[i].castsShadow = castsShadow;
        }
    }

    /**
     * Gets if this Node casts shadows.
     *
     * Child Nodes and {@link Mesh}es may have different values for this property.
     *
     * @type {Boolean}
     */
    get castsShadow() {
        return this._castsShadow;
    }

    /**
     * Sets if this Node and all child Nodes and {@link Mesh}es can have shadows cast upon them.
     *
     * @type {Boolean}
     */
    set receivesShadow(receivesShadow) {
        receivesShadow = !!receivesShadow;
        this._receivesShadow = receivesShadow;
        for (let i = 0, len = this._children.length; i < len; i++) {
            this._children[i].receivesShadow = receivesShadow;
        }
    }

    /**
     * Whether or not to this Node can have shadows cast upon it.
     *
     * Child Nodes and {@link Mesh}es may have different values for this property.
     *
     * @type {Boolean}
     */
    get receivesShadow() {
        return this._receivesShadow;
    }

    /**
     * Gets if this Node can have Scalable Ambient Obscurance (SAO) applied to it.
     *
     * SAO is configured by {@link SAO}.
     *
     * @type {Boolean}
     * @abstract
     */
    get saoEnabled() {
        return false; // TODO: Support SAO on Nodes
    }


    /**
     * Sets the 3D World-space offset for this Node and all child Nodes and {@link Mesh}es}.
     *
     * The offset dynamically translates those components in World-space.
     *
     * Default value is ````[0, 0, 0]````.
     *
     * Note that child Nodes and {@link Mesh}es may subsequently be given different values for this property.
     *
     * @type {Number[]}
     */
    set offset(offset) {
        if (offset) {
            this._offset[0] = offset[0];
            this._offset[1] = offset[1];
            this._offset[2] = offset[2];
        } else {
            this._offset[0] = 0;
            this._offset[1] = 0;
            this._offset[2] = 0;
        }
        for (let i = 0, len = this._children.length; i < len; i++) {
            this._children[i].offset = this._offset;
        }
        if (this._isObject) {
            this.scene._objectOffsetUpdated(this, offset);
        }
    }

    /**
     * Gets the Node's 3D World-space offset.
     *
     * Default value is ````[0, 0, 0]````.
     *
     * Child Nodes and {@link Mesh}es may have different values for this property.
     *
     * @type {Number[]}
     */
    get offset() {
        return this._offset;
    }


    //------------------------------------------------------------------------------------------------------------------
    // Node members
    //------------------------------------------------------------------------------------------------------------------

    /**
     * Returns true to indicate that this Component is a Node.
     * @type {Boolean}
     */
    get isNode() {
        return true;
    }

    _setLocalMatrixDirty() {
        this._localMatrixDirty = true;
        this._setWorldMatrixDirty();
    }

    _setWorldMatrixDirty() {
        this._worldMatrixDirty = true;
        for (let i = 0, len = this._children.length; i < len; i++) {
            this._children[i]._setWorldMatrixDirty();
        }
    }

    _buildWorldMatrix() {
        const localMatrix = this.matrix;
        if (!this._parentNode) {
            for (let i = 0, len = localMatrix.length; i < len; i++) {
                this._worldMatrix[i] = localMatrix[i];
            }
        } else {
            math.mulMat4(this._parentNode.worldMatrix, localMatrix, this._worldMatrix);
        }
        this._worldMatrixDirty = false;
    }

    _setSubtreeAABBsDirty(node) {
        node._aabbDirty = true;
        if (node._children) {
            for (let i = 0, len = node._children.length; i < len; i++) {
                this._setSubtreeAABBsDirty(node._children[i]);
            }
        }
    }

    _setAABBDirty() {
        this._setSubtreeAABBsDirty(this);
        if (this.collidable) {
            for (let node = this; node; node = node._parentNode) {
                node._aabbDirty = true;
            }
        }
    }

    _updateAABB() {
        this.scene._aabbDirty = true;
        if (!this._aabb) {
            this._aabb = math.AABB3();
        }
        if (this._buildAABB) {
            this._buildAABB(this.worldMatrix, this._aabb); // Mesh or VBOSceneModel
        } else { // Node | Node | Model
            math.collapseAABB3(this._aabb);
            let node;
            for (let i = 0, len = this._children.length; i < len; i++) {
                node = this._children[i];
                if (!node.collidable) {
                    continue;
                }
                math.expandAABB3(this._aabb, node.aabb);
            }
        }
        this._aabbDirty = false;
    }

    /**
     * Adds a child Node or {@link Mesh}.
     *
     * The child must be a Node or {@link Mesh} in the same {@link Scene}.
     *
     * If the child already has a parent, will be removed from that parent first.
     *
     * Does nothing if already a child.
     *
     * @param {Node|Mesh|String} child Instance or ID of the child to add.
     * @param [inheritStates=false] Indicates if the child should inherit rendering states from this parent as it is added. Rendering state includes {@link Node#visible}, {@link Node#culled}, {@link Node#pickable}, {@link Node#clippable}, {@link Node#castsShadow}, {@link Node#receivesShadow}, {@link Node#selected}, {@link Node#highlighted}, {@link Node#colorize} and {@link Node#opacity}.
     * @returns {Node|Mesh} The child.
     */
    addChild(child, inheritStates) {
        if (utils.isNumeric(child) || utils.isString(child)) {
            const nodeId = child;
            child = this.scene.component[nodeId];
            if (!child) {
                this.warn("Component not found: " + utils.inQuotes(nodeId));
                return;
            }
            if (!child.isNode && !child.isMesh) {
                this.error("Not a Node or Mesh: " + nodeId);
                return;
            }
        } else {
            if (!child.isNode && !child.isMesh) {
                this.error("Not a Node or Mesh: " + child.id);
                return;
            }
            if (child._parentNode) {
                if (child._parentNode.id === this.id) {
                    this.warn("Already a child: " + child.id);
                    return;
                }
                child._parentNode.removeChild(child);
            }
        }
        child.id;
        if (child.scene.id !== this.scene.id) {
            this.error("Child not in same Scene: " + child.id);
            return;
        }
        this._children.push(child);
        child._parentNode = this;
        if (!!inheritStates) {
            child.visible = this.visible;
            child.culled = this.culled;
            child.xrayed = this.xrayed;
            child.highlited = this.highlighted;
            child.selected = this.selected;
            child.edges = this.edges;
            child.clippable = this.clippable;
            child.pickable = this.pickable;
            child.collidable = this.collidable;
            child.castsShadow = this.castsShadow;
            child.receivesShadow = this.receivesShadow;
            child.colorize = this.colorize;
            child.opacity = this.opacity;
            child.offset = this.offset;
        }
        child._setWorldMatrixDirty();
        child._setAABBDirty();
        this._numTriangles += child.numTriangles;
        return child;
    }

    /**
     * Removes the given child Node or {@link Mesh}.
     *
     * @param {Node|Mesh} child Child to remove.
     */
    removeChild(child) {
        for (let i = 0, len = this._children.length; i < len; i++) {
            if (this._children[i].id === child.id) {
                child._parentNode = null;
                this._children = this._children.splice(i, 1);
                child._setWorldMatrixDirty();
                child._setAABBDirty();
                this._setAABBDirty();
                this._numTriangles -= child.numTriangles;
                return;
            }
        }
    }

    /**
     * Removes all child Nodes and {@link Mesh}es.
     */
    removeChildren() {
        let child;
        for (let i = 0, len = this._children.length; i < len; i++) {
            child = this._children[i];
            child._parentNode = null;
            child._setWorldMatrixDirty();
            child._setAABBDirty();
            this._numTriangles -= child.numTriangles;
        }
        this._children = [];
        this._setAABBDirty();
    }

    /**
     * Number of child Nodes or {@link Mesh}es.
     *
     * @type {Number}
     */
    get numChildren() {
        return this._children.length;
    }

    /**
     * Array of child Nodes or {@link Mesh}es.
     *
     * @type {Array}
     */
    get children() {
        return this._children;
    }

    /**
     * The parent Node.
     *
     * The parent Node may also be set by passing the Node to the parent's {@link Node#addChild} method.
     *
     * @type {Node}
     */
    set parent(node) {
        if (utils.isNumeric(node) || utils.isString(node)) {
            const nodeId = node;
            node = this.scene.components[nodeId];
            if (!node) {
                this.warn("Node not found: " + utils.inQuotes(nodeId));
                return;
            }
            if (!node.isNode) {
                this.error("Not a Node: " + node.id);
                return;
            }
        }
        if (node.scene.id !== this.scene.id) {
            this.error("Node not in same Scene: " + node.id);
            return;
        }
        if (this._parentNode && this._parentNode.id === node.id) {
            this.warn("Already a child of Node: " + node.id);
            return;
        }
        node.addChild(this);
    }

    /**
     * The parent Node.
     *
     * @type {Node}
     */
    get parent() {
        return this._parentNode;
    }

    /**
     * Sets the Node's local translation.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    set position(value) {
        this._position.set(value || [0, 0, 0]);
        this._setLocalMatrixDirty();
        this._setAABBDirty();
        this.glRedraw();
    }

    /**
     * Gets the Node's local translation.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    get position() {
        return this._position;
    }

    /**
     * Sets the Node's local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    set rotation(value) {
        this._rotation.set(value || [0, 0, 0]);
        math.eulerToQuaternion(this._rotation, "XYZ", this._quaternion);
        this._setLocalMatrixDirty();
        this._setAABBDirty();
        this.glRedraw();
    }

    /**
     * Gets the Node's local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    get rotation() {
        return this._rotation;
    }

    /**
     * Sets the Node's local rotation quaternion.
     *
     * Default value is ````[0,0,0,1]````.
     *
     * @type {Number[]}
     */
    set quaternion(value) {
        this._quaternion.set(value || [0, 0, 0, 1]);
        math.quaternionToEuler(this._quaternion, "XYZ", this._rotation);
        this._setLocalMatrixDirty();
        this._setAABBDirty();
        this.glRedraw();
    }

    /**
     * Gets the Node's local rotation quaternion.
     *
     * Default value is ````[0,0,0,1]````.
     *
     * @type {Number[]}
     */
    get quaternion() {
        return this._quaternion;
    }

    /**
     * Sets the Node's local scale.
     *
     * Default value is ````[1,1,1]````.
     *
     * @type {Number[]}
     */
    set scale(value) {
        this._scale.set(value || [1, 1, 1]);
        this._setLocalMatrixDirty();
        this._setAABBDirty();
        this.glRedraw();
    }

    /**
     * Gets the Node's local scale.
     *
     * Default value is ````[1,1,1]````.
     *
     * @type {Number[]}
     */
    get scale() {
        return this._scale;
    }

    /**
     * Sets the Node's local modeling transform matrix.
     *
     * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````.
     *
     * @type {Number[]}
     */
    set matrix(value) {
        if (!this._localMatrix) {
            this._localMatrix = math.identityMat4();
        }
        this._localMatrix.set(value || identityMat$2);
        math.decomposeMat4(this._localMatrix, this._position, this._quaternion, this._scale);
        this._localMatrixDirty = false;
        this._setWorldMatrixDirty();
        this._setAABBDirty();
        this.glRedraw();
    }

    /**
     * Gets the Node's local modeling transform matrix.
     *
     * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````.
     *
     * @type {Number[]}
     */
    get matrix() {
        if (this._localMatrixDirty) {
            if (!this._localMatrix) {
                this._localMatrix = math.identityMat4();
            }
            math.composeMat4(this._position, this._quaternion, this._scale, this._localMatrix);
            this._localMatrixDirty = false;
        }
        return this._localMatrix;
    }

    /**
     * Gets the Node's World matrix.
     *
     * @property worldMatrix
     * @type {Number[]}
     */
    get worldMatrix() {
        if (this._worldMatrixDirty) {
            this._buildWorldMatrix();
        }
        return this._worldMatrix;
    }

    /**
     * Rotates the Node about the given local axis by the given increment.
     *
     * @param {Number[]} axis Local axis about which to rotate.
     * @param {Number} angle Angle increment in degrees.
     */
    rotate(axis, angle) {
        angleAxis$2[0] = axis[0];
        angleAxis$2[1] = axis[1];
        angleAxis$2[2] = axis[2];
        angleAxis$2[3] = angle * math.DEGTORAD;
        math.angleAxisToQuaternion(angleAxis$2, q1$2);
        math.mulQuaternions(this.quaternion, q1$2, q2$2);
        this.quaternion = q2$2;
        this._setLocalMatrixDirty();
        this._setAABBDirty();
        this.glRedraw();
        return this;
    }

    /**
     * Rotates the Node about the given World-space axis by the given increment.
     *
     * @param {Number[]} axis Local axis about which to rotate.
     * @param {Number} angle Angle increment in degrees.
     */
    rotateOnWorldAxis(axis, angle) {
        angleAxis$2[0] = axis[0];
        angleAxis$2[1] = axis[1];
        angleAxis$2[2] = axis[2];
        angleAxis$2[3] = angle * math.DEGTORAD;
        math.angleAxisToQuaternion(angleAxis$2, q1$2);
        math.mulQuaternions(q1$2, this.quaternion, q1$2);
        //this.quaternion.premultiply(q1);
        return this;
    }

    /**
     * Rotates the Node about the local X-axis by the given increment.
     *
     * @param {Number} angle Angle increment in degrees.
     */
    rotateX(angle) {
        return this.rotate(xAxis$2, angle);
    }

    /**
     * Rotates the Node about the local Y-axis by the given increment.
     *
     * @param {Number} angle Angle increment in degrees.
     */
    rotateY(angle) {
        return this.rotate(yAxis$2, angle);
    }

    /**
     * Rotates the Node about the local Z-axis by the given increment.
     *
     * @param {Number} angle Angle increment in degrees.
     */
    rotateZ(angle) {
        return this.rotate(zAxis$2, angle);
    }

    /**
     * Translates the Node along local space vector by the given increment.
     *
     * @param {Number[]} axis Normalized local space 3D vector along which to translate.
     * @param {Number} distance Distance to translate along  the vector.
     */
    translate(axis, distance) {
        math.vec3ApplyQuaternion(this.quaternion, axis, veca$1);
        math.mulVec3Scalar(veca$1, distance, vecb$1);
        math.addVec3(this.position, vecb$1, this.position);
        this._setLocalMatrixDirty();
        this._setAABBDirty();
        this.glRedraw();
        return this;
    }

    /**
     * Translates the Node along the local X-axis by the given increment.
     *
     * @param {Number} distance Distance to translate along  the X-axis.
     */
    translateX(distance) {
        return this.translate(xAxis$2, distance);
    }

    /**
     * Translates the Node along the local Y-axis by the given increment.
     *
     * @param {Number} distance Distance to translate along  the Y-axis.
     */
    translateY(distance) {
        return this.translate(yAxis$2, distance);
    }

    /**
     * Translates the Node along the local Z-axis by the given increment.
     *
     * @param {Number} distance Distance to translate along  the Z-axis.
     */
    translateZ(distance) {
        return this.translate(zAxis$2, distance);
    }

    //------------------------------------------------------------------------------------------------------------------
    // Component members
    //------------------------------------------------------------------------------------------------------------------

    /**
     @private
     */
    get type() {
        return "Node";
    }

    /**
     * Destroys this Node.
     */
    destroy() {
        super.destroy();
        if (this._parentNode) {
            this._parentNode.removeChild(this);
        }
        if (this._isObject) {
            this.scene._deregisterObject(this);
            if (this._visible) {
                this.scene._objectVisibilityUpdated(this, false, false);
            }
            if (this._xrayed) {
                this.scene._objectXRayedUpdated(this, false, false);
            }
            if (this._selected) {
                this.scene._objectSelectedUpdated(this, false, false);
            }
            if (this._highlighted) {
                this.scene._objectHighlightedUpdated(this, false, false);
            }
            this.scene._objectColorizeUpdated(this, false);
            this.scene._objectOpacityUpdated(this, false);
            if (this.offset.some((v) => v !== 0))
                this.scene._objectOffsetUpdated(this, false);
        }
        if (this._isModel) {
            this.scene._deregisterModel(this);
        }
        if (this._children.length) {
            // Clone the _children before iterating, so our children don't mess us up when calling removeChild().
            const tempChildList = this._children.splice();
            let child;
            for (let i = 0, len = tempChildList.length; i < len; i++) {
                child = tempChildList[i];
                child.destroy();
            }
        }
        this._children = [];
        this._setAABBDirty();
        this.scene._aabbDirty = true;
    }

}

const tempVec3a$M = math.vec3();
const tempVec4$1  = math.vec4();

/**
 * Given a view matrix and a relative-to-center (RTC) coordinate origin, returns a view matrix
 * to transform RTC coordinates to View-space.
 *
 * The returned view matrix is
 *
 * @private
 */
const createRTCViewMat = (function () {

    const tempMat = new Float64Array(16);
    const rtcCenterWorld = new Float64Array(4);
    const rtcCenterView = new Float64Array(4);

    return function (viewMat, rtcCenter, rtcViewMat) {
        rtcViewMat = rtcViewMat || tempMat;
        rtcCenterWorld[0] = rtcCenter[0];
        rtcCenterWorld[1] = rtcCenter[1];
        rtcCenterWorld[2] = rtcCenter[2];
        rtcCenterWorld[3] = 1;
        math.transformVec4(viewMat, rtcCenterWorld, rtcCenterView);
        math.setMat4Translation(viewMat, rtcCenterView, rtcViewMat);
        return rtcViewMat.slice ();
    }
}());

/**
 * Converts a World-space 3D position to RTC.
 *
 * Given a double-precision World-space position, returns a double-precision relative-to-center (RTC) center pos
 * and a single-precision offset fom that center.
 * @private
 * @param {Float64Array} worldPos The World-space position.
 * @param {Float64Array} rtcCenter Double-precision relative-to-center (RTC) center pos.
 * @param {Float32Array} rtcPos Single-precision offset fom that center.
 */
function worldToRTCPos(worldPos, rtcCenter, rtcPos) {

    const xHigh = Float32Array.from([worldPos[0]])[0];
    const xLow = worldPos[0] - xHigh;

    const yHigh = Float32Array.from([worldPos[1]])[0];
    const yLow = worldPos[1] - yHigh;

    const zHigh = Float32Array.from([worldPos[2]])[0];
    const zLow = worldPos[2] - zHigh;

    rtcCenter[0] = xHigh;
    rtcCenter[1] = yHigh;
    rtcCenter[2] = zHigh;

    rtcPos[0] = xLow;
    rtcPos[1] = yLow;
    rtcPos[2] = zLow;
}


/**
 * Converts a flat array of double-precision positions to RTC positions, if necessary.
 *
 * Conversion is necessary if the coordinates have values larger than can be expressed at single-precision. When
 * that's the case, then this function will compute the RTC coordinates and RTC center and return true. Otherwise
 * this function does nothing and returns false.
 *
 * When computing the RTC position, this function uses a modulus operation to ensure that, whenever possible,
 * identical RTC centers are reused for different positions arrays.
 *
 * @private
 * @param {Float64Array} worldPositions Flat array of World-space 3D positions.
 * @param {Float64Array} rtcPositions Outputs the computed flat array of 3D RTC positions.
 * @param {Float64Array} rtcCenter Outputs the computed double-precision relative-to-center (RTC) center pos.
 * @param {Number} [cellSize=10000000] The size of each coordinate cell within the RTC coordinate system.
 * @returns {Boolean} ````True```` if the positions actually needed conversion to RTC, else ````false````. When
 * ````false````, we can safely ignore the data returned in ````rtcPositions```` and ````rtcCenter````,
 * since ````rtcCenter```` will equal ````[0,0,0]````, and ````rtcPositions```` will contain identical values to ````positions````.
 */
function worldToRTCPositions(worldPositions, rtcPositions, rtcCenter, cellSize = 1000) {

    const center = math.getPositionsCenter(worldPositions, tempVec3a$M);

    const rtcCenterX = Math.round(center[0] / cellSize) * cellSize;
    const rtcCenterY = Math.round(center[1] / cellSize) * cellSize;
    const rtcCenterZ = Math.round(center[2] / cellSize) * cellSize;

    rtcCenter[0] = rtcCenterX;
    rtcCenter[1] = rtcCenterY;
    rtcCenter[2] = rtcCenterZ;

    const rtcNeeded = (rtcCenter[0] !== 0 || rtcCenter[1] !== 0 || rtcCenter[2] !== 0);

    if (rtcNeeded) {
        for (let i = 0, len = worldPositions.length; i < len; i += 3) {
            rtcPositions[i + 0] = worldPositions[i + 0] - rtcCenterX;
            rtcPositions[i + 1] = worldPositions[i + 1] - rtcCenterY;
            rtcPositions[i + 2] = worldPositions[i + 2] - rtcCenterZ;
        }
    }

    return rtcNeeded;
}

/**
 * Given a 3D plane defined by distance from origin and direction, and an RTC center position,
 * return a plane position that is relative to the RTC center.
 *
 * @private
 * @param dist
 * @param dir
 * @param rtcCenter
 * @param rtcPlanePos
 * @returns {*}
 */
function getPlaneRTCPos(dist, dir, rtcOrigin, rtcPlanePos, originMatrix) {
    const rtcCenter = (originMatrix
                       ? (tempVec4$1.set(rtcOrigin, 0), tempVec4$1[3] = 1, math.mulMat4v4(originMatrix, tempVec4$1, tempVec4$1))
                       : rtcOrigin);
    const rtcCenterToPlaneDist = math.dotVec3(dir, rtcCenter) + dist;
    const dirNormalized = math.normalizeVec3(dir, tempVec3a$M);
    math.mulVec3Scalar(dirNormalized, -rtcCenterToPlaneDist, rtcPlanePos);
    return rtcPlanePos;
}

/**
 * Texture wrapping mode in which the texture repeats to infinity.
 */
const RepeatWrapping = 1000;

/**
 * Texture wrapping mode in which the last pixel of the texture stretches to the edge of the mesh.
 */
const ClampToEdgeWrapping = 1001;

/**
 * Texture wrapping mode in which the texture repeats to infinity, mirroring on each repeat.
 */
const MirroredRepeatWrapping = 1002;

/**
 * Texture magnification and minification filter that returns the nearest texel to the given sample coordinates.
 */
const NearestFilter = 1003;

/**
 * Texture minification filter that chooses the mipmap that most closely matches the size of the pixel being textured and returns the nearest texel to the given sample coordinates.
 */
const NearestMipMapNearestFilter = 1004;

/**
 * Texture minification filter that chooses the mipmap that most closely matches the size of the pixel being textured
 * and returns the nearest texel to the given sample coordinates.
 */
const NearestMipmapNearestFilter = 1004;

/**
 * Texture minification filter that chooses two mipmaps that most closely match the size of the pixel being textured
 * and returns the nearest texel to the center of the pixel at the given sample coordinates.
 */
const NearestMipmapLinearFilter = 1005;

/**
 * Texture minification filter that chooses two mipmaps that most closely match the size of the pixel being textured
 * and returns the nearest texel to the center of the pixel at the given sample coordinates.
 */
const NearestMipMapLinearFilter = 1005;

/**
 * Texture magnification and minification filter that returns the weighted average of the four nearest texels to the given sample coordinates.
 */
const LinearFilter = 1006;

/**
 * Texture minification filter that chooses the mipmap that most closely matches the size of the pixel being textured and
 * returns the weighted average of the four nearest texels to the given sample coordinates.
 */
const LinearMipMapNearestFilter = 1007;

/**
 * Texture minification filter that chooses two mipmaps that most closely match the size of the pixel being textured,
 * finds within each mipmap the weighted average of the nearest texel to the center of the pixel, then returns the
 * weighted average of those two values.
 */
const LinearMipmapLinearFilter = 1008;

/**
 * Texture minification filter that chooses two mipmaps that most closely match the size of the pixel being textured,
 * finds within each mipmap the weighted average of the nearest texel to the center of the pixel, then returns the
 * weighted average of those two values.
 */
const LinearMipMapLinearFilter = 1008;

/**
 * Unsigned 8-bit integer type.
 */
const UnsignedByteType = 1009;

/**
 * Signed 8-bit integer type.
 */
const ByteType = 1010;

/**
 * Signed 16-bit integer type.
 */
const ShortType = 1011;

/**
 * Unsigned 16-bit integer type.
 */
const UnsignedShortType = 1012;

/**
 * Signed 32-bit integer type.
 */
const IntType = 1013;

/**
 * Unsigned 32-bit integer type.
 */
const UnsignedIntType = 1014;

/**
 * Signed 32-bit floating-point type.
 */
const FloatType = 1015;

/**
 * Signed 16-bit half-precision floating-point type.
 */
const HalfFloatType = 1016;

/**
 * Texture packing mode in which each ````RGBA```` channel is packed into 4 bits, for a combined total of 16 bits.
 */
const UnsignedShort4444Type = 1017;

/**
 * Texture packing mode in which the ````RGB```` channels are each packed into 5 bits, and the ````A```` channel is packed into 1 bit, for a combined total of 16 bits.
 */
const UnsignedShort5551Type = 1018;

/**
 * Unsigned integer type for 24-bit depth texture data.
 */
const UnsignedInt248Type = 1020;

/**
 * Texture sampling mode that discards the ````RGBA```` components and just reads the ````A```` component.
 */
const AlphaFormat = 1021;

/**
 * Texture sampling mode that discards the ````A```` component and reads the ````RGB```` components.
 */
const RGBFormat = 1022;

/**
 * Texture sampling mode that reads the ````RGBA```` components.
 */
const RGBAFormat = 1023;

/**
 * Texture sampling mode that reads each ````RGB```` texture component as a luminance value, converted to a float and clamped
 * to ````[0,1]````, while always reading the ````A```` channel as ````1.0````.
 */
const LuminanceFormat = 1024;

/**
 * Texture sampling mode that reads each of the ````RGBA```` texture components as a luminance/alpha value, converted to a float and clamped to ````[0,1]````.
 */
const LuminanceAlphaFormat = 1025;

/**
 * Texture sampling mode that reads each element as a single depth value, converts it to a float and clamps to ````[0,1]````.
 */
const DepthFormat = 1026;

/**
 * Texture sampling mode that
 */
const DepthStencilFormat = 1027;

/**
 * Texture sampling mode that discards the ````GBA```` components and just reads the ````R```` component.
 */
const RedFormat = 1028;

/**
 * Texture sampling mode that discards the ````GBA```` components and just reads the ````R```` component, as an integer instead of as a float.
 */
const RedIntegerFormat = 1029;

/**
 * Texture sampling mode that discards the ````A```` and ````B```` components and just reads the ````R```` and ````G```` components.
 */
const RGFormat = 1030;

/**
 * Texture sampling mode that discards the ````A```` and ````B```` components and just reads the ````R```` and ````G```` components, as integers instead of floats.
 */
const RGIntegerFormat = 1031;

/**
 * Texture sampling mode that reads the ````RGBA```` components as integers instead of floats.
 */
const RGBAIntegerFormat = 1033;

/**
 * Texture format mode in which the texture is formatted as a <a href="https://en.wikipedia.org/wiki/S3_Texture_Compression">DXT1 compressed</a> ````RGB```` image.
 */
const RGB_S3TC_DXT1_Format = 33776;

/**
 * Texture format mode in which the texture is formatted as a <a href="https://en.wikipedia.org/wiki/S3_Texture_Compression">DXT1 compressed</a> ````RGBA```` image.
 */
const RGBA_S3TC_DXT1_Format = 33777;

/**
 * Texture format mode in which the texture is formatted as a <a href="https://en.wikipedia.org/wiki/S3_Texture_Compression">DXT3 compressed</a> ````RGBA```` image.
 */
const RGBA_S3TC_DXT3_Format = 33778;

/**
 * Texture format mode in which the texture is formatted as a <a href="https://en.wikipedia.org/wiki/S3_Texture_Compression">DXT5 compressed</a> ````RGBA```` image.
 */
const RGBA_S3TC_DXT5_Format = 33779;

/**
 * Texture format mode in which the texture is formatted as a <a href="https://en.wikipedia.org/wiki/PVRTC">PVRTC compressed</a>
 * image, with ````RGB```` compression in 4-bit mode and one block for each 4×4 pixels.
 */
const RGB_PVRTC_4BPPV1_Format = 35840;

/**
 * Texture format mode in which the texture is formatted as a <a href="https://en.wikipedia.org/wiki/PVRTC">PVRTC compressed</a>
 * image, with ````RGB```` compression in 2-bit mode and one block for each 8×4 pixels.
 */
const RGB_PVRTC_2BPPV1_Format = 35841;

/**
 * Texture format mode in which the texture is formatted as a <a href="https://en.wikipedia.org/wiki/PVRTC">PVRTC compressed</a>
 * image, with ````RGBA```` compression in 4-bit mode and one block for each 4×4 pixels.
 */
const RGBA_PVRTC_4BPPV1_Format = 35842;

/**
 * Texture format mode in which the texture is formatted as a <a href="https://en.wikipedia.org/wiki/PVRTC">PVRTC compressed</a>
 * image, with ````RGBA```` compression in 2-bit mode and one block for each 8×4 pixels.
 */
const RGBA_PVRTC_2BPPV1_Format = 35843;

/**
 * Texture format mode in which the texture is formatted as an <a href="https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_etc1/">ETC1 compressed</a>
 * ````RGB```` image.
 */
const RGB_ETC1_Format = 36196;

/**
 * Texture format mode in which the texture is formatted as an <a href="https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_etc2/">ETC2 compressed</a>
 * ````RGB```` image.
 */
const RGB_ETC2_Format = 37492;

/**
 * Texture format mode in which the texture is formatted as an <a href="https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_etc2/">ETC2 compressed</a>
 * ````RGBA```` image.
 */
const RGBA_ETC2_EAC_Format = 37496;

/**
 * Texture format mode in which the texture is formatted as an <a href="https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/">ATSC compressed</a>
 * ````RGBA```` image.
 */
const RGBA_ASTC_4x4_Format = 37808;

/**
 * Texture format mode in which the texture is formatted as an <a href="https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/">ATSC compressed</a>
 * ````RGBA```` image.
 */
const RGBA_ASTC_5x4_Format = 37809;

/**
 * Texture format mode in which the texture is formatted as an <a href="https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/">ATSC compressed</a>
 * ````RGBA```` image.
 */
const RGBA_ASTC_5x5_Format = 37810;

/**
 * Texture format mode in which the texture is formatted as an <a href="https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/">ATSC compressed</a>
 * ````RGBA```` image.
 */
const RGBA_ASTC_6x5_Format = 37811;

/**
 * Texture format mode in which the texture is formatted as an <a href="https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/">ATSC compressed</a>
 * ````RGBA```` image.
 */
const RGBA_ASTC_6x6_Format = 37812;

/**
 * Texture format mode in which the texture is formatted as an <a href="https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/">ATSC compressed</a>
 * ````RGBA```` image.
 */
const RGBA_ASTC_8x5_Format = 37813;

/**
 * Texture format mode in which the texture is formatted as an <a href="https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/">ATSC compressed</a>
 * ````RGBA```` image.
 */
const RGBA_ASTC_8x6_Format = 37814;

/**
 * Texture format mode in which the texture is formatted as an <a href="https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/">ATSC compressed</a>
 * ````RGBA```` image.
 */
const RGBA_ASTC_8x8_Format = 37815;

/**
 * Texture format mode in which the texture is formatted as an <a href="https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/">ATSC compressed</a>
 * ````RGBA```` image.
 */
const RGBA_ASTC_10x5_Format = 37816;

/**
 * Texture format mode in which the texture is formatted as an <a href="https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/">ATSC compressed</a>
 * ````RGBA```` image.
 */
const RGBA_ASTC_10x6_Format = 37817;

/**
 * Texture format mode in which the texture is formatted as an <a href="https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/">ATSC compressed</a>
 * ````RGBA```` image.
 */
const RGBA_ASTC_10x8_Format = 37818;

/**
 * Texture format mode in which the texture is formatted as an <a href="https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/">ATSC compressed</a>
 * ````RGBA```` image.
 */
const RGBA_ASTC_10x10_Format = 37819;

/**
 * Texture format mode in which the texture is formatted as an <a href="https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/">ATSC compressed</a>
 * ````RGBA```` image.
 */
const RGBA_ASTC_12x10_Format = 37820;

/**
 * Texture format mode in which the texture is formatted as an <a href="https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/">ATSC compressed</a>
 * ````RGBA```` image.
 */
const RGBA_ASTC_12x12_Format = 37821;

/**
 * Texture format mode in which the texture is formatted as an <a href="https://www.khronos.org/opengl/wiki/BPTC_Texture_Compression">BPTC compressed</a>
 * ````RGBA```` image.
 */
const RGBA_BPTC_Format = 36492;

/**
 * Texture encoding mode in which the texture image is in linear color space.
 */
const LinearEncoding = 3000;

/**
 * Texture encoding mode in which the texture image is in sRGB color space.
 */
const sRGBEncoding = 3001;

/**
 * Media type for JPEG images.
 */
const JPEGMediaType = 10001;

/**
 * Media type for PNG images.
 */
const PNGMediaType = 10002;

/**
 * @private
 */

const DrawShaderSource = function (mesh) {
    if (mesh._material._state.type === "LambertMaterial") {
        this.vertex = buildVertexLambert(mesh);
        this.fragment = buildFragmentLambert(mesh);
    } else {
        this.vertex = buildVertexDraw(mesh);
        this.fragment = buildFragmentDraw(mesh);
    }
};

const TEXTURE_DECODE_FUNCS$1 = {};
TEXTURE_DECODE_FUNCS$1[LinearEncoding] = "linearToLinear";
TEXTURE_DECODE_FUNCS$1[sRGBEncoding] = "sRGBToLinear";

function getReceivesShadow(mesh) {
    if (!mesh.receivesShadow) {
        return false;
    }
    const lights = mesh.scene._lightsState.lights;
    if (!lights || lights.length === 0) {
        return false;
    }
    for (let i = 0, len = lights.length; i < len; i++) {
        if (lights[i].castsShadow) {
            return true;
        }
    }
    return false;
}

function hasTextures(mesh) {
    if (!mesh._geometry._state.uvBuf) {
        return false;
    }
    const material = mesh._material;
    return !!(material._ambientMap ||
        material._occlusionMap ||
        material._baseColorMap ||
        material._diffuseMap ||
        material._alphaMap ||
        material._specularMap ||
        material._glossinessMap ||
        material._specularGlossinessMap ||
        material._emissiveMap ||
        material._metallicMap ||
        material._roughnessMap ||
        material._metallicRoughnessMap ||
        material._reflectivityMap ||
        material._normalMap);
}

function hasNormals$1(mesh) {
    const primitive = mesh._geometry._state.primitiveName;
    if ((mesh._geometry._state.autoVertexNormals || mesh._geometry._state.normalsBuf) && (primitive === "triangles" || primitive === "triangle-strip" || primitive === "triangle-fan")) {
        return true;
    }
    return false;
}

function buildVertexLambert(mesh) {

    const scene = mesh.scene;
    const sectionPlanesState = mesh.scene._sectionPlanesState;
    const lightsState = mesh.scene._lightsState;
    const geometryState = mesh._geometry._state;
    const billboard = mesh._state.billboard;
    const stationary = mesh._state.stationary;
    const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
    const quantizedGeometry = !!geometryState.compressGeometry;

    const src = [];
    src.push("#version 300 es");
    src.push("// Lambertian drawing vertex shader");
    src.push("in vec3 position;");
    src.push("uniform mat4 modelMatrix;");
    src.push("uniform mat4 viewMatrix;");
    src.push("uniform mat4 projMatrix;");
    src.push("uniform vec4 colorize;");
    src.push("uniform vec3 offset;");
    if (quantizedGeometry) {
        src.push("uniform mat4 positionsDecodeMatrix;");
    }
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("uniform float logDepthBufFC;");
        src.push("out float vFragDepth;");
        src.push("bool isPerspectiveMatrix(mat4 m) {");
        src.push("    return (m[2][3] == - 1.0);");
        src.push("}");
        src.push("out float isPerspective;");
    }
    if (clipping) {
        src.push("out vec4 vWorldPosition;");
    }
    src.push("uniform vec4 lightAmbient;");
    src.push("uniform vec4 materialColor;");
    src.push("uniform vec3 materialEmissive;");
    if (geometryState.normalsBuf) {
        src.push("in vec3 normal;");
        src.push("uniform mat4 modelNormalMatrix;");
        src.push("uniform mat4 viewNormalMatrix;");
        for (let i = 0, len = lightsState.lights.length; i < len; i++) {
            const light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            src.push("uniform vec4 lightColor" + i + ";");
            if (light.type === "dir") {
                src.push("uniform vec3 lightDir" + i + ";");
            }
            if (light.type === "point") {
                src.push("uniform vec3 lightPos" + i + ";");
            }
            if (light.type === "spot") {
                src.push("uniform vec3 lightPos" + i + ";");
                src.push("uniform vec3 lightDir" + i + ";");
            }
        }
        if (quantizedGeometry) {
            src.push("vec3 octDecode(vec2 oct) {");
            src.push("    vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));");
            src.push("    if (v.z < 0.0) {");
            src.push("        v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);");
            src.push("    }");
            src.push("    return normalize(v);");
            src.push("}");
        }
    }
    src.push("out vec4 vColor;");
    if (geometryState.primitiveName === "points") {
        src.push("uniform float pointSize;");
    }
    if (billboard === "spherical" || billboard === "cylindrical") {
        src.push("void billboard(inout mat4 mat) {");
        src.push("   mat[0][0] = 1.0;");
        src.push("   mat[0][1] = 0.0;");
        src.push("   mat[0][2] = 0.0;");
        if (billboard === "spherical") {
            src.push("   mat[1][0] = 0.0;");
            src.push("   mat[1][1] = 1.0;");
            src.push("   mat[1][2] = 0.0;");
        }
        src.push("   mat[2][0] = 0.0;");
        src.push("   mat[2][1] = 0.0;");
        src.push("   mat[2][2] =1.0;");
        src.push("}");
    }
    src.push("void main(void) {");
    src.push("vec4 localPosition = vec4(position, 1.0); ");
    src.push("vec4 worldPosition;");
    if (quantizedGeometry) {
        src.push("localPosition = positionsDecodeMatrix * localPosition;");
    }
    if (geometryState.normalsBuf) {
        if (quantizedGeometry) {
            src.push("vec4 localNormal = vec4(octDecode(normal.xy), 0.0); ");
        } else {
            src.push("vec4 localNormal = vec4(normal, 0.0); ");
        }
        src.push("mat4 modelNormalMatrix2 = modelNormalMatrix;");
        src.push("mat4 viewNormalMatrix2 = viewNormalMatrix;");
    }
    src.push("mat4 viewMatrix2 = viewMatrix;");
    src.push("mat4 modelMatrix2 = modelMatrix;");
    if (stationary) {
        src.push("viewMatrix2[3][0] = viewMatrix2[3][1] = viewMatrix2[3][2] = 0.0;");
    }
    if (billboard === "spherical" || billboard === "cylindrical") {
        src.push("mat4 modelViewMatrix = viewMatrix2 * modelMatrix2;");
        src.push("billboard(modelMatrix2);");
        src.push("billboard(viewMatrix2);");
        src.push("billboard(modelViewMatrix);");
        if (geometryState.normalsBuf) {
            src.push("mat4 modelViewNormalMatrix =  viewNormalMatrix2 * modelNormalMatrix2;");
            src.push("billboard(modelNormalMatrix2);");
            src.push("billboard(viewNormalMatrix2);");
            src.push("billboard(modelViewNormalMatrix);");
        }
        src.push("worldPosition = modelMatrix2 * localPosition;");
        src.push("worldPosition.xyz = worldPosition.xyz + offset;");
        src.push("vec4 viewPosition = modelViewMatrix * localPosition;");
    } else {
        src.push("worldPosition = modelMatrix2 * localPosition;");
        src.push("worldPosition.xyz = worldPosition.xyz + offset;");
        src.push("vec4 viewPosition  = viewMatrix2 * worldPosition; ");
    }
    if (geometryState.normalsBuf) {
        src.push("vec3 viewNormal = normalize((viewNormalMatrix2 * modelNormalMatrix2 * localNormal).xyz);");
    }
    src.push("vec3 reflectedColor = vec3(0.0, 0.0, 0.0);");
    src.push("vec3 viewLightDir = vec3(0.0, 0.0, -1.0);");
    src.push("float lambertian = 1.0;");
    if (geometryState.normalsBuf) {
        for (let i = 0, len = lightsState.lights.length; i < len; i++) {
            const light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            if (light.type === "dir") {
                if (light.space === "view") {
                    src.push("viewLightDir = normalize(lightDir" + i + ");");
                } else {
                    src.push("viewLightDir = normalize((viewMatrix2 * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "point") {
                if (light.space === "view") {
                    src.push("viewLightDir = -normalize(lightPos" + i + " - viewPosition.xyz);");
                } else {
                    src.push("viewLightDir = -normalize((viewMatrix2 * vec4(lightPos" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "spot") {
                if (light.space === "view") {
                    src.push("viewLightDir = normalize(lightDir" + i + ");");
                } else {
                    src.push("viewLightDir = normalize((viewMatrix2 * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else {
                continue;
            }
            src.push("lambertian = max(dot(-viewNormal, viewLightDir), 0.0);");
            src.push("reflectedColor += lambertian * (lightColor" + i + ".rgb * lightColor" + i + ".a);");
        }
    }
    src.push("vColor = vec4((lightAmbient.rgb * lightAmbient.a * materialColor.rgb) + materialEmissive.rgb + (reflectedColor * materialColor.rgb), materialColor.a) * colorize;"); // TODO: How to have ambient bright enough for canvas BG but not too bright for scene?
    if (clipping) {
        src.push("vWorldPosition = worldPosition;");
    }
    if (geometryState.primitiveName === "points") {
        src.push("gl_PointSize = pointSize;");
    }
    src.push("vec4 clipPos = projMatrix * viewPosition;");
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("vFragDepth = 1.0 + clipPos.w;");
        src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
    }
    src.push("gl_Position = clipPos;");
    src.push("}");
    return src;
}

function buildFragmentLambert(mesh) {
    const scene = mesh.scene;
    const sectionPlanesState = scene._sectionPlanesState;
    mesh._material._state;
    const geometryState = mesh._geometry._state;
    const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
    const gammaOutput = scene.gammaOutput; // If set, then it expects that all textures and colors need to be outputted in premultiplied gamma. Default is false.
    const src = [];
    src.push("#version 300 es");
    src.push("// Lambertian drawing fragment shader");
    src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
    src.push("precision highp float;");
    src.push("precision highp int;");
    src.push("#else");
    src.push("precision mediump float;");
    src.push("precision mediump int;");
    src.push("#endif");
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("in float isPerspective;");
        src.push("uniform float logDepthBufFC;");
        src.push("in float vFragDepth;");
    }
    if (clipping) {
        src.push("in vec4 vWorldPosition;");
        src.push("uniform bool clippable;");
        for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            src.push("uniform bool sectionPlaneActive" + i + ";");
            src.push("uniform vec3 sectionPlanePos" + i + ";");
            src.push("uniform vec3 sectionPlaneDir" + i + ";");
        }
    }
    src.push("in vec4 vColor;");
    if (gammaOutput) {
        src.push("uniform float gammaFactor;");
        src.push("    vec4 linearToGamma( in vec4 value, in float gammaFactor ) {");
        src.push("    return vec4( pow( value.xyz, vec3( 1.0 / gammaFactor ) ), value.w );");
        src.push("}");
    }
    src.push("out vec4 outColor;");
    src.push("void main(void) {");
    if (clipping) {
        src.push("if (clippable) {");
        src.push("  float dist = 0.0;");
        for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            src.push("if (sectionPlaneActive" + i + ") {");
            src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
            src.push("}");
        }
        src.push("  if (dist > 0.0) { discard; }");
        src.push("}");
    }
    if (geometryState.primitiveName === "points") {
        src.push("vec2 cxy = 2.0 * gl_PointCoord - 1.0;");
        src.push("float r = dot(cxy, cxy);");
        src.push("if (r > 1.0) {");
        src.push("   discard;");
        src.push("}");

    }
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
    }
    if (gammaOutput) {
        src.push("outColor = linearToGamma(vColor, gammaFactor);");
    } else {
        src.push("outColor = vColor;");
    }
    src.push("}");
    return src;
}

function buildVertexDraw(mesh) {
    const scene = mesh.scene;
    mesh._material;
    const meshState = mesh._state;
    const sectionPlanesState = scene._sectionPlanesState;
    const geometryState = mesh._geometry._state;
    const lightsState = scene._lightsState;
    let light;
    const billboard = meshState.billboard;
    const background = meshState.background;
    const stationary = meshState.stationary;
    const texturing = hasTextures(mesh);
    const normals = hasNormals$1(mesh);
    const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
    const receivesShadow = getReceivesShadow(mesh);
    const quantizedGeometry = !!geometryState.compressGeometry;
    const src = [];
    src.push("#version 300 es");
    src.push("// Drawing vertex shader");
    src.push("in  vec3 position;");
    if (quantizedGeometry) {
        src.push("uniform mat4 positionsDecodeMatrix;");
    }
    src.push("uniform  mat4 modelMatrix;");
    src.push("uniform  mat4 viewMatrix;");
    src.push("uniform  mat4 projMatrix;");
    src.push("out  vec3 vViewPosition;");
    src.push("uniform  vec3 offset;");
    if (clipping) {
        src.push("out vec4 vWorldPosition;");
    }
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("uniform float logDepthBufFC;");
        src.push("out float vFragDepth;");
        src.push("bool isPerspectiveMatrix(mat4 m) {");
        src.push("    return (m[2][3] == - 1.0);");
        src.push("}");
        src.push("out float isPerspective;");
    }
    if (lightsState.lightMaps.length > 0) {
        src.push("out    vec3 vWorldNormal;");
    }
    if (normals) {
        src.push("in  vec3 normal;");
        src.push("uniform    mat4 modelNormalMatrix;");
        src.push("uniform    mat4 viewNormalMatrix;");
        src.push("out    vec3 vViewNormal;");
        for (let i = 0, len = lightsState.lights.length; i < len; i++) {
            light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            if (light.type === "dir") {
                src.push("uniform vec3 lightDir" + i + ";");
            }
            if (light.type === "point") {
                src.push("uniform vec3 lightPos" + i + ";");
            }
            if (light.type === "spot") {
                src.push("uniform vec3 lightPos" + i + ";");
                src.push("uniform vec3 lightDir" + i + ";");
            }
            if (!(light.type === "dir" && light.space === "view")) {
                src.push("out vec4 vViewLightReverseDirAndDist" + i + ";");
            }
        }
        if (quantizedGeometry) {
            src.push("vec3 octDecode(vec2 oct) {");
            src.push("    vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));");
            src.push("    if (v.z < 0.0) {");
            src.push("        v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);");
            src.push("    }");
            src.push("    return normalize(v);");
            src.push("}");
        }
    }
    if (texturing) {
        src.push("in vec2 uv;");
        src.push("out vec2 vUV;");
        if (quantizedGeometry) {
            src.push("uniform mat3 uvDecodeMatrix;");
        }
    }
    if (geometryState.colors) {
        src.push("in vec4 color;");
        src.push("out vec4 vColor;");
    }
    if (geometryState.primitiveName === "points") {
        src.push("uniform float pointSize;");
    }
    if (billboard === "spherical" || billboard === "cylindrical") {
        src.push("void billboard(inout mat4 mat) {");
        src.push("   mat[0][0] = 1.0;");
        src.push("   mat[0][1] = 0.0;");
        src.push("   mat[0][2] = 0.0;");
        if (billboard === "spherical") {
            src.push("   mat[1][0] = 0.0;");
            src.push("   mat[1][1] = 1.0;");
            src.push("   mat[1][2] = 0.0;");
        }
        src.push("   mat[2][0] = 0.0;");
        src.push("   mat[2][1] = 0.0;");
        src.push("   mat[2][2] =1.0;");
        src.push("}");
    }
    if (receivesShadow) {
        src.push("const mat4 texUnitConverter = mat4(0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.5, 0.5, 0.5, 1.0);");
        for (let i = 0, len = lightsState.lights.length; i < len; i++) { // Light sources
            if (lightsState.lights[i].castsShadow) {
                src.push("uniform mat4 shadowViewMatrix" + i + ";");
                src.push("uniform mat4 shadowProjMatrix" + i + ";");
                src.push("out vec4 vShadowPosFromLight" + i + ";");
            }
        }
    }
    src.push("void main(void) {");
    src.push("vec4 localPosition = vec4(position, 1.0); ");
    src.push("vec4 worldPosition;");
    if (quantizedGeometry) {
        src.push("localPosition = positionsDecodeMatrix * localPosition;");
    }
    if (normals) {
        if (quantizedGeometry) {
            src.push("vec4 localNormal = vec4(octDecode(normal.xy), 0.0); ");
        } else {
            src.push("vec4 localNormal = vec4(normal, 0.0); ");
        }
        src.push("mat4 modelNormalMatrix2    = modelNormalMatrix;");
        src.push("mat4 viewNormalMatrix2     = viewNormalMatrix;");
    }
    src.push("mat4 viewMatrix2           = viewMatrix;");
    src.push("mat4 modelMatrix2          = modelMatrix;");
    if (stationary) {
        src.push("viewMatrix2[3][0] = viewMatrix2[3][1] = viewMatrix2[3][2] = 0.0;");
    } else if (background) {
        src.push("viewMatrix2[3] = vec4(0.0, 0.0, 0.0 ,1.0);");
    }
    if (billboard === "spherical" || billboard === "cylindrical") {
        src.push("mat4 modelViewMatrix = viewMatrix2 * modelMatrix2;");
        src.push("billboard(modelMatrix2);");
        src.push("billboard(viewMatrix2);");
        src.push("billboard(modelViewMatrix);");
        if (normals) {
            src.push("mat4 modelViewNormalMatrix =  viewNormalMatrix2 * modelNormalMatrix2;");
            src.push("billboard(modelNormalMatrix2);");
            src.push("billboard(viewNormalMatrix2);");
            src.push("billboard(modelViewNormalMatrix);");
        }
        src.push("worldPosition = modelMatrix2 * localPosition;");
        src.push("worldPosition.xyz = worldPosition.xyz + offset;");
        src.push("vec4 viewPosition = modelViewMatrix * localPosition;");
    } else {
        src.push("worldPosition = modelMatrix2 * localPosition;");
        src.push("worldPosition.xyz = worldPosition.xyz + offset;");
        src.push("vec4 viewPosition  = viewMatrix2 * worldPosition; ");
    }
    if (normals) {
        src.push("vec3 worldNormal = (modelNormalMatrix2 * localNormal).xyz; ");
        if (lightsState.lightMaps.length > 0) {
            src.push("vWorldNormal = worldNormal;");
        }
        src.push("vViewNormal = normalize((viewNormalMatrix2 * vec4(worldNormal, 1.0)).xyz);");
        src.push("vec3 tmpVec3;");
        src.push("float lightDist;");
        for (let i = 0, len = lightsState.lights.length; i < len; i++) { // Lights
            light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            if (light.type === "dir") {
                if (light.space === "world") {
                    src.push("tmpVec3 = vec3(viewMatrix2 * vec4(lightDir" + i + ", 0.0) ).xyz;");
                    src.push("vViewLightReverseDirAndDist" + i + " = vec4(-tmpVec3, 0.0);");
                }
            }
            if (light.type === "point") {
                if (light.space === "world") {
                    src.push("tmpVec3 = (viewMatrix2 * vec4(lightPos" + i + ", 1.0)).xyz - viewPosition.xyz;");
                    src.push("lightDist = abs(length(tmpVec3));");
                } else {
                    src.push("tmpVec3 = lightPos" + i + ".xyz - viewPosition.xyz;");
                    src.push("lightDist = abs(length(tmpVec3));");
                }
                src.push("vViewLightReverseDirAndDist" + i + " = vec4(tmpVec3, lightDist);");
            }
        }
    }
    if (texturing) {
        if (quantizedGeometry) {
            src.push("vUV = (uvDecodeMatrix * vec3(uv, 1.0)).xy;");
        } else {
            src.push("vUV = uv;");
        }
    }
    if (geometryState.colors) {
        src.push("vColor = color;");
    }
    if (geometryState.primitiveName === "points") {
        src.push("gl_PointSize = pointSize;");
    }
    if (clipping) {
        src.push("vWorldPosition = worldPosition;");
    }
    src.push("   vViewPosition = viewPosition.xyz;");
    src.push("vec4 clipPos = projMatrix * viewPosition;");
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("vFragDepth = 1.0 + clipPos.w;");
        src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
    }
    if (background) {
        src.push("clipPos.z = clipPos.w;");
    }
    src.push("gl_Position = clipPos;");
    if (receivesShadow) {
        src.push("const mat4 texUnitConverter = mat4(0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.5, 0.5, 0.5, 1.0);");
        src.push("vec4 tempx; ");
        for (let i = 0, len = lightsState.lights.length; i < len; i++) { // Light sources
            if (lightsState.lights[i].castsShadow) {
                src.push("vShadowPosFromLight" + i + " = texUnitConverter * shadowProjMatrix" + i + " * (shadowViewMatrix" + i + " * worldPosition); ");
            }
        }
    }
    src.push("}");
    return src;
}

function buildFragmentDraw(mesh) {

    const scene = mesh.scene;
    scene.canvas.gl;
    const material = mesh._material;
    const geometryState = mesh._geometry._state;
    const sectionPlanesState = mesh.scene._sectionPlanesState;
    const lightsState = mesh.scene._lightsState;
    const materialState = mesh._material._state;
    const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
    const normals = hasNormals$1(mesh);
    const uvs = geometryState.uvBuf;
    const phongMaterial = (materialState.type === "PhongMaterial");
    const metallicMaterial = (materialState.type === "MetallicMaterial");
    const specularMaterial = (materialState.type === "SpecularMaterial");
    const receivesShadow = getReceivesShadow(mesh);
    scene.gammaInput; // If set, then it expects that all textures and colors are premultiplied gamma. Default is false.
    const gammaOutput = scene.gammaOutput; // If set, then it expects that all textures and colors need to be outputted in premultiplied gamma. Default is false.
    const src = [];
    src.push("#version 300 es");
    src.push("// Drawing fragment shader");

    src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
    src.push("precision highp float;");
    src.push("precision highp int;");
    src.push("#else");
    src.push("precision mediump float;");
    src.push("precision mediump int;");
    src.push("#endif");

    if (scene.logarithmicDepthBufferEnabled) {
        src.push("in float isPerspective;");
        src.push("uniform float logDepthBufFC;");
        src.push("in float vFragDepth;");
    }

    if (receivesShadow) {
        src.push("float unpackDepth (vec4 color) {");
        src.push("  const vec4 bitShift = vec4(1.0, 1.0/256.0, 1.0/(256.0 * 256.0), 1.0/(256.0*256.0*256.0));");
        src.push("  return dot(color, bitShift);");
        src.push("}");
    }

    //--------------------------------------------------------------------------------
    // GAMMA CORRECTION
    //--------------------------------------------------------------------------------

    src.push("uniform float gammaFactor;");
    src.push("vec4 linearToLinear( in vec4 value ) {");
    src.push("  return value;");
    src.push("}");
    src.push("vec4 sRGBToLinear( in vec4 value ) {");
    src.push("  return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.w );");
    src.push("}");
    src.push("vec4 gammaToLinear( in vec4 value) {");
    src.push("  return vec4( pow( value.xyz, vec3( gammaFactor ) ), value.w );");
    src.push("}");
    if (gammaOutput) {
        src.push("vec4 linearToGamma( in vec4 value, in float gammaFactor ) {");
        src.push("  return vec4( pow( value.xyz, vec3( 1.0 / gammaFactor ) ), value.w );");
        src.push("}");
    }

    //--------------------------------------------------------------------------------
    // USER CLIP PLANES
    //--------------------------------------------------------------------------------

    if (clipping) {
        src.push("in vec4 vWorldPosition;");
        src.push("uniform bool clippable;");
        for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
            src.push("uniform bool sectionPlaneActive" + i + ";");
            src.push("uniform vec3 sectionPlanePos" + i + ";");
            src.push("uniform vec3 sectionPlaneDir" + i + ";");
        }
    }

    if (normals) {

        //--------------------------------------------------------------------------------
        // LIGHT AND REFLECTION MAP INPUTS
        // Define here so available globally to shader functions
        //--------------------------------------------------------------------------------

        if (lightsState.lightMaps.length > 0) {
            src.push("uniform samplerCube lightMap;");
            src.push("uniform mat4 viewNormalMatrix;");
        }
        if (lightsState.reflectionMaps.length > 0) {
            src.push("uniform samplerCube reflectionMap;");
        }
        if (lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0) {
            src.push("uniform mat4 viewMatrix;");
        }

        //--------------------------------------------------------------------------------
        // SHADING FUNCTIONS
        //--------------------------------------------------------------------------------

        // CONSTANT DEFINITIONS

        src.push("#define PI 3.14159265359");
        src.push("#define RECIPROCAL_PI 0.31830988618");
        src.push("#define RECIPROCAL_PI2 0.15915494");
        src.push("#define EPSILON 1e-6");

        src.push("#define saturate(a) clamp( a, 0.0, 1.0 )");

        // UTILITY DEFINITIONS

        src.push("vec3 inverseTransformDirection(in vec3 dir, in mat4 matrix) {");
        src.push("   return normalize( ( vec4( dir, 0.0 ) * matrix ).xyz );");
        src.push("}");

        // STRUCTURES

        src.push("struct IncidentLight {");
        src.push("   vec3 color;");
        src.push("   vec3 direction;");
        src.push("};");

        src.push("struct ReflectedLight {");
        src.push("   vec3 diffuse;");
        src.push("   vec3 specular;");
        src.push("};");

        src.push("struct Geometry {");
        src.push("   vec3 position;");
        src.push("   vec3 viewNormal;");
        src.push("   vec3 worldNormal;");
        src.push("   vec3 viewEyeDir;");
        src.push("};");

        src.push("struct Material {");
        src.push("   vec3    diffuseColor;");
        src.push("   float   specularRoughness;");
        src.push("   vec3    specularColor;");
        src.push("   float   shine;"); // Only used for Phong
        src.push("};");

        // COMMON UTILS

        if (phongMaterial) {

            if (lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0) {

                src.push("void computePhongLightMapping(const in Geometry geometry, const in Material material, inout ReflectedLight reflectedLight) {");

                if (lightsState.lightMaps.length > 0) {
                    src.push("   vec3 irradiance = " + TEXTURE_DECODE_FUNCS$1[lightsState.lightMaps[0].encoding] + "(texture(lightMap, geometry.worldNormal)).rgb;");
                    src.push("   irradiance *= PI;");
                    src.push("   vec3 diffuseBRDFContrib = (RECIPROCAL_PI * material.diffuseColor);");
                    src.push("   reflectedLight.diffuse += irradiance * diffuseBRDFContrib;");
                }
                if (lightsState.reflectionMaps.length > 0) {
                    src.push("   vec3 reflectVec             = reflect(-geometry.viewEyeDir, geometry.viewNormal);");
                    src.push("   vec3 radiance               = texture(reflectionMap, reflectVec).rgb * 0.2;");
                    src.push("   radiance *= PI;");
                    src.push("   reflectedLight.specular     += radiance;");
                }
                src.push("}");
            }

            src.push("void computePhongLighting(const in IncidentLight directLight, const in Geometry geometry, const in Material material, inout ReflectedLight reflectedLight) {");
            src.push("   float dotNL     = saturate(dot(geometry.viewNormal, directLight.direction));");
            src.push("   vec3 irradiance = dotNL * directLight.color * PI;");
            src.push("   reflectedLight.diffuse  += irradiance * (RECIPROCAL_PI * material.diffuseColor);");
            src.push("   reflectedLight.specular += directLight.color * material.specularColor * pow(max(dot(reflect(-directLight.direction, -geometry.viewNormal), geometry.viewEyeDir), 0.0), material.shine);");
            src.push("}");
        }

        if (metallicMaterial || specularMaterial) {

            // IRRADIANCE EVALUATION

            src.push("float GGXRoughnessToBlinnExponent(const in float ggxRoughness) {");
            src.push("   float r = ggxRoughness + 0.0001;");
            src.push("   return (2.0 / (r * r) - 2.0);");
            src.push("}");

            src.push("float getSpecularMIPLevel(const in float blinnShininessExponent, const in int maxMIPLevel) {");
            src.push("   float maxMIPLevelScalar = float( maxMIPLevel );");
            src.push("   float desiredMIPLevel = maxMIPLevelScalar - 0.79248 - 0.5 * log2( ( blinnShininessExponent * blinnShininessExponent ) + 1.0 );");
            src.push("   return clamp( desiredMIPLevel, 0.0, maxMIPLevelScalar );");
            src.push("}");

            if (lightsState.reflectionMaps.length > 0) {
                src.push("vec3 getLightProbeIndirectRadiance(const in vec3 reflectVec, const in float blinnShininessExponent, const in int maxMIPLevel) {");
                src.push("   float mipLevel = 0.5 * getSpecularMIPLevel(blinnShininessExponent, maxMIPLevel);"); //TODO: a random factor - fix this
                src.push("   vec3 envMapColor = " + TEXTURE_DECODE_FUNCS$1[lightsState.reflectionMaps[0].encoding] + "(texture(reflectionMap, reflectVec, mipLevel)).rgb;");
                src.push("  return envMapColor;");
                src.push("}");
            }

            // SPECULAR BRDF EVALUATION

            src.push("vec3 F_Schlick(const in vec3 specularColor, const in float dotLH) {");
            src.push("   float fresnel = exp2( ( -5.55473 * dotLH - 6.98316 ) * dotLH );");
            src.push("   return ( 1.0 - specularColor ) * fresnel + specularColor;");
            src.push("}");

            src.push("float G_GGX_Smith(const in float alpha, const in float dotNL, const in float dotNV) {");
            src.push("   float a2 = ( alpha * alpha );");
            src.push("   float gl = dotNL + sqrt( a2 + ( 1.0 - a2 ) * ( dotNL * dotNL ) );");
            src.push("   float gv = dotNV + sqrt( a2 + ( 1.0 - a2 ) * ( dotNV * dotNV ) );");
            src.push("   return 1.0 / ( gl * gv );");
            src.push("}");

            src.push("float G_GGX_SmithCorrelated(const in float alpha, const in float dotNL, const in float dotNV) {");
            src.push("   float a2 = ( alpha * alpha );");
            src.push("   float gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * ( dotNV * dotNV ) );");
            src.push("   float gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * ( dotNL * dotNL ) );");
            src.push("   return 0.5 / max( gv + gl, EPSILON );");
            src.push("}");

            src.push("float D_GGX(const in float alpha, const in float dotNH) {");
            src.push("   float a2 = ( alpha * alpha );");
            src.push("   float denom = ( dotNH * dotNH) * ( a2 - 1.0 ) + 1.0;");
            src.push("   return RECIPROCAL_PI * a2 / ( denom * denom);");
            src.push("}");

            src.push("vec3 BRDF_Specular_GGX(const in IncidentLight incidentLight, const in Geometry geometry, const in vec3 specularColor, const in float roughness) {");
            src.push("   float alpha = ( roughness * roughness );");
            src.push("   vec3 halfDir = normalize( incidentLight.direction + geometry.viewEyeDir );");
            src.push("   float dotNL = saturate( dot( geometry.viewNormal, incidentLight.direction ) );");
            src.push("   float dotNV = saturate( dot( geometry.viewNormal, geometry.viewEyeDir ) );");
            src.push("   float dotNH = saturate( dot( geometry.viewNormal, halfDir ) );");
            src.push("   float dotLH = saturate( dot( incidentLight.direction, halfDir ) );");
            src.push("   vec3  F = F_Schlick( specularColor, dotLH );");
            src.push("   float G = G_GGX_SmithCorrelated( alpha, dotNL, dotNV );");
            src.push("   float D = D_GGX( alpha, dotNH );");
            src.push("   return F * (G * D);");
            src.push("}");

            src.push("vec3 BRDF_Specular_GGX_Environment(const in Geometry geometry, const in vec3 specularColor, const in float roughness) {");
            src.push("   float dotNV = saturate(dot(geometry.viewNormal, geometry.viewEyeDir));");
            src.push("   const vec4 c0 = vec4( -1, -0.0275, -0.572,  0.022);");
            src.push("   const vec4 c1 = vec4(  1,  0.0425,   1.04, -0.04);");
            src.push("   vec4 r = roughness * c0 + c1;");
            src.push("   float a004 = min(r.x * r.x, exp2(-9.28 * dotNV)) * r.x + r.y;");
            src.push("   vec2 AB    = vec2(-1.04, 1.04) * a004 + r.zw;");
            src.push("   return specularColor * AB.x + AB.y;");
            src.push("}");

            if (lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0) {

                src.push("void computePBRLightMapping(const in Geometry geometry, const in Material material, inout ReflectedLight reflectedLight) {");
                if (lightsState.lightMaps.length > 0) {
                    src.push("   vec3 irradiance = sRGBToLinear(texture(lightMap, geometry.worldNormal)).rgb;");
                    src.push("   irradiance *= PI;");
                    src.push("   vec3 diffuseBRDFContrib = (RECIPROCAL_PI * material.diffuseColor);");
                    src.push("   reflectedLight.diffuse += irradiance * diffuseBRDFContrib;");
                    //   src.push("   reflectedLight.diffuse = vec3(1.0, 0.0, 0.0);");
                }
                if (lightsState.reflectionMaps.length > 0) {
                    src.push("   vec3 reflectVec             = reflect(-geometry.viewEyeDir, geometry.viewNormal);");
                    src.push("   reflectVec                  = inverseTransformDirection(reflectVec, viewMatrix);");
                    src.push("   float blinnExpFromRoughness = GGXRoughnessToBlinnExponent(material.specularRoughness);");
                    src.push("   vec3 radiance               = getLightProbeIndirectRadiance(reflectVec, blinnExpFromRoughness, 8);");
                    src.push("   vec3 specularBRDFContrib    = BRDF_Specular_GGX_Environment(geometry, material.specularColor, material.specularRoughness);");
                    src.push("   reflectedLight.specular     += radiance * specularBRDFContrib;");
                }
                src.push("}");
            }

            // MAIN LIGHTING COMPUTATION FUNCTION

            src.push("void computePBRLighting(const in IncidentLight incidentLight, const in Geometry geometry, const in Material material, inout ReflectedLight reflectedLight) {");
            src.push("   float dotNL     = saturate(dot(geometry.viewNormal, incidentLight.direction));");
            src.push("   vec3 irradiance = dotNL * incidentLight.color * PI;");
            src.push("   reflectedLight.diffuse  += irradiance * (RECIPROCAL_PI * material.diffuseColor);");
            src.push("   reflectedLight.specular += irradiance * BRDF_Specular_GGX(incidentLight, geometry, material.specularColor, material.specularRoughness);");
            src.push("}");

        } // (metallicMaterial || specularMaterial)

    } // geometry.normals

    //--------------------------------------------------------------------------------
    // GEOMETRY INPUTS
    //--------------------------------------------------------------------------------

    src.push("in vec3 vViewPosition;");

    if (geometryState.colors) {
        src.push("in vec4 vColor;");
    }

    if (uvs &&
        ((normals && material._normalMap)
            || material._ambientMap
            || material._baseColorMap
            || material._diffuseMap
            || material._emissiveMap
            || material._metallicMap
            || material._roughnessMap
            || material._metallicRoughnessMap
            || material._specularMap
            || material._glossinessMap
            || material._specularGlossinessMap
            || material._occlusionMap
            || material._alphaMap)) {
        src.push("in vec2 vUV;");
    }

    if (normals) {
        if (lightsState.lightMaps.length > 0) {
            src.push("in vec3 vWorldNormal;");
        }
        src.push("in vec3 vViewNormal;");
    }

    //--------------------------------------------------------------------------------
    // MATERIAL CHANNEL INPUTS
    //--------------------------------------------------------------------------------

    if (materialState.ambient) {
        src.push("uniform vec3 materialAmbient;");
    }
    if (materialState.baseColor) {
        src.push("uniform vec3 materialBaseColor;");
    }
    if (materialState.alpha !== undefined && materialState.alpha !== null) {
        src.push("uniform vec4 materialAlphaModeCutoff;"); // [alpha, alphaMode, alphaCutoff]
    }
    if (materialState.emissive) {
        src.push("uniform vec3 materialEmissive;");
    }
    if (materialState.diffuse) {
        src.push("uniform vec3 materialDiffuse;");
    }
    if (materialState.glossiness !== undefined && materialState.glossiness !== null) {
        src.push("uniform float materialGlossiness;");
    }
    if (materialState.shininess !== undefined && materialState.shininess !== null) {
        src.push("uniform float materialShininess;");  // Phong channel
    }
    if (materialState.specular) {
        src.push("uniform vec3 materialSpecular;");
    }
    if (materialState.metallic !== undefined && materialState.metallic !== null) {
        src.push("uniform float materialMetallic;");
    }
    if (materialState.roughness !== undefined && materialState.roughness !== null) {
        src.push("uniform float materialRoughness;");
    }
    if (materialState.specularF0 !== undefined && materialState.specularF0 !== null) {
        src.push("uniform float materialSpecularF0;");
    }

    //--------------------------------------------------------------------------------
    // MATERIAL TEXTURE INPUTS
    //--------------------------------------------------------------------------------

    if (uvs && material._ambientMap) {
        src.push("uniform sampler2D ambientMap;");
        if (material._ambientMap._state.matrix) {
            src.push("uniform mat4 ambientMapMatrix;");
        }
    }
    if (uvs && material._baseColorMap) {
        src.push("uniform sampler2D baseColorMap;");
        if (material._baseColorMap._state.matrix) {
            src.push("uniform mat4 baseColorMapMatrix;");
        }
    }
    if (uvs && material._diffuseMap) {
        src.push("uniform sampler2D diffuseMap;");
        if (material._diffuseMap._state.matrix) {
            src.push("uniform mat4 diffuseMapMatrix;");
        }
    }
    if (uvs && material._emissiveMap) {
        src.push("uniform sampler2D emissiveMap;");
        if (material._emissiveMap._state.matrix) {
            src.push("uniform mat4 emissiveMapMatrix;");
        }
    }
    if (normals && uvs && material._metallicMap) {
        src.push("uniform sampler2D metallicMap;");
        if (material._metallicMap._state.matrix) {
            src.push("uniform mat4 metallicMapMatrix;");
        }
    }
    if (normals && uvs && material._roughnessMap) {
        src.push("uniform sampler2D roughnessMap;");
        if (material._roughnessMap._state.matrix) {
            src.push("uniform mat4 roughnessMapMatrix;");
        }
    }
    if (normals && uvs && material._metallicRoughnessMap) {
        src.push("uniform sampler2D metallicRoughnessMap;");
        if (material._metallicRoughnessMap._state.matrix) {
            src.push("uniform mat4 metallicRoughnessMapMatrix;");
        }
    }
    if (normals && material._normalMap) {
        src.push("uniform sampler2D normalMap;");
        if (material._normalMap._state.matrix) {
            src.push("uniform mat4 normalMapMatrix;");
        }
        src.push("vec3 perturbNormal2Arb( vec3 eye_pos, vec3 surf_norm, vec2 uv ) {");
        src.push("      vec3 q0 = vec3( dFdx( eye_pos.x ), dFdx( eye_pos.y ), dFdx( eye_pos.z ) );");
        src.push("      vec3 q1 = vec3( dFdy( eye_pos.x ), dFdy( eye_pos.y ), dFdy( eye_pos.z ) );");
        src.push("      vec2 st0 = dFdx( uv.st );");
        src.push("      vec2 st1 = dFdy( uv.st );");
        src.push("      vec3 S = normalize( q0 * st1.t - q1 * st0.t );");
        src.push("      vec3 T = normalize( -q0 * st1.s + q1 * st0.s );");
        src.push("      vec3 N = normalize( surf_norm );");
        src.push("      vec3 mapN = texture( normalMap, uv ).xyz * 2.0 - 1.0;");
        src.push("      mat3 tsn = mat3( S, T, N );");
        //     src.push("      mapN *= 3.0;");
        src.push("      return normalize( tsn * mapN );");
        src.push("}");
    }
    if (uvs && material._occlusionMap) {
        src.push("uniform sampler2D occlusionMap;");
        if (material._occlusionMap._state.matrix) {
            src.push("uniform mat4 occlusionMapMatrix;");
        }
    }
    if (uvs && material._alphaMap) {
        src.push("uniform sampler2D alphaMap;");
        if (material._alphaMap._state.matrix) {
            src.push("uniform mat4 alphaMapMatrix;");
        }
    }
    if (normals && uvs && material._specularMap) {
        src.push("uniform sampler2D specularMap;");
        if (material._specularMap._state.matrix) {
            src.push("uniform mat4 specularMapMatrix;");
        }
    }
    if (normals && uvs && material._glossinessMap) {
        src.push("uniform sampler2D glossinessMap;");
        if (material._glossinessMap._state.matrix) {
            src.push("uniform mat4 glossinessMapMatrix;");
        }
    }
    if (normals && uvs && material._specularGlossinessMap) {
        src.push("uniform sampler2D materialSpecularGlossinessMap;");
        if (material._specularGlossinessMap._state.matrix) {
            src.push("uniform mat4 materialSpecularGlossinessMapMatrix;");
        }
    }

    //--------------------------------------------------------------------------------
    // MATERIAL FRESNEL INPUTS
    //--------------------------------------------------------------------------------

    if (normals && (material._diffuseFresnel ||
        material._specularFresnel ||
        material._alphaFresnel ||
        material._emissiveFresnel ||
        material._reflectivityFresnel)) {
        src.push("float fresnel(vec3 eyeDir, vec3 normal, float edgeBias, float centerBias, float power) {");
        src.push("    float fr = abs(dot(eyeDir, normal));");
        src.push("    float finalFr = clamp((fr - edgeBias) / (centerBias - edgeBias), 0.0, 1.0);");
        src.push("    return pow(finalFr, power);");
        src.push("}");
        if (material._diffuseFresnel) {
            src.push("uniform float  diffuseFresnelCenterBias;");
            src.push("uniform float  diffuseFresnelEdgeBias;");
            src.push("uniform float  diffuseFresnelPower;");
            src.push("uniform vec3   diffuseFresnelCenterColor;");
            src.push("uniform vec3   diffuseFresnelEdgeColor;");
        }
        if (material._specularFresnel) {
            src.push("uniform float  specularFresnelCenterBias;");
            src.push("uniform float  specularFresnelEdgeBias;");
            src.push("uniform float  specularFresnelPower;");
            src.push("uniform vec3   specularFresnelCenterColor;");
            src.push("uniform vec3   specularFresnelEdgeColor;");
        }
        if (material._alphaFresnel) {
            src.push("uniform float  alphaFresnelCenterBias;");
            src.push("uniform float  alphaFresnelEdgeBias;");
            src.push("uniform float  alphaFresnelPower;");
            src.push("uniform vec3   alphaFresnelCenterColor;");
            src.push("uniform vec3   alphaFresnelEdgeColor;");
        }
        if (material._reflectivityFresnel) {
            src.push("uniform float  materialSpecularF0FresnelCenterBias;");
            src.push("uniform float  materialSpecularF0FresnelEdgeBias;");
            src.push("uniform float  materialSpecularF0FresnelPower;");
            src.push("uniform vec3   materialSpecularF0FresnelCenterColor;");
            src.push("uniform vec3   materialSpecularF0FresnelEdgeColor;");
        }
        if (material._emissiveFresnel) {
            src.push("uniform float  emissiveFresnelCenterBias;");
            src.push("uniform float  emissiveFresnelEdgeBias;");
            src.push("uniform float  emissiveFresnelPower;");
            src.push("uniform vec3   emissiveFresnelCenterColor;");
            src.push("uniform vec3   emissiveFresnelEdgeColor;");
        }
    }

    //--------------------------------------------------------------------------------
    // LIGHT SOURCES
    //--------------------------------------------------------------------------------

    src.push("uniform vec4   lightAmbient;");

    if (normals) {
        for (let i = 0, len = lightsState.lights.length; i < len; i++) { // Light sources
            const light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            src.push("uniform vec4 lightColor" + i + ";");
            if (light.type === "point") {
                src.push("uniform vec3 lightAttenuation" + i + ";");
            }
            if (light.type === "dir" && light.space === "view") {
                src.push("uniform vec3 lightDir" + i + ";");
            }
            if (light.type === "point" && light.space === "view") {
                src.push("uniform vec3 lightPos" + i + ";");
            } else {
                src.push("in vec4 vViewLightReverseDirAndDist" + i + ";");
            }
        }
    }

    if (receivesShadow) {

        // Variance castsShadow mapping filter

        // src.push("float linstep(float low, float high, float v){");
        // src.push("      return clamp((v-low)/(high-low), 0.0, 1.0);");
        // src.push("}");
        //
        // src.push("float VSM(sampler2D depths, vec2 uv, float compare){");
        // src.push("      vec2 moments = texture(depths, uv).xy;");
        // src.push("      float p = smoothstep(compare-0.02, compare, moments.x);");
        // src.push("      float variance = max(moments.y - moments.x*moments.x, -0.001);");
        // src.push("      float d = compare - moments.x;");
        // src.push("      float p_max = linstep(0.2, 1.0, variance / (variance + d*d));");
        // src.push("      return clamp(max(p, p_max), 0.0, 1.0);");
        // src.push("}");

        for (let i = 0, len = lightsState.lights.length; i < len; i++) { // Light sources
            if (lightsState.lights[i].castsShadow) {
                src.push("in vec4 vShadowPosFromLight" + i + ";");
                src.push("uniform sampler2D shadowMap" + i + ";");
            }
        }
    }

    src.push("uniform vec4 colorize;");

    //================================================================================
    // MAIN
    //================================================================================
    src.push("out vec4 outColor;");
    src.push("void main(void) {");

    if (clipping) {
        src.push("if (clippable) {");
        src.push("  float dist = 0.0;");
        for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
            src.push("if (sectionPlaneActive" + i + ") {");
            src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
            src.push("}");
        }
        src.push("  if (dist > 0.0) { discard; }");
        src.push("}");
    }

    if (geometryState.primitiveName === "points") {
        src.push("vec2 cxy = 2.0 * gl_PointCoord - 1.0;");
        src.push("float r = dot(cxy, cxy);");
        src.push("if (r > 1.0) {");
        src.push("   discard;");
        src.push("}");
    }

    src.push("float occlusion = 1.0;");

    if (materialState.ambient) {
        src.push("vec3 ambientColor = materialAmbient;");
    } else {
        src.push("vec3 ambientColor = vec3(1.0, 1.0, 1.0);");
    }

    if (materialState.diffuse) {
        src.push("vec3 diffuseColor = materialDiffuse;");
    } else if (materialState.baseColor) {
        src.push("vec3 diffuseColor = materialBaseColor;");
    } else {
        src.push("vec3 diffuseColor = vec3(1.0, 1.0, 1.0);");
    }

    if (geometryState.colors) {
        src.push("diffuseColor *= vColor.rgb;");
    }

    if (materialState.emissive) {
        src.push("vec3 emissiveColor = materialEmissive;"); // Emissive default is (0,0,0), so initializing here
    } else {
        src.push("vec3  emissiveColor = vec3(0.0, 0.0, 0.0);");
    }

    if (materialState.specular) {
        src.push("vec3 specular = materialSpecular;");
    } else {
        src.push("vec3 specular = vec3(1.0, 1.0, 1.0);");
    }

    if (materialState.alpha !== undefined) {
        src.push("float alpha = materialAlphaModeCutoff[0];");
    } else {
        src.push("float alpha = 1.0;");
    }

    if (geometryState.colors) {
        src.push("alpha *= vColor.a;");
    }

    if (materialState.glossiness !== undefined) {
        src.push("float glossiness = materialGlossiness;");
    } else {
        src.push("float glossiness = 1.0;");
    }

    if (materialState.metallic !== undefined) {
        src.push("float metallic = materialMetallic;");
    } else {
        src.push("float metallic = 1.0;");
    }

    if (materialState.roughness !== undefined) {
        src.push("float roughness = materialRoughness;");
    } else {
        src.push("float roughness = 1.0;");
    }

    if (materialState.specularF0 !== undefined) {
        src.push("float specularF0 = materialSpecularF0;");
    } else {
        src.push("float specularF0 = 1.0;");
    }

    //--------------------------------------------------------------------------------
    // TEXTURING
    //--------------------------------------------------------------------------------

    if (uvs && ((normals && material._normalMap)
        || material._ambientMap
        || material._baseColorMap
        || material._diffuseMap
        || material._occlusionMap
        || material._emissiveMap
        || material._metallicMap
        || material._roughnessMap
        || material._metallicRoughnessMap
        || material._specularMap
        || material._glossinessMap
        || material._specularGlossinessMap
        || material._alphaMap)) {
        src.push("vec4 texturePos = vec4(vUV.s, vUV.t, 1.0, 1.0);");
        src.push("vec2 textureCoord;");
    }

    if (uvs && material._ambientMap) {
        if (material._ambientMap._state.matrix) {
            src.push("textureCoord = (ambientMapMatrix * texturePos).xy;");
        } else {
            src.push("textureCoord = texturePos.xy;");
        }
        src.push("vec4 ambientTexel = texture(ambientMap, textureCoord).rgb;");
        src.push("ambientTexel = " + TEXTURE_DECODE_FUNCS$1[material._ambientMap._state.encoding] + "(ambientTexel);");
        src.push("ambientColor *= ambientTexel.rgb;");
    }

    if (uvs && material._diffuseMap) {
        if (material._diffuseMap._state.matrix) {
            src.push("textureCoord = (diffuseMapMatrix * texturePos).xy;");
        } else {
            src.push("textureCoord = texturePos.xy;");
        }
        src.push("vec4 diffuseTexel = texture(diffuseMap, textureCoord);");
        src.push("diffuseTexel = " + TEXTURE_DECODE_FUNCS$1[material._diffuseMap._state.encoding] + "(diffuseTexel);");
        src.push("diffuseColor *= diffuseTexel.rgb;");
        src.push("alpha *= diffuseTexel.a;");
    }

    if (uvs && material._baseColorMap) {
        if (material._baseColorMap._state.matrix) {
            src.push("textureCoord = (baseColorMapMatrix * texturePos).xy;");
        } else {
            src.push("textureCoord = texturePos.xy;");
        }
        src.push("vec4 baseColorTexel = texture(baseColorMap, textureCoord);");
        src.push("baseColorTexel = " + TEXTURE_DECODE_FUNCS$1[material._baseColorMap._state.encoding] + "(baseColorTexel);");
        src.push("diffuseColor *= baseColorTexel.rgb;");
        src.push("alpha *= baseColorTexel.a;");
    }

    if (uvs && material._emissiveMap) {
        if (material._emissiveMap._state.matrix) {
            src.push("textureCoord = (emissiveMapMatrix * texturePos).xy;");
        } else {
            src.push("textureCoord = texturePos.xy;");
        }
        src.push("vec4 emissiveTexel = texture(emissiveMap, textureCoord);");
        src.push("emissiveTexel = " + TEXTURE_DECODE_FUNCS$1[material._emissiveMap._state.encoding] + "(emissiveTexel);");
        src.push("emissiveColor = emissiveTexel.rgb;");
    }

    if (uvs && material._alphaMap) {
        if (material._alphaMap._state.matrix) {
            src.push("textureCoord = (alphaMapMatrix * texturePos).xy;");
        } else {
            src.push("textureCoord = texturePos.xy;");
        }
        src.push("alpha *= texture(alphaMap, textureCoord).r;");
    }

    if (uvs && material._occlusionMap) {
        if (material._occlusionMap._state.matrix) {
            src.push("textureCoord = (occlusionMapMatrix * texturePos).xy;");
        } else {
            src.push("textureCoord = texturePos.xy;");
        }
        src.push("occlusion *= texture(occlusionMap, textureCoord).r;");
    }

    if (normals && ((lightsState.lights.length > 0) || lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0)) {

        //--------------------------------------------------------------------------------
        // SHADING
        //--------------------------------------------------------------------------------

        if (uvs && material._normalMap) {
            if (material._normalMap._state.matrix) {
                src.push("textureCoord = (normalMapMatrix * texturePos).xy;");
            } else {
                src.push("textureCoord = texturePos.xy;");
            }
            src.push("vec3 viewNormal = perturbNormal2Arb( vViewPosition, normalize(vViewNormal), textureCoord );");
        } else {
            src.push("vec3 viewNormal = normalize(vViewNormal);");
        }

        if (uvs && material._specularMap) {
            if (material._specularMap._state.matrix) {
                src.push("textureCoord = (specularMapMatrix * texturePos).xy;");
            } else {
                src.push("textureCoord = texturePos.xy;");
            }
            src.push("specular *= texture(specularMap, textureCoord).rgb;");
        }

        if (uvs && material._glossinessMap) {
            if (material._glossinessMap._state.matrix) {
                src.push("textureCoord = (glossinessMapMatrix * texturePos).xy;");
            } else {
                src.push("textureCoord = texturePos.xy;");
            }
            src.push("glossiness *= texture(glossinessMap, textureCoord).r;");
        }

        if (uvs && material._specularGlossinessMap) {
            if (material._specularGlossinessMap._state.matrix) {
                src.push("textureCoord = (materialSpecularGlossinessMapMatrix * texturePos).xy;");
            } else {
                src.push("textureCoord = texturePos.xy;");
            }
            src.push("vec4 specGlossRGB = texture(materialSpecularGlossinessMap, textureCoord).rgba;"); // TODO: what if only RGB texture?
            src.push("specular *= specGlossRGB.rgb;");
            src.push("glossiness *= specGlossRGB.a;");
        }

        if (uvs && material._metallicMap) {
            if (material._metallicMap._state.matrix) {
                src.push("textureCoord = (metallicMapMatrix * texturePos).xy;");
            } else {
                src.push("textureCoord = texturePos.xy;");
            }
            src.push("metallic *= texture(metallicMap, textureCoord).r;");
        }

        if (uvs && material._roughnessMap) {
            if (material._roughnessMap._state.matrix) {
                src.push("textureCoord = (roughnessMapMatrix * texturePos).xy;");
            } else {
                src.push("textureCoord = texturePos.xy;");
            }
            src.push("roughness *= texture(roughnessMap, textureCoord).r;");
        }

        if (uvs && material._metallicRoughnessMap) {
            if (material._metallicRoughnessMap._state.matrix) {
                src.push("textureCoord = (metallicRoughnessMapMatrix * texturePos).xy;");
            } else {
                src.push("textureCoord = texturePos.xy;");
            }
            src.push("vec3 metalRoughRGB = texture(metallicRoughnessMap, textureCoord).rgb;");
            src.push("metallic *= metalRoughRGB.b;");
            src.push("roughness *= metalRoughRGB.g;");
        }

        src.push("vec3 viewEyeDir = normalize(-vViewPosition);");

        if (material._diffuseFresnel) {
            src.push("float diffuseFresnel = fresnel(viewEyeDir, viewNormal, diffuseFresnelEdgeBias, diffuseFresnelCenterBias, diffuseFresnelPower);");
            src.push("diffuseColor *= mix(diffuseFresnelEdgeColor, diffuseFresnelCenterColor, diffuseFresnel);");
        }
        if (material._specularFresnel) {
            src.push("float specularFresnel = fresnel(viewEyeDir, viewNormal, specularFresnelEdgeBias, specularFresnelCenterBias, specularFresnelPower);");
            src.push("specular *= mix(specularFresnelEdgeColor, specularFresnelCenterColor, specularFresnel);");
        }
        if (material._alphaFresnel) {
            src.push("float alphaFresnel = fresnel(viewEyeDir, viewNormal, alphaFresnelEdgeBias, alphaFresnelCenterBias, alphaFresnelPower);");
            src.push("alpha *= mix(alphaFresnelEdgeColor.r, alphaFresnelCenterColor.r, alphaFresnel);");
        }
        if (material._emissiveFresnel) {
            src.push("float emissiveFresnel = fresnel(viewEyeDir, viewNormal, emissiveFresnelEdgeBias, emissiveFresnelCenterBias, emissiveFresnelPower);");
            src.push("emissiveColor *= mix(emissiveFresnelEdgeColor, emissiveFresnelCenterColor, emissiveFresnel);");
        }

        src.push("if (materialAlphaModeCutoff[1] == 1.0 && alpha < materialAlphaModeCutoff[2]) {"); // ie. (alphaMode == "mask" && alpha < alphaCutoff)
        src.push("   discard;"); // TODO: Discard earlier within this shader?
        src.push("}");

        // PREPARE INPUTS FOR SHADER FUNCTIONS

        src.push("IncidentLight  light;");
        src.push("Material       material;");
        src.push("Geometry       geometry;");
        src.push("ReflectedLight reflectedLight = ReflectedLight(vec3(0.0,0.0,0.0), vec3(0.0,0.0,0.0));");
        src.push("vec3           viewLightDir;");

        if (phongMaterial) {
            src.push("material.diffuseColor      = diffuseColor;");
            src.push("material.specularColor     = specular;");
            src.push("material.shine             = materialShininess;");
        }

        if (specularMaterial) {
            src.push("float oneMinusSpecularStrength = 1.0 - max(max(specular.r, specular.g ),specular.b);"); // Energy conservation
            src.push("material.diffuseColor      = diffuseColor * oneMinusSpecularStrength;");
            src.push("material.specularRoughness = clamp( 1.0 - glossiness, 0.04, 1.0 );");
            src.push("material.specularColor     = specular;");
        }

        if (metallicMaterial) {
            src.push("float dielectricSpecular = 0.16 * specularF0 * specularF0;");
            src.push("material.diffuseColor      = diffuseColor * (1.0 - dielectricSpecular) * (1.0 - metallic);");
            src.push("material.specularRoughness = clamp(roughness, 0.04, 1.0);");
            src.push("material.specularColor     = mix(vec3(dielectricSpecular), diffuseColor, metallic);");
        }

        src.push("geometry.position      = vViewPosition;");
        if (lightsState.lightMaps.length > 0) {
            src.push("geometry.worldNormal   = normalize(vWorldNormal);");
        }
        src.push("geometry.viewNormal    = viewNormal;");
        src.push("geometry.viewEyeDir    = viewEyeDir;");

        // ENVIRONMENT AND REFLECTION MAP SHADING

        if ((phongMaterial) && (lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0)) {
            src.push("computePhongLightMapping(geometry, material, reflectedLight);");
        }

        if ((specularMaterial || metallicMaterial) && (lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0)) {
            src.push("computePBRLightMapping(geometry, material, reflectedLight);");
        }

        // LIGHT SOURCE SHADING

        src.push("float shadow = 1.0;");

        // if (receivesShadow) {
        //
        //     src.push("float lightDepth2 = clamp(length(lightPos)/40.0, 0.0, 1.0);");
        //     src.push("float illuminated = VSM(sLightDepth, lightUV, lightDepth2);");
        //
        src.push("float shadowAcneRemover = 0.007;");
        src.push("vec3 fragmentDepth;");
        src.push("float texelSize = 1.0 / 1024.0;");
        src.push("float amountInLight = 0.0;");
        src.push("vec3 shadowCoord;");
        src.push('vec4 rgbaDepth;');
        src.push("float depth;");
        for (let i = 0, len = lightsState.lights.length; i < len; i++) {

            const light = lightsState.lights[i];

            if (light.type === "ambient") {
                continue;
            }
            if (light.type === "dir" && light.space === "view") {
                src.push("viewLightDir = -normalize(lightDir" + i + ");");
            } else if (light.type === "point" && light.space === "view") {
                src.push("viewLightDir = normalize(lightPos" + i + " - vViewPosition);");
                //src.push("tmpVec3 = lightPos" + i + ".xyz - viewPosition.xyz;");
                //src.push("lightDist = abs(length(tmpVec3));");
            } else {
                src.push("viewLightDir = normalize(vViewLightReverseDirAndDist" + i + ".xyz);"); // If normal mapping, the fragment->light vector will be in tangent space
            }

            if (receivesShadow && light.castsShadow) {

                // if (true) {
                //     src.push('shadowCoord = (vShadowPosFromLight' + i + '.xyz/vShadowPosFromLight' + i + '.w)/2.0 + 0.5;');
                //     src.push("lightDepth2 = clamp(length(vec3[0.0, 20.0, 20.0])/40.0, 0.0, 1.0);");
                //     src.push("castsShadow *= VSM(shadowMap' + i + ', shadowCoord, lightDepth2);");
                // }
                //
                // if (false) {
                //
                // PCF

                src.push("shadow = 0.0;");

                src.push("fragmentDepth = vShadowPosFromLight" + i + ".xyz;");
                src.push("fragmentDepth.z -= shadowAcneRemover;");
                src.push("for (int x = -3; x <= 3; x++) {");
                src.push("  for (int y = -3; y <= 3; y++) {");
                src.push("      float texelDepth = unpackDepth(texture(shadowMap" + i + ", fragmentDepth.xy + vec2(x, y) * texelSize));");
                src.push("      if (fragmentDepth.z < texelDepth) {");
                src.push("          shadow += 1.0;");
                src.push("      }");
                src.push("  }");
                src.push("}");

                src.push("shadow = shadow / 9.0;");

                src.push("light.color =  lightColor" + i + ".rgb * (lightColor" + i + ".a * shadow);"); // a is intensity
                //
                // }
                //
                // if (false){
                //
                //     src.push("shadow = 1.0;");
                //
                //     src.push('shadowCoord = (vShadowPosFromLight' + i + '.xyz/vShadowPosFromLight' + i + '.w)/2.0 + 0.5;');
                //
                //     src.push('shadow -= (shadowCoord.z > unpackDepth(texture(shadowMap' + i + ', shadowCoord.xy + vec2( -0.94201624, -0.39906216 ) / 700.0)) + 0.0015) ? 0.2 : 0.0;');
                //     src.push('shadow -= (shadowCoord.z > unpackDepth(texture(shadowMap' + i + ', shadowCoord.xy + vec2( 0.94558609, -0.76890725 ) / 700.0)) + 0.0015) ? 0.2 : 0.0;');
                //     src.push('shadow -= (shadowCoord.z > unpackDepth(texture(shadowMap' + i + ', shadowCoord.xy + vec2( -0.094184101, -0.92938870 ) / 700.0)) + 0.0015) ? 0.2 : 0.0;');
                //     src.push('shadow -= (shadowCoord.z > unpackDepth(texture(shadowMap' + i + ', shadowCoord.xy + vec2( 0.34495938, 0.29387760 ) / 700.0)) + 0.0015) ? 0.2 : 0.0;');
                //
                //     src.push("light.color =  lightColor" + i + ".rgb * (lightColor" + i + ".a * shadow);");
                // }
            } else {
                src.push("light.color =  lightColor" + i + ".rgb * (lightColor" + i + ".a );"); // a is intensity
            }

            src.push("light.direction = viewLightDir;");

            if (phongMaterial) {
                src.push("computePhongLighting(light, geometry, material, reflectedLight);");
            }

            if (specularMaterial || metallicMaterial) {
                src.push("computePBRLighting(light, geometry, material, reflectedLight);");
            }
        }

        //src.push("reflectedLight.diffuse *= shadow;");

        // COMBINE TERMS

        if (phongMaterial) {
            src.push("vec3 outgoingLight = (lightAmbient.rgb * lightAmbient.a * diffuseColor) + ((occlusion * (( reflectedLight.diffuse + reflectedLight.specular)))) + emissiveColor;");

        } else {
            src.push("vec3 outgoingLight = (occlusion * (reflectedLight.diffuse)) + (occlusion * reflectedLight.specular) + emissiveColor;");
        }

    } else {

        //--------------------------------------------------------------------------------
        // NO SHADING - EMISSIVE and AMBIENT ONLY
        //--------------------------------------------------------------------------------

        src.push("ambientColor *= (lightAmbient.rgb * lightAmbient.a);");

        src.push("vec3 outgoingLight = emissiveColor + ambientColor;");
    }

    src.push("vec4 fragColor = vec4(outgoingLight, alpha) * colorize;");

    if (gammaOutput) {
        src.push("fragColor = linearToGamma(fragColor, gammaFactor);");
    }

    src.push("outColor = fragColor;");

    if (scene.logarithmicDepthBufferEnabled) {
        src.push("gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
    }

    src.push("}");

    return src;
}

/**
 * @desc Represents a vertex or fragment stage within a {@link Program}.
 * @private
 */
class Shader {

    constructor(gl, type, source) {

        this.allocated = false;
        this.compiled = false;
        this.handle = gl.createShader(type);

        if (!this.handle) {
            this.errors = [
                "Failed to allocate"
            ];
            return;
        }

        this.allocated = true;

        gl.shaderSource(this.handle, source);
        gl.compileShader(this.handle);

        this.compiled = gl.getShaderParameter(this.handle, gl.COMPILE_STATUS);

        if (!this.compiled) {

            if (!gl.isContextLost()) { // Handled explicitly elsewhere, so won't re-handle here

                const lines = source.split("\n");
                const numberedLines = [];
                for (let i = 0; i < lines.length; i++) {
                    numberedLines.push((i + 1) + ": " + lines[i] + "\n");
                }
                this.errors = [];
                this.errors.push("");
                this.errors.push(gl.getShaderInfoLog(this.handle));
                this.errors = this.errors.concat(numberedLines.join(""));
            }
        }
    }

    destroy() {

    }
}

/**
 * @desc A low-level component that represents a WebGL Sampler.
 * @private
 */
class Sampler {

    constructor(gl, location) {
        this.bindTexture = function (texture, unit) {
            if (texture.bind(unit)) {
                gl.uniform1i(location, unit);
                return true;
            }
            return false;
        };
    }
}

/**
 * @desc Represents a WebGL vertex attribute buffer (VBO).
 * @private
 * @param gl {WebGLRenderingContext} The WebGL rendering context.
 */
class Attribute {

    constructor(gl, location) {
        this._gl = gl;
        this.location = location;
    }

    bindArrayBuffer(arrayBuf) {
        if (!arrayBuf) {
            return;
        }
        arrayBuf.bind();
        this._gl.enableVertexAttribArray(this.location);
        this._gl.vertexAttribPointer(this.location, arrayBuf.itemSize, arrayBuf.itemType, arrayBuf.normalized, arrayBuf.stride, arrayBuf.offset);
    }
}

const ids$3 = new Map$1$1({});

function joinSansComments(srcLines) {
    const src = [];
    let line;
    let n;
    for (let i = 0, len = srcLines.length; i < len; i++) {
        line = srcLines[i];
        n = line.indexOf("/");
        if (n > 0) {
            if (line.charAt(n + 1) === "/") {
                line = line.substring(0, n);
            }
        }
        src.push(line);
    }
    return src.join("\n");
}

function logErrors(errors) {
    console.error(errors.join("\n"));
}

/**
 * @desc Represents a WebGL program.
 * @private
 */
class Program {

    constructor(gl, shaderSource) {
        this.id = ids$3.addItem({});
        this.source = shaderSource;
        this.init(gl);
    }

    init(gl) {
        this.gl = gl;
        this.allocated = false;
        this.compiled = false;
        this.linked = false;
        this.validated = false;
        this.errors = null;
        this.uniforms = {};
        this.samplers = {};
        this.attributes = {};
        this._vertexShader = new Shader(gl, gl.VERTEX_SHADER, joinSansComments(this.source.vertex));
        this._fragmentShader = new Shader(gl, gl.FRAGMENT_SHADER, joinSansComments(this.source.fragment));
        if (!this._vertexShader.allocated) {
            this.errors = ["Vertex shader failed to allocate"].concat(this._vertexShader.errors);
            logErrors(this.errors);
            return;
        }
        if (!this._fragmentShader.allocated) {
            this.errors = ["Fragment shader failed to allocate"].concat(this._fragmentShader.errors);
            logErrors(this.errors);
            return;
        }
        this.allocated = true;
        if (!this._vertexShader.compiled) {
            this.errors = ["Vertex shader failed to compile"].concat(this._vertexShader.errors);
            logErrors(this.errors);
            return;
        }
        if (!this._fragmentShader.compiled) {
            this.errors = ["Fragment shader failed to compile"].concat(this._fragmentShader.errors);
            logErrors(this.errors);
            return;
        }
        this.compiled = true;
        let a;
        let i;
        let u;
        let uName;
        let location;
        this.handle = gl.createProgram();
        if (!this.handle) {
            this.errors = ["Failed to allocate program"];
            return;
        }
        gl.attachShader(this.handle, this._vertexShader.handle);
        gl.attachShader(this.handle, this._fragmentShader.handle);
        gl.linkProgram(this.handle);
        this.linked = gl.getProgramParameter(this.handle, gl.LINK_STATUS);
        // HACK: Disable validation temporarily
        // Perhaps we should defer validation until render-time, when the program has values set for all inputs?
        this.validated = true;
        if (!this.linked || !this.validated) {
            this.errors = [];
            this.errors.push("");
            this.errors.push(gl.getProgramInfoLog(this.handle));
            this.errors.push("\nVertex shader:\n");
            this.errors = this.errors.concat(this.source.vertex);
            this.errors.push("\nFragment shader:\n");
            this.errors = this.errors.concat(this.source.fragment);
            logErrors(this.errors);
            return;
        }
        const numUniforms = gl.getProgramParameter(this.handle, gl.ACTIVE_UNIFORMS);
        for (i = 0; i < numUniforms; ++i) {
            u = gl.getActiveUniform(this.handle, i);
            if (u) {
                uName = u.name;
                if (uName[uName.length - 1] === "\u0000") {
                    uName = uName.substr(0, uName.length - 1);
                }
                location = gl.getUniformLocation(this.handle, uName);
                if ((u.type === gl.SAMPLER_2D) || (u.type === gl.SAMPLER_CUBE) || (u.type === 35682)) {
                    this.samplers[uName] = new Sampler(gl, location);
                } else if (gl instanceof WebGL2RenderingContext && (u.type === gl.UNSIGNED_INT_SAMPLER_2D || u.type === gl.INT_SAMPLER_2D)) {
                    this.samplers[uName] = new Sampler(gl, location);
                } else {
                    this.uniforms[uName] = location;
                }
            }
        }
        const numAttribs = gl.getProgramParameter(this.handle, gl.ACTIVE_ATTRIBUTES);
        for (i = 0; i < numAttribs; i++) {
            a = gl.getActiveAttrib(this.handle, i);
            if (a) {
                location = gl.getAttribLocation(this.handle, a.name);
                this.attributes[a.name] = new Attribute(gl, location);
            }
        }
        this.allocated = true;
    }

    bind() {
        if (!this.allocated) {
            return;
        }
        this.gl.useProgram(this.handle);
    }

    getLocation(name) {
        if (!this.allocated) {
            return;
        }
        return this.uniforms[name];
    }

    getAttribute(name) {
        if (!this.allocated) {
            return;
        }
        return this.attributes[name];
    }

    bindTexture(name, texture, unit) {
        if (!this.allocated) {
            return false;
        }
        const sampler = this.samplers[name];
        if (sampler) {
            return sampler.bindTexture(texture, unit);
        } else {
            return false;
        }
    }

    destroy() {
        if (!this.allocated) {
            return;
        }
        ids$3.removeItem(this.id);
        this.gl.deleteProgram(this.handle);
        this.gl.deleteShader(this._vertexShader.handle);
        this.gl.deleteShader(this._fragmentShader.handle);
        this.handle = null;
        this.attributes = null;
        this.uniforms = null;
        this.samplers = null;
        this.allocated = false;
    }
}

/**
 * @private
 * @type {{WEBGL: boolean, SUPPORTED_EXTENSIONS: {}}}
 */
const WEBGL_INFO = {
    WEBGL: false,
    SUPPORTED_EXTENSIONS: {}
};

const canvas = document.createElement("canvas");

if (canvas) {

    const gl = canvas.getContext("webgl", {antialias: true}) || canvas.getContext("experimental-webgl", {antialias: true});

    WEBGL_INFO.WEBGL = !!gl;

    if (WEBGL_INFO.WEBGL) {
        WEBGL_INFO.ANTIALIAS = gl.getContextAttributes().antialias;
        if (gl.getShaderPrecisionFormat) {
            if (gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT).precision > 0) {
                WEBGL_INFO.FS_MAX_FLOAT_PRECISION = "highp";
            } else if (gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT).precision > 0) {
                WEBGL_INFO.FS_MAX_FLOAT_PRECISION = "mediump";
            } else {
                WEBGL_INFO.FS_MAX_FLOAT_PRECISION = "lowp";
            }
        } else {
            WEBGL_INFO.FS_MAX_FLOAT_PRECISION = "mediump";
        }
        WEBGL_INFO.DEPTH_BUFFER_BITS = gl.getParameter(gl.DEPTH_BITS);
        WEBGL_INFO.MAX_TEXTURE_SIZE = gl.getParameter(gl.MAX_TEXTURE_SIZE);
        WEBGL_INFO.MAX_CUBE_MAP_SIZE = gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE);
        WEBGL_INFO.MAX_RENDERBUFFER_SIZE = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE);
        WEBGL_INFO.MAX_TEXTURE_UNITS = gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS);
        WEBGL_INFO.MAX_TEXTURE_IMAGE_UNITS = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
        WEBGL_INFO.MAX_VERTEX_ATTRIBS = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
        WEBGL_INFO.MAX_VERTEX_UNIFORM_VECTORS = gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS);
        WEBGL_INFO.MAX_FRAGMENT_UNIFORM_VECTORS = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS);
        WEBGL_INFO.MAX_VARYING_VECTORS = gl.getParameter(gl.MAX_VARYING_VECTORS);
        gl.getSupportedExtensions().forEach(function (ext) {
            WEBGL_INFO.SUPPORTED_EXTENSIONS[ext] = true;
        });
    }
}

/**
 * @author xeolabs / https://github.com/xeolabs
 */

const tempVec3a$L = math.vec3();

const ids$2 = new Map$1$1({});

/**
 * @private
 */
const DrawRenderer = function (hash, mesh) {
    this.id = ids$2.addItem({});
    this._hash = hash;
    this._scene = mesh.scene;
    this._useCount = 0;
    this._shaderSource = new DrawShaderSource(mesh);
    this._allocate(mesh);
};

const drawRenderers = {};

DrawRenderer.get = function (mesh) {
    const scene = mesh.scene;
    const hash = [
        scene.canvas.canvas.id,
        (scene.gammaInput ? "gi;" : ";") + (scene.gammaOutput ? "go" : ""),
        scene._lightsState.getHash(),
        scene._sectionPlanesState.getHash(),
        mesh._geometry._state.hash,
        mesh._material._state.hash,
        mesh._state.drawHash
    ].join(";");
    let renderer = drawRenderers[hash];
    if (!renderer) {
        renderer = new DrawRenderer(hash, mesh);
        if (renderer.errors) {
            console.log(renderer.errors.join("\n"));
            return null;
        }
        drawRenderers[hash] = renderer;
        stats.memory.programs++;
    }
    renderer._useCount++;
    return renderer;
};

DrawRenderer.prototype.put = function () {
    if (--this._useCount === 0) {
        ids$2.removeItem(this.id);
        if (this._program) {
            this._program.destroy();
        }
        delete drawRenderers[this._hash];
        stats.memory.programs--;
    }
};

DrawRenderer.prototype.webglContextRestored = function () {
    this._program = null;
};

DrawRenderer.prototype.drawMesh = function (frameCtx, mesh) {

    if (!this._program) {
        this._allocate(mesh);
    }

    const maxTextureUnits = WEBGL_INFO.MAX_TEXTURE_UNITS;
    const scene = mesh.scene;
    const material = mesh._material;
    const gl = scene.canvas.gl;
    const program = this._program;
    const meshState = mesh._state;
    const materialState = mesh._material._state;
    const geometryState = mesh._geometry._state;
    const camera = scene.camera;
    const origin = mesh.origin;
    const background = meshState.background;

    if (frameCtx.lastProgramId !== this._program.id) {
        frameCtx.lastProgramId = this._program.id;
        if (background) {
            gl.depthFunc(gl.LEQUAL);
        }
        this._bindProgram(frameCtx);
    }

    gl.uniformMatrix4fv(this._uViewMatrix, false, origin ? frameCtx.getRTCViewMatrix(meshState.originHash, origin) : camera.viewMatrix);
    gl.uniformMatrix4fv(this._uViewNormalMatrix, false, camera.viewNormalMatrix);

    if (meshState.clippable) {
        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const renderFlags = mesh.renderFlags;
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$L);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }
    }

    if (materialState.id !== this._lastMaterialId) {

        frameCtx.textureUnit = this._baseTextureUnit;

        const backfaces = materialState.backfaces;
        if (frameCtx.backfaces !== backfaces) {
            if (backfaces) {
                gl.disable(gl.CULL_FACE);
            } else {
                gl.enable(gl.CULL_FACE);
            }
            frameCtx.backfaces = backfaces;
        }

        const frontface = materialState.frontface;
        if (frameCtx.frontface !== frontface) {
            if (frontface) {
                gl.frontFace(gl.CCW);
            } else {
                gl.frontFace(gl.CW);
            }
            frameCtx.frontface = frontface;
        }

        if (frameCtx.lineWidth !== materialState.lineWidth) {
            gl.lineWidth(materialState.lineWidth);
            frameCtx.lineWidth = materialState.lineWidth;
        }

        if (this._uPointSize) {
            gl.uniform1f(this._uPointSize, materialState.pointSize);
        }

        switch (materialState.type) {
            case "LambertMaterial":
                if (this._uMaterialAmbient) {
                    gl.uniform3fv(this._uMaterialAmbient, materialState.ambient);
                }
                if (this._uMaterialColor) {
                    gl.uniform4f(this._uMaterialColor, materialState.color[0], materialState.color[1], materialState.color[2], materialState.alpha);
                }
                if (this._uMaterialEmissive) {
                    gl.uniform3fv(this._uMaterialEmissive, materialState.emissive);
                }
                break;

            case "PhongMaterial":
                if (this._uMaterialShininess) {
                    gl.uniform1f(this._uMaterialShininess, materialState.shininess);
                }
                if (this._uMaterialAmbient) {
                    gl.uniform3fv(this._uMaterialAmbient, materialState.ambient);
                }
                if (this._uMaterialDiffuse) {
                    gl.uniform3fv(this._uMaterialDiffuse, materialState.diffuse);
                }
                if (this._uMaterialSpecular) {
                    gl.uniform3fv(this._uMaterialSpecular, materialState.specular);
                }
                if (this._uMaterialEmissive) {
                    gl.uniform3fv(this._uMaterialEmissive, materialState.emissive);
                }
                if (this._uAlphaModeCutoff) {
                    gl.uniform4f(
                        this._uAlphaModeCutoff,
                        1.0 * materialState.alpha,
                        materialState.alphaMode === 1 ? 1.0 : 0.0,
                        materialState.alphaCutoff,
                        0);
                }
                if (material._ambientMap && material._ambientMap._state.texture && this._uMaterialAmbientMap) {
                    program.bindTexture(this._uMaterialAmbientMap, material._ambientMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uMaterialAmbientMapMatrix) {
                        gl.uniformMatrix4fv(this._uMaterialAmbientMapMatrix, false, material._ambientMap._state.matrix);
                    }
                }
                if (material._diffuseMap && material._diffuseMap._state.texture && this._uDiffuseMap) {
                    program.bindTexture(this._uDiffuseMap, material._diffuseMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uDiffuseMapMatrix) {
                        gl.uniformMatrix4fv(this._uDiffuseMapMatrix, false, material._diffuseMap._state.matrix);
                    }
                }
                if (material._specularMap && material._specularMap._state.texture && this._uSpecularMap) {
                    program.bindTexture(this._uSpecularMap, material._specularMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uSpecularMapMatrix) {
                        gl.uniformMatrix4fv(this._uSpecularMapMatrix, false, material._specularMap._state.matrix);
                    }
                }
                if (material._emissiveMap && material._emissiveMap._state.texture && this._uEmissiveMap) {
                    program.bindTexture(this._uEmissiveMap, material._emissiveMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uEmissiveMapMatrix) {
                        gl.uniformMatrix4fv(this._uEmissiveMapMatrix, false, material._emissiveMap._state.matrix);
                    }
                }
                if (material._alphaMap && material._alphaMap._state.texture && this._uAlphaMap) {
                    program.bindTexture(this._uAlphaMap, material._alphaMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uAlphaMapMatrix) {
                        gl.uniformMatrix4fv(this._uAlphaMapMatrix, false, material._alphaMap._state.matrix);
                    }
                }
                if (material._reflectivityMap && material._reflectivityMap._state.texture && this._uReflectivityMap) {
                    program.bindTexture(this._uReflectivityMap, material._reflectivityMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    if (this._uReflectivityMapMatrix) {
                        gl.uniformMatrix4fv(this._uReflectivityMapMatrix, false, material._reflectivityMap._state.matrix);
                    }
                }
                if (material._normalMap && material._normalMap._state.texture && this._uNormalMap) {
                    program.bindTexture(this._uNormalMap, material._normalMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uNormalMapMatrix) {
                        gl.uniformMatrix4fv(this._uNormalMapMatrix, false, material._normalMap._state.matrix);
                    }
                }
                if (material._occlusionMap && material._occlusionMap._state.texture && this._uOcclusionMap) {
                    program.bindTexture(this._uOcclusionMap, material._occlusionMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uOcclusionMapMatrix) {
                        gl.uniformMatrix4fv(this._uOcclusionMapMatrix, false, material._occlusionMap._state.matrix);
                    }
                }
                if (material._diffuseFresnel) {
                    if (this._uDiffuseFresnelEdgeBias) {
                        gl.uniform1f(this._uDiffuseFresnelEdgeBias, material._diffuseFresnel.edgeBias);
                    }
                    if (this._uDiffuseFresnelCenterBias) {
                        gl.uniform1f(this._uDiffuseFresnelCenterBias, material._diffuseFresnel.centerBias);
                    }
                    if (this._uDiffuseFresnelEdgeColor) {
                        gl.uniform3fv(this._uDiffuseFresnelEdgeColor, material._diffuseFresnel.edgeColor);
                    }
                    if (this._uDiffuseFresnelCenterColor) {
                        gl.uniform3fv(this._uDiffuseFresnelCenterColor, material._diffuseFresnel.centerColor);
                    }
                    if (this._uDiffuseFresnelPower) {
                        gl.uniform1f(this._uDiffuseFresnelPower, material._diffuseFresnel.power);
                    }
                }
                if (material._specularFresnel) {
                    if (this._uSpecularFresnelEdgeBias) {
                        gl.uniform1f(this._uSpecularFresnelEdgeBias, material._specularFresnel.edgeBias);
                    }
                    if (this._uSpecularFresnelCenterBias) {
                        gl.uniform1f(this._uSpecularFresnelCenterBias, material._specularFresnel.centerBias);
                    }
                    if (this._uSpecularFresnelEdgeColor) {
                        gl.uniform3fv(this._uSpecularFresnelEdgeColor, material._specularFresnel.edgeColor);
                    }
                    if (this._uSpecularFresnelCenterColor) {
                        gl.uniform3fv(this._uSpecularFresnelCenterColor, material._specularFresnel.centerColor);
                    }
                    if (this._uSpecularFresnelPower) {
                        gl.uniform1f(this._uSpecularFresnelPower, material._specularFresnel.power);
                    }
                }
                if (material._alphaFresnel) {
                    if (this._uAlphaFresnelEdgeBias) {
                        gl.uniform1f(this._uAlphaFresnelEdgeBias, material._alphaFresnel.edgeBias);
                    }
                    if (this._uAlphaFresnelCenterBias) {
                        gl.uniform1f(this._uAlphaFresnelCenterBias, material._alphaFresnel.centerBias);
                    }
                    if (this._uAlphaFresnelEdgeColor) {
                        gl.uniform3fv(this._uAlphaFresnelEdgeColor, material._alphaFresnel.edgeColor);
                    }
                    if (this._uAlphaFresnelCenterColor) {
                        gl.uniform3fv(this._uAlphaFresnelCenterColor, material._alphaFresnel.centerColor);
                    }
                    if (this._uAlphaFresnelPower) {
                        gl.uniform1f(this._uAlphaFresnelPower, material._alphaFresnel.power);
                    }
                }
                if (material._reflectivityFresnel) {
                    if (this._uReflectivityFresnelEdgeBias) {
                        gl.uniform1f(this._uReflectivityFresnelEdgeBias, material._reflectivityFresnel.edgeBias);
                    }
                    if (this._uReflectivityFresnelCenterBias) {
                        gl.uniform1f(this._uReflectivityFresnelCenterBias, material._reflectivityFresnel.centerBias);
                    }
                    if (this._uReflectivityFresnelEdgeColor) {
                        gl.uniform3fv(this._uReflectivityFresnelEdgeColor, material._reflectivityFresnel.edgeColor);
                    }
                    if (this._uReflectivityFresnelCenterColor) {
                        gl.uniform3fv(this._uReflectivityFresnelCenterColor, material._reflectivityFresnel.centerColor);
                    }
                    if (this._uReflectivityFresnelPower) {
                        gl.uniform1f(this._uReflectivityFresnelPower, material._reflectivityFresnel.power);
                    }
                }
                if (material._emissiveFresnel) {
                    if (this._uEmissiveFresnelEdgeBias) {
                        gl.uniform1f(this._uEmissiveFresnelEdgeBias, material._emissiveFresnel.edgeBias);
                    }
                    if (this._uEmissiveFresnelCenterBias) {
                        gl.uniform1f(this._uEmissiveFresnelCenterBias, material._emissiveFresnel.centerBias);
                    }
                    if (this._uEmissiveFresnelEdgeColor) {
                        gl.uniform3fv(this._uEmissiveFresnelEdgeColor, material._emissiveFresnel.edgeColor);
                    }
                    if (this._uEmissiveFresnelCenterColor) {
                        gl.uniform3fv(this._uEmissiveFresnelCenterColor, material._emissiveFresnel.centerColor);
                    }
                    if (this._uEmissiveFresnelPower) {
                        gl.uniform1f(this._uEmissiveFresnelPower, material._emissiveFresnel.power);
                    }
                }
                break;

            case "MetallicMaterial":
                if (this._uBaseColor) {
                    gl.uniform3fv(this._uBaseColor, materialState.baseColor);
                }
                if (this._uMaterialMetallic) {
                    gl.uniform1f(this._uMaterialMetallic, materialState.metallic);
                }
                if (this._uMaterialRoughness) {
                    gl.uniform1f(this._uMaterialRoughness, materialState.roughness);
                }
                if (this._uMaterialSpecularF0) {
                    gl.uniform1f(this._uMaterialSpecularF0, materialState.specularF0);
                }
                if (this._uMaterialEmissive) {
                    gl.uniform3fv(this._uMaterialEmissive, materialState.emissive);
                }
                if (this._uAlphaModeCutoff) {
                    gl.uniform4f(
                        this._uAlphaModeCutoff,
                        1.0 * materialState.alpha,
                        materialState.alphaMode === 1 ? 1.0 : 0.0,
                        materialState.alphaCutoff,
                        0.0);
                }
                const baseColorMap = material._baseColorMap;
                if (baseColorMap && baseColorMap._state.texture && this._uBaseColorMap) {
                    program.bindTexture(this._uBaseColorMap, baseColorMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uBaseColorMapMatrix) {
                        gl.uniformMatrix4fv(this._uBaseColorMapMatrix, false, baseColorMap._state.matrix);
                    }
                }
                const metallicMap = material._metallicMap;
                if (metallicMap && metallicMap._state.texture && this._uMetallicMap) {
                    program.bindTexture(this._uMetallicMap, metallicMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uMetallicMapMatrix) {
                        gl.uniformMatrix4fv(this._uMetallicMapMatrix, false, metallicMap._state.matrix);
                    }
                }
                const roughnessMap = material._roughnessMap;
                if (roughnessMap && roughnessMap._state.texture && this._uRoughnessMap) {
                    program.bindTexture(this._uRoughnessMap, roughnessMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uRoughnessMapMatrix) {
                        gl.uniformMatrix4fv(this._uRoughnessMapMatrix, false, roughnessMap._state.matrix);
                    }
                }
                const metallicRoughnessMap = material._metallicRoughnessMap;
                if (metallicRoughnessMap && metallicRoughnessMap._state.texture && this._uMetallicRoughnessMap) {
                    program.bindTexture(this._uMetallicRoughnessMap, metallicRoughnessMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uMetallicRoughnessMapMatrix) {
                        gl.uniformMatrix4fv(this._uMetallicRoughnessMapMatrix, false, metallicRoughnessMap._state.matrix);
                    }
                }
                var emissiveMap = material._emissiveMap;
                if (emissiveMap && emissiveMap._state.texture && this._uEmissiveMap) {
                    program.bindTexture(this._uEmissiveMap, emissiveMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uEmissiveMapMatrix) {
                        gl.uniformMatrix4fv(this._uEmissiveMapMatrix, false, emissiveMap._state.matrix);
                    }
                }
                var occlusionMap = material._occlusionMap;
                if (occlusionMap && material._occlusionMap._state.texture && this._uOcclusionMap) {
                    program.bindTexture(this._uOcclusionMap, occlusionMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uOcclusionMapMatrix) {
                        gl.uniformMatrix4fv(this._uOcclusionMapMatrix, false, occlusionMap._state.matrix);
                    }
                }
                var alphaMap = material._alphaMap;
                if (alphaMap && alphaMap._state.texture && this._uAlphaMap) {
                    program.bindTexture(this._uAlphaMap, alphaMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uAlphaMapMatrix) {
                        gl.uniformMatrix4fv(this._uAlphaMapMatrix, false, alphaMap._state.matrix);
                    }
                }
                var normalMap = material._normalMap;
                if (normalMap && normalMap._state.texture && this._uNormalMap) {
                    program.bindTexture(this._uNormalMap, normalMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uNormalMapMatrix) {
                        gl.uniformMatrix4fv(this._uNormalMapMatrix, false, normalMap._state.matrix);
                    }
                }
                break;

            case "SpecularMaterial":
                if (this._uMaterialDiffuse) {
                    gl.uniform3fv(this._uMaterialDiffuse, materialState.diffuse);
                }
                if (this._uMaterialSpecular) {
                    gl.uniform3fv(this._uMaterialSpecular, materialState.specular);
                }
                if (this._uMaterialGlossiness) {
                    gl.uniform1f(this._uMaterialGlossiness, materialState.glossiness);
                }
                if (this._uMaterialReflectivity) {
                    gl.uniform1f(this._uMaterialReflectivity, materialState.reflectivity);
                }
                if (this._uMaterialEmissive) {
                    gl.uniform3fv(this._uMaterialEmissive, materialState.emissive);
                }
                if (this._uAlphaModeCutoff) {
                    gl.uniform4f(
                        this._uAlphaModeCutoff,
                        1.0 * materialState.alpha,
                        materialState.alphaMode === 1 ? 1.0 : 0.0,
                        materialState.alphaCutoff,
                        0.0);
                }
                const diffuseMap = material._diffuseMap;
                if (diffuseMap && diffuseMap._state.texture && this._uDiffuseMap) {
                    program.bindTexture(this._uDiffuseMap, diffuseMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uDiffuseMapMatrix) {
                        gl.uniformMatrix4fv(this._uDiffuseMapMatrix, false, diffuseMap._state.matrix);
                    }
                }
                const specularMap = material._specularMap;
                if (specularMap && specularMap._state.texture && this._uSpecularMap) {
                    program.bindTexture(this._uSpecularMap, specularMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uSpecularMapMatrix) {
                        gl.uniformMatrix4fv(this._uSpecularMapMatrix, false, specularMap._state.matrix);
                    }
                }
                const glossinessMap = material._glossinessMap;
                if (glossinessMap && glossinessMap._state.texture && this._uGlossinessMap) {
                    program.bindTexture(this._uGlossinessMap, glossinessMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uGlossinessMapMatrix) {
                        gl.uniformMatrix4fv(this._uGlossinessMapMatrix, false, glossinessMap._state.matrix);
                    }
                }
                const specularGlossinessMap = material._specularGlossinessMap;
                if (specularGlossinessMap && specularGlossinessMap._state.texture && this._uSpecularGlossinessMap) {
                    program.bindTexture(this._uSpecularGlossinessMap, specularGlossinessMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uSpecularGlossinessMapMatrix) {
                        gl.uniformMatrix4fv(this._uSpecularGlossinessMapMatrix, false, specularGlossinessMap._state.matrix);
                    }
                }
                var emissiveMap = material._emissiveMap;
                if (emissiveMap && emissiveMap._state.texture && this._uEmissiveMap) {
                    program.bindTexture(this._uEmissiveMap, emissiveMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uEmissiveMapMatrix) {
                        gl.uniformMatrix4fv(this._uEmissiveMapMatrix, false, emissiveMap._state.matrix);
                    }
                }
                var occlusionMap = material._occlusionMap;
                if (occlusionMap && occlusionMap._state.texture && this._uOcclusionMap) {
                    program.bindTexture(this._uOcclusionMap, occlusionMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uOcclusionMapMatrix) {
                        gl.uniformMatrix4fv(this._uOcclusionMapMatrix, false, occlusionMap._state.matrix);
                    }
                }
                var alphaMap = material._alphaMap;
                if (alphaMap && alphaMap._state.texture && this._uAlphaMap) {
                    program.bindTexture(this._uAlphaMap, alphaMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uAlphaMapMatrix) {
                        gl.uniformMatrix4fv(this._uAlphaMapMatrix, false, alphaMap._state.matrix);
                    }
                }
                var normalMap = material._normalMap;
                if (normalMap && normalMap._state.texture && this._uNormalMap) {
                    program.bindTexture(this._uNormalMap, normalMap._state.texture, frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                    if (this._uNormalMapMatrix) {
                        gl.uniformMatrix4fv(this._uNormalMapMatrix, false, normalMap._state.matrix);
                    }
                }
                break;
        }
        this._lastMaterialId = materialState.id;
    }

    gl.uniformMatrix4fv(this._uModelMatrix, gl.FALSE, mesh.worldMatrix);
    if (this._uModelNormalMatrix) {
        gl.uniformMatrix4fv(this._uModelNormalMatrix, gl.FALSE, mesh.worldNormalMatrix);
    }

    if (this._uClippable) {
        gl.uniform1i(this._uClippable, meshState.clippable);
    }

    if (this._uColorize) {
        const colorize = meshState.colorize;
        const lastColorize = this._lastColorize;
        if (lastColorize[0] !== colorize[0] ||
            lastColorize[1] !== colorize[1] ||
            lastColorize[2] !== colorize[2] ||
            lastColorize[3] !== colorize[3]) {
            gl.uniform4fv(this._uColorize, colorize);
            lastColorize[0] = colorize[0];
            lastColorize[1] = colorize[1];
            lastColorize[2] = colorize[2];
            lastColorize[3] = colorize[3];
        }
    }

    gl.uniform3fv(this._uOffset, meshState.offset);

    // Bind VBOs

    if (geometryState.id !== this._lastGeometryId) {
        if (this._uPositionsDecodeMatrix) {
            gl.uniformMatrix4fv(this._uPositionsDecodeMatrix, false, geometryState.positionsDecodeMatrix);
        }
        if (this._uUVDecodeMatrix) {
            gl.uniformMatrix3fv(this._uUVDecodeMatrix, false, geometryState.uvDecodeMatrix);
        }
        if (this._aPosition) {
            this._aPosition.bindArrayBuffer(geometryState.positionsBuf);
            frameCtx.bindArray++;
        }
        if (this._aNormal) {
            this._aNormal.bindArrayBuffer(geometryState.normalsBuf);
            frameCtx.bindArray++;
        }
        if (this._aUV) {
            this._aUV.bindArrayBuffer(geometryState.uvBuf);
            frameCtx.bindArray++;
        }
        if (this._aColor) {
            this._aColor.bindArrayBuffer(geometryState.colorsBuf);
            frameCtx.bindArray++;
        }
        if (this._aFlags) {
            this._aFlags.bindArrayBuffer(geometryState.flagsBuf);
            frameCtx.bindArray++;
        }
        if (geometryState.indicesBuf) {
            geometryState.indicesBuf.bind();
            frameCtx.bindArray++;
        }
        this._lastGeometryId = geometryState.id;
    }

    // Draw (indices bound in prev step)

    if (geometryState.indicesBuf) {
        gl.drawElements(geometryState.primitive, geometryState.indicesBuf.numItems, geometryState.indicesBuf.itemType, 0);
        frameCtx.drawElements++;
    } else if (geometryState.positions) {
        gl.drawArrays(gl.TRIANGLES, 0, geometryState.positions.numItems);
        frameCtx.drawArrays++;
    }

    if (background) {
        gl.depthFunc(gl.LESS);
    }
};

DrawRenderer.prototype._allocate = function (mesh) {
    const scene = mesh.scene;
    const gl = scene.canvas.gl;
    const material = mesh._material;
    const lightsState = scene._lightsState;
    const sectionPlanesState = scene._sectionPlanesState;
    const materialState = mesh._material._state;

    this._program = new Program(gl, this._shaderSource);
    if (this._program.errors) {
        this.errors = this._program.errors;
        return;
    }
    const program = this._program;
    this._uPositionsDecodeMatrix = program.getLocation("positionsDecodeMatrix");
    this._uUVDecodeMatrix = program.getLocation("uvDecodeMatrix");
    this._uModelMatrix = program.getLocation("modelMatrix");
    this._uModelNormalMatrix = program.getLocation("modelNormalMatrix");
    this._uViewMatrix = program.getLocation("viewMatrix");
    this._uViewNormalMatrix = program.getLocation("viewNormalMatrix");
    this._uProjMatrix = program.getLocation("projMatrix");
    this._uGammaFactor = program.getLocation("gammaFactor");
    this._uLightAmbient = [];
    this._uLightColor = [];
    this._uLightDir = [];
    this._uLightPos = [];
    this._uLightAttenuation = [];
    this._uShadowViewMatrix = [];
    this._uShadowProjMatrix = [];

    if (scene.logarithmicDepthBufferEnabled) {
        this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
    }

    const lights = lightsState.lights;
    let light;

    for (var i = 0, len = lights.length; i < len; i++) {
        light = lights[i];
        switch (light.type) {

            case "ambient":
                this._uLightAmbient[i] = program.getLocation("lightAmbient");
                break;

            case "dir":
                this._uLightColor[i] = program.getLocation("lightColor" + i);
                this._uLightPos[i] = null;
                this._uLightDir[i] = program.getLocation("lightDir" + i);
                break;

            case "point":
                this._uLightColor[i] = program.getLocation("lightColor" + i);
                this._uLightPos[i] = program.getLocation("lightPos" + i);
                this._uLightDir[i] = null;
                this._uLightAttenuation[i] = program.getLocation("lightAttenuation" + i);
                break;

            case "spot":
                this._uLightColor[i] = program.getLocation("lightColor" + i);
                this._uLightPos[i] = program.getLocation("lightPos" + i);
                this._uLightDir[i] = program.getLocation("lightDir" + i);
                this._uLightAttenuation[i] = program.getLocation("lightAttenuation" + i);
                break;
        }

        if (light.castsShadow) {
            this._uShadowViewMatrix[i] = program.getLocation("shadowViewMatrix" + i);
            this._uShadowProjMatrix[i] = program.getLocation("shadowProjMatrix" + i);
        }
    }

    if (lightsState.lightMaps.length > 0) {
        this._uLightMap = "lightMap";
    }

    if (lightsState.reflectionMaps.length > 0) {
        this._uReflectionMap = "reflectionMap";
    }

    this._uSectionPlanes = [];
    const sectionPlanes = sectionPlanesState.sectionPlanes;
    for (var i = 0, len = sectionPlanes.length; i < len; i++) {
        this._uSectionPlanes.push({
            active: program.getLocation("sectionPlaneActive" + i),
            pos: program.getLocation("sectionPlanePos" + i),
            dir: program.getLocation("sectionPlaneDir" + i)
        });
    }

    this._uPointSize = program.getLocation("pointSize");

    switch (materialState.type) {
        case "LambertMaterial":
            this._uMaterialColor = program.getLocation("materialColor");
            this._uMaterialEmissive = program.getLocation("materialEmissive");
            this._uAlphaModeCutoff = program.getLocation("materialAlphaModeCutoff");
            break;

        case "PhongMaterial":
            this._uMaterialAmbient = program.getLocation("materialAmbient");
            this._uMaterialDiffuse = program.getLocation("materialDiffuse");
            this._uMaterialSpecular = program.getLocation("materialSpecular");
            this._uMaterialEmissive = program.getLocation("materialEmissive");
            this._uAlphaModeCutoff = program.getLocation("materialAlphaModeCutoff");
            this._uMaterialShininess = program.getLocation("materialShininess");
            if (material._ambientMap) {
                this._uMaterialAmbientMap = "ambientMap";
                this._uMaterialAmbientMapMatrix = program.getLocation("ambientMapMatrix");
            }
            if (material._diffuseMap) {
                this._uDiffuseMap = "diffuseMap";
                this._uDiffuseMapMatrix = program.getLocation("diffuseMapMatrix");
            }
            if (material._specularMap) {
                this._uSpecularMap = "specularMap";
                this._uSpecularMapMatrix = program.getLocation("specularMapMatrix");
            }
            if (material._emissiveMap) {
                this._uEmissiveMap = "emissiveMap";
                this._uEmissiveMapMatrix = program.getLocation("emissiveMapMatrix");
            }
            if (material._alphaMap) {
                this._uAlphaMap = "alphaMap";
                this._uAlphaMapMatrix = program.getLocation("alphaMapMatrix");
            }
            if (material._reflectivityMap) {
                this._uReflectivityMap = "reflectivityMap";
                this._uReflectivityMapMatrix = program.getLocation("reflectivityMapMatrix");
            }
            if (material._normalMap) {
                this._uNormalMap = "normalMap";
                this._uNormalMapMatrix = program.getLocation("normalMapMatrix");
            }
            if (material._occlusionMap) {
                this._uOcclusionMap = "occlusionMap";
                this._uOcclusionMapMatrix = program.getLocation("occlusionMapMatrix");
            }
            if (material._diffuseFresnel) {
                this._uDiffuseFresnelEdgeBias = program.getLocation("diffuseFresnelEdgeBias");
                this._uDiffuseFresnelCenterBias = program.getLocation("diffuseFresnelCenterBias");
                this._uDiffuseFresnelEdgeColor = program.getLocation("diffuseFresnelEdgeColor");
                this._uDiffuseFresnelCenterColor = program.getLocation("diffuseFresnelCenterColor");
                this._uDiffuseFresnelPower = program.getLocation("diffuseFresnelPower");
            }
            if (material._specularFresnel) {
                this._uSpecularFresnelEdgeBias = program.getLocation("specularFresnelEdgeBias");
                this._uSpecularFresnelCenterBias = program.getLocation("specularFresnelCenterBias");
                this._uSpecularFresnelEdgeColor = program.getLocation("specularFresnelEdgeColor");
                this._uSpecularFresnelCenterColor = program.getLocation("specularFresnelCenterColor");
                this._uSpecularFresnelPower = program.getLocation("specularFresnelPower");
            }
            if (material._alphaFresnel) {
                this._uAlphaFresnelEdgeBias = program.getLocation("alphaFresnelEdgeBias");
                this._uAlphaFresnelCenterBias = program.getLocation("alphaFresnelCenterBias");
                this._uAlphaFresnelEdgeColor = program.getLocation("alphaFresnelEdgeColor");
                this._uAlphaFresnelCenterColor = program.getLocation("alphaFresnelCenterColor");
                this._uAlphaFresnelPower = program.getLocation("alphaFresnelPower");
            }
            if (material._reflectivityFresnel) {
                this._uReflectivityFresnelEdgeBias = program.getLocation("reflectivityFresnelEdgeBias");
                this._uReflectivityFresnelCenterBias = program.getLocation("reflectivityFresnelCenterBias");
                this._uReflectivityFresnelEdgeColor = program.getLocation("reflectivityFresnelEdgeColor");
                this._uReflectivityFresnelCenterColor = program.getLocation("reflectivityFresnelCenterColor");
                this._uReflectivityFresnelPower = program.getLocation("reflectivityFresnelPower");
            }
            if (material._emissiveFresnel) {
                this._uEmissiveFresnelEdgeBias = program.getLocation("emissiveFresnelEdgeBias");
                this._uEmissiveFresnelCenterBias = program.getLocation("emissiveFresnelCenterBias");
                this._uEmissiveFresnelEdgeColor = program.getLocation("emissiveFresnelEdgeColor");
                this._uEmissiveFresnelCenterColor = program.getLocation("emissiveFresnelCenterColor");
                this._uEmissiveFresnelPower = program.getLocation("emissiveFresnelPower");
            }
            break;

        case "MetallicMaterial":
            this._uBaseColor = program.getLocation("materialBaseColor");
            this._uMaterialMetallic = program.getLocation("materialMetallic");
            this._uMaterialRoughness = program.getLocation("materialRoughness");
            this._uMaterialSpecularF0 = program.getLocation("materialSpecularF0");
            this._uMaterialEmissive = program.getLocation("materialEmissive");
            this._uAlphaModeCutoff = program.getLocation("materialAlphaModeCutoff");
            if (material._baseColorMap) {
                this._uBaseColorMap = "baseColorMap";
                this._uBaseColorMapMatrix = program.getLocation("baseColorMapMatrix");
            }
            if (material._metallicMap) {
                this._uMetallicMap = "metallicMap";
                this._uMetallicMapMatrix = program.getLocation("metallicMapMatrix");
            }
            if (material._roughnessMap) {
                this._uRoughnessMap = "roughnessMap";
                this._uRoughnessMapMatrix = program.getLocation("roughnessMapMatrix");
            }
            if (material._metallicRoughnessMap) {
                this._uMetallicRoughnessMap = "metallicRoughnessMap";
                this._uMetallicRoughnessMapMatrix = program.getLocation("metallicRoughnessMapMatrix");
            }
            if (material._emissiveMap) {
                this._uEmissiveMap = "emissiveMap";
                this._uEmissiveMapMatrix = program.getLocation("emissiveMapMatrix");
            }
            if (material._occlusionMap) {
                this._uOcclusionMap = "occlusionMap";
                this._uOcclusionMapMatrix = program.getLocation("occlusionMapMatrix");
            }
            if (material._alphaMap) {
                this._uAlphaMap = "alphaMap";
                this._uAlphaMapMatrix = program.getLocation("alphaMapMatrix");
            }
            if (material._normalMap) {
                this._uNormalMap = "normalMap";
                this._uNormalMapMatrix = program.getLocation("normalMapMatrix");
            }
            break;

        case "SpecularMaterial":
            this._uMaterialDiffuse = program.getLocation("materialDiffuse");
            this._uMaterialSpecular = program.getLocation("materialSpecular");
            this._uMaterialGlossiness = program.getLocation("materialGlossiness");
            this._uMaterialReflectivity = program.getLocation("reflectivityFresnel");
            this._uMaterialEmissive = program.getLocation("materialEmissive");
            this._uAlphaModeCutoff = program.getLocation("materialAlphaModeCutoff");
            if (material._diffuseMap) {
                this._uDiffuseMap = "diffuseMap";
                this._uDiffuseMapMatrix = program.getLocation("diffuseMapMatrix");
            }
            if (material._specularMap) {
                this._uSpecularMap = "specularMap";
                this._uSpecularMapMatrix = program.getLocation("specularMapMatrix");
            }
            if (material._glossinessMap) {
                this._uGlossinessMap = "glossinessMap";
                this._uGlossinessMapMatrix = program.getLocation("glossinessMapMatrix");
            }
            if (material._specularGlossinessMap) {
                this._uSpecularGlossinessMap = "materialSpecularGlossinessMap";
                this._uSpecularGlossinessMapMatrix = program.getLocation("materialSpecularGlossinessMapMatrix");
            }
            if (material._emissiveMap) {
                this._uEmissiveMap = "emissiveMap";
                this._uEmissiveMapMatrix = program.getLocation("emissiveMapMatrix");
            }
            if (material._occlusionMap) {
                this._uOcclusionMap = "occlusionMap";
                this._uOcclusionMapMatrix = program.getLocation("occlusionMapMatrix");
            }
            if (material._alphaMap) {
                this._uAlphaMap = "alphaMap";
                this._uAlphaMapMatrix = program.getLocation("alphaMapMatrix");
            }
            if (material._normalMap) {
                this._uNormalMap = "normalMap";
                this._uNormalMapMatrix = program.getLocation("normalMapMatrix");
            }
            break;
    }

    this._aPosition = program.getAttribute("position");
    this._aNormal = program.getAttribute("normal");
    this._aUV = program.getAttribute("uv");
    this._aColor = program.getAttribute("color");
    this._aFlags = program.getAttribute("flags");

    this._uClippable = program.getLocation("clippable");
    this._uColorize = program.getLocation("colorize");
    this._uOffset = program.getLocation("offset");

    this._lastMaterialId = null;
    this._lastVertexBufsId = null;
    this._lastGeometryId = null;

    this._lastColorize = new Float32Array(4);

    this._baseTextureUnit = 0;

};

DrawRenderer.prototype._bindProgram = function (frameCtx) {

    const maxTextureUnits = WEBGL_INFO.MAX_TEXTURE_UNITS;
    const scene = this._scene;
    const gl = scene.canvas.gl;
    const lightsState = scene._lightsState;
    const project = scene.camera.project;
    let light;

    const program = this._program;

    program.bind();

    frameCtx.useProgram++;
    frameCtx.textureUnit = 0;

    this._lastMaterialId = null;
    this._lastVertexBufsId = null;
    this._lastGeometryId = null;

    this._lastColorize[0] = -1;
    this._lastColorize[1] = -1;
    this._lastColorize[2] = -1;
    this._lastColorize[3] = -1;

    gl.uniformMatrix4fv(this._uProjMatrix, false, project.matrix);

    if (scene.logarithmicDepthBufferEnabled) {
        const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2);
        gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
    }

    for (var i = 0, len = lightsState.lights.length; i < len; i++) {

        light = lightsState.lights[i];

        if (this._uLightAmbient[i]) {
            gl.uniform4f(this._uLightAmbient[i], light.color[0], light.color[1], light.color[2], light.intensity);

        } else {

            if (this._uLightColor[i]) {
                gl.uniform4f(this._uLightColor[i], light.color[0], light.color[1], light.color[2], light.intensity);
            }

            if (this._uLightPos[i]) {
                gl.uniform3fv(this._uLightPos[i], light.pos);
                if (this._uLightAttenuation[i]) {
                    gl.uniform1f(this._uLightAttenuation[i], light.attenuation);
                }
            }

            if (this._uLightDir[i]) {
                gl.uniform3fv(this._uLightDir[i], light.dir);
            }

            if (light.castsShadow) {
                if (this._uShadowViewMatrix[i]) {
                    gl.uniformMatrix4fv(this._uShadowViewMatrix[i], false, light.getShadowViewMatrix());
                }
                if (this._uShadowProjMatrix[i]) {
                    gl.uniformMatrix4fv(this._uShadowProjMatrix[i], false, light.getShadowProjMatrix());
                }
                const shadowRenderBuf = light.getShadowRenderBuf();
                if (shadowRenderBuf) {
                    program.bindTexture("shadowMap" + i, shadowRenderBuf.getTexture(), frameCtx.textureUnit);
                    frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                    frameCtx.bindTexture++;
                }
            }
        }
    }

    if (lightsState.lightMaps.length > 0 && lightsState.lightMaps[0].texture && this._uLightMap) {
        program.bindTexture(this._uLightMap, lightsState.lightMaps[0].texture, frameCtx.textureUnit);
        frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
        frameCtx.bindTexture++;
    }

    if (lightsState.reflectionMaps.length > 0 && lightsState.reflectionMaps[0].texture && this._uReflectionMap) {
        program.bindTexture(this._uReflectionMap, lightsState.reflectionMaps[0].texture, frameCtx.textureUnit);
        frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
        frameCtx.bindTexture++;
    }

    if (this._uGammaFactor) {
        gl.uniform1f(this._uGammaFactor, scene.gammaFactor);
    }

    this._baseTextureUnit = frameCtx.textureUnit;
};

/**
 * @private
 */
class EmphasisFillShaderSource {
    constructor(mesh) {
        this.vertex = buildVertex$5(mesh);
        this.fragment = buildFragment$5(mesh);
    }
}

function buildVertex$5(mesh) {

    const scene = mesh.scene;
    const lightsState = scene._lightsState;
    const normals = hasNormals(mesh);
    const sectionPlanesState = scene._sectionPlanesState;
    const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
    const quantizedGeometry = !!mesh._geometry._state.compressGeometry;
    const billboard = mesh._state.billboard;
    const stationary = mesh._state.stationary;
    const src = [];
    src.push("#version 300 es");
    src.push("// EmphasisFillShaderSource vertex shader");
    src.push("in vec3 position;");
    src.push("uniform mat4 modelMatrix;");
    src.push("uniform mat4 viewMatrix;");
    src.push("uniform mat4 projMatrix;");
    src.push("uniform vec4 colorize;");
    src.push("uniform vec3 offset;");
    if (quantizedGeometry) {
        src.push("uniform mat4 positionsDecodeMatrix;");
    }
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("uniform float logDepthBufFC;");
        src.push("out float vFragDepth;");
        src.push("bool isPerspectiveMatrix(mat4 m) {");
        src.push("    return (m[2][3] == - 1.0);");
        src.push("}");
        src.push("out float isPerspective;");
    }
    if (clipping) {
        src.push("out vec4 vWorldPosition;");
    }
    src.push("uniform vec4   lightAmbient;");
    src.push("uniform vec4   fillColor;");
    if (normals) {
        src.push("in vec3 normal;");
        src.push("uniform mat4 modelNormalMatrix;");
        src.push("uniform mat4 viewNormalMatrix;");
        for (let i = 0, len = lightsState.lights.length; i < len; i++) {
            const light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            src.push("uniform vec4 lightColor" + i + ";");
            if (light.type === "dir") {
                src.push("uniform vec3 lightDir" + i + ";");
            }
            if (light.type === "point") {
                src.push("uniform vec3 lightPos" + i + ";");
            }
            if (light.type === "spot") {
                src.push("uniform vec3 lightPos" + i + ";");
            }
        }
        if (quantizedGeometry) {
            src.push("vec3 octDecode(vec2 oct) {");
            src.push("    vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));");
            src.push("    if (v.z < 0.0) {");
            src.push("        v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);");
            src.push("    }");
            src.push("    return normalize(v);");
            src.push("}");
        }
    }
    src.push("out vec4 vColor;");
    if (billboard === "spherical" || billboard === "cylindrical") {
        src.push("void billboard(inout mat4 mat) {");
        src.push("   mat[0][0] = 1.0;");
        src.push("   mat[0][1] = 0.0;");
        src.push("   mat[0][2] = 0.0;");
        if (billboard === "spherical") {
            src.push("   mat[1][0] = 0.0;");
            src.push("   mat[1][1] = 1.0;");
            src.push("   mat[1][2] = 0.0;");
        }
        src.push("   mat[2][0] = 0.0;");
        src.push("   mat[2][1] = 0.0;");
        src.push("   mat[2][2] =1.0;");
        src.push("}");
    }
    src.push("void main(void) {");
    src.push("vec4 localPosition = vec4(position, 1.0); ");
    src.push("vec4 worldPosition;");
    if (quantizedGeometry) {
        src.push("localPosition = positionsDecodeMatrix * localPosition;");
    }
    if (normals) {
        if (quantizedGeometry) {
            src.push("vec4 localNormal = vec4(octDecode(normal.xy), 0.0); ");
        } else {
            src.push("vec4 localNormal = vec4(normal, 0.0); ");
        }
        src.push("mat4 modelNormalMatrix2 = modelNormalMatrix;");
        src.push("mat4 viewNormalMatrix2 = viewNormalMatrix;");
    }
    src.push("mat4 viewMatrix2 = viewMatrix;");
    src.push("mat4 modelMatrix2 = modelMatrix;");
    if (stationary) {
        src.push("viewMatrix2[3][0] = viewMatrix2[3][1] = viewMatrix2[3][2] = 0.0;");
    }
    if (billboard === "spherical" || billboard === "cylindrical") {
        src.push("mat4 modelViewMatrix = viewMatrix2 * modelMatrix2;");
        src.push("billboard(modelMatrix2);");
        src.push("billboard(viewMatrix2);");
        src.push("billboard(modelViewMatrix);");
        if (normals) {
            src.push("mat4 modelViewNormalMatrix =  viewNormalMatrix2 * modelNormalMatrix2;");
            src.push("billboard(modelNormalMatrix2);");
            src.push("billboard(viewNormalMatrix2);");
            src.push("billboard(modelViewNormalMatrix);");
        }
        src.push("worldPosition = modelMatrix2 * localPosition;");
        src.push("vec4 viewPosition = modelViewMatrix * localPosition;");
    } else {
        src.push("worldPosition = modelMatrix2 * localPosition;");
        src.push("worldPosition.xyz = worldPosition.xyz + offset;");
        src.push("vec4 viewPosition  = viewMatrix2 * worldPosition; ");
    }
    if (normals) {
        src.push("vec3 viewNormal = normalize((viewNormalMatrix2 * modelNormalMatrix2 * localNormal).xyz);");
    }
    src.push("vec3 reflectedColor = vec3(0.0, 0.0, 0.0);");
    src.push("vec3 viewLightDir = vec3(0.0, 0.0, -1.0);");
    src.push("float lambertian = 1.0;");
    if (normals) {
        for (let i = 0, len = lightsState.lights.length; i < len; i++) {
            const light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            if (light.type === "dir") {
                if (light.space === "view") {
                    src.push("viewLightDir = normalize(lightDir" + i + ");");
                } else {
                    src.push("viewLightDir = normalize((viewMatrix2 * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "point") {
                if (light.space === "view") {
                    src.push("viewLightDir = normalize(lightPos" + i + " - viewPosition.xyz);");
                } else {
                    src.push("viewLightDir = normalize((viewMatrix2 * vec4(lightPos" + i + ", 0.0)).xyz);");
                }
            } else {
                continue;
            }
            src.push("lambertian = max(dot(-viewNormal, viewLightDir), 0.0);");
            src.push("reflectedColor += lambertian * (lightColor" + i + ".rgb * lightColor" + i + ".a);");
        }
    }
    // TODO: A blending mode for emphasis materials, to select add/multiply/mix
    //src.push("vColor = vec4((mix(reflectedColor, fillColor.rgb, 0.7)), fillColor.a);");
    src.push("vColor = vec4(reflectedColor * fillColor.rgb, fillColor.a);");
    //src.push("vColor = vec4(reflectedColor + fillColor.rgb, fillColor.a);");
    if (clipping) {
        src.push("vWorldPosition = worldPosition;");
    }
    if (mesh._geometry._state.primitiveName === "points") {
        src.push("gl_PointSize = pointSize;");
    }
    src.push("vec4 clipPos = projMatrix * viewPosition;");
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("vFragDepth = 1.0 + clipPos.w;");
        src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
    }
    src.push("gl_Position = clipPos;");
    src.push("}");
    return src;
}

function hasNormals(mesh) {
    const primitive = mesh._geometry._state.primitiveName;
    if ((mesh._geometry._state.autoVertexNormals || mesh._geometry._state.normalsBuf) && (primitive === "triangles" || primitive === "triangle-strip" || primitive === "triangle-fan")) {
        return true;
    }
    return false;
}

function buildFragment$5(mesh) {

    const scene = mesh.scene;
    const sectionPlanesState = mesh.scene._sectionPlanesState;
    const gammaOutput = mesh.scene.gammaOutput;
    const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
    const src = [];
    src.push("#version 300 es");
    src.push("// Lambertian drawing fragment shader");
    src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
    src.push("precision highp float;");
    src.push("precision highp int;");
    src.push("#else");
    src.push("precision mediump float;");
    src.push("precision mediump int;");
    src.push("#endif");

    if (scene.logarithmicDepthBufferEnabled) {
        src.push("in float isPerspective;");
        src.push("uniform float logDepthBufFC;");
        src.push("in float vFragDepth;");
    }

    if (gammaOutput) {
        src.push("uniform float gammaFactor;");
        src.push("vec4 linearToGamma( in vec4 value, in float gammaFactor ) {");
        src.push("  return vec4( pow( value.xyz, vec3( 1.0 / gammaFactor ) ), value.w );");
        src.push("}");
    }
    if (clipping) {
        src.push("in vec4 vWorldPosition;");
        src.push("uniform bool clippable;");
        for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            src.push("uniform bool sectionPlaneActive" + i + ";");
            src.push("uniform vec3 sectionPlanePos" + i + ";");
            src.push("uniform vec3 sectionPlaneDir" + i + ";");
        }
    }
    src.push("in vec4 vColor;");
    src.push("out vec4 outColor;");
    src.push("void main(void) {");
    if (clipping) {
        src.push("if (clippable) {");
        src.push("  float dist = 0.0;");
        for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            src.push("if (sectionPlaneActive" + i + ") {");
            src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
            src.push("}");
        }
        src.push("  if (dist > 0.0) { discard; }");
        src.push("}");
    }
    if (mesh._geometry._state.primitiveName === "points") {
        src.push("vec2 cxy = 2.0 * gl_PointCoord - 1.0;");
        src.push("float r = dot(cxy, cxy);");
        src.push("if (r > 1.0) {");
        src.push("   discard;");
        src.push("}");
    }
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
    }
    if (gammaOutput) {
        src.push("outColor = linearToGamma(vColor, gammaFactor);");
    } else {
        src.push("outColor = vColor;");
    }
    src.push("}");
    return src;
}

/**
 * @author xeolabs / https://github.com/xeolabs
 */

const ids$1 = new Map$1$1({});

const tempVec3a$K = math.vec3();

/**
 * @private
 */
const EmphasisFillRenderer = function (hash, mesh) {
    this.id = ids$1.addItem({});
    this._hash = hash;
    this._scene = mesh.scene;
    this._useCount = 0;
    this._shaderSource = new EmphasisFillShaderSource(mesh);
    this._allocate(mesh);
};

const xrayFillRenderers = {};

EmphasisFillRenderer.get = function (mesh) {
    const hash = [
        mesh.scene.id,
        mesh.scene.gammaOutput ? "go" : "", // Gamma input not needed
        mesh.scene._sectionPlanesState.getHash(),
        !!mesh._geometry._state.normalsBuf ? "n" : "",
        mesh._geometry._state.compressGeometry ? "cp" : "",
        mesh._state.hash
    ].join(";");
    let renderer = xrayFillRenderers[hash];
    if (!renderer) {
        renderer = new EmphasisFillRenderer(hash, mesh);
        xrayFillRenderers[hash] = renderer;
        stats.memory.programs++;
    }
    renderer._useCount++;
    return renderer;
};

EmphasisFillRenderer.prototype.put = function () {
    if (--this._useCount === 0) {
        ids$1.removeItem(this.id);
        if (this._program) {
            this._program.destroy();
        }
        delete xrayFillRenderers[this._hash];
        stats.memory.programs--;
    }
};

EmphasisFillRenderer.prototype.webglContextRestored = function () {
    this._program = null;
};

EmphasisFillRenderer.prototype.drawMesh = function (frameCtx, mesh, mode) {

    if (!this._program) {
        this._allocate(mesh);
    }

    const scene = this._scene;
    const camera = scene.camera;
    const gl = scene.canvas.gl;
    const materialState = mode === 0 ? mesh._xrayMaterial._state : (mode === 1 ? mesh._highlightMaterial._state : mesh._selectedMaterial._state);
    const meshState = mesh._state;
    const geometryState = mesh._geometry._state;
    const origin = mesh.origin;

    if (frameCtx.lastProgramId !== this._program.id) {
        frameCtx.lastProgramId = this._program.id;
        this._bindProgram(frameCtx);
    }

    gl.uniformMatrix4fv(this._uViewMatrix, false, origin ? frameCtx.getRTCViewMatrix(meshState.originHash, origin) : camera.viewMatrix);
    gl.uniformMatrix4fv(this._uViewNormalMatrix, false, camera.viewNormalMatrix);

    if (meshState.clippable) {
        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const renderFlags = mesh.renderFlags;
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$K);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }
    }

    if (materialState.id !== this._lastMaterialId) {
        const fillColor = materialState.fillColor;
        const backfaces = materialState.backfaces;
        if (frameCtx.backfaces !== backfaces) {
            if (backfaces) {
                gl.disable(gl.CULL_FACE);
            } else {
                gl.enable(gl.CULL_FACE);
            }
            frameCtx.backfaces = backfaces;
        }
        gl.uniform4f(this._uFillColor, fillColor[0], fillColor[1], fillColor[2], materialState.fillAlpha);
        this._lastMaterialId = materialState.id;
    }

    gl.uniformMatrix4fv(this._uModelMatrix, gl.FALSE, mesh.worldMatrix);
    if (this._uModelNormalMatrix) {
        gl.uniformMatrix4fv(this._uModelNormalMatrix, gl.FALSE, mesh.worldNormalMatrix);
    }

    if (this._uClippable) {
        gl.uniform1i(this._uClippable, meshState.clippable);
    }

    gl.uniform3fv(this._uOffset, meshState.offset);

    // Bind VBOs
    if (geometryState.id !== this._lastGeometryId) {
        if (this._uPositionsDecodeMatrix) {
            gl.uniformMatrix4fv(this._uPositionsDecodeMatrix, false, geometryState.positionsDecodeMatrix);
        }
        if (this._uUVDecodeMatrix) {
            gl.uniformMatrix3fv(this._uUVDecodeMatrix, false, geometryState.uvDecodeMatrix);
        }
        if (this._aPosition) {
            this._aPosition.bindArrayBuffer(geometryState.positionsBuf);
            frameCtx.bindArray++;
        }
        if (this._aNormal) {
            this._aNormal.bindArrayBuffer(geometryState.normalsBuf);
            frameCtx.bindArray++;
        }
        if (geometryState.indicesBuf) {
            geometryState.indicesBuf.bind();
            frameCtx.bindArray++;
            // gl.drawElements(geometryState.primitive, geometryState.indicesBuf.numItems, geometryState.indicesBuf.itemType, 0);
            // frameCtx.drawElements++;
        } else if (geometryState.positionsBuf) ;
        this._lastGeometryId = geometryState.id;
    }

    if (geometryState.indicesBuf) {
        gl.drawElements(geometryState.primitive, geometryState.indicesBuf.numItems, geometryState.indicesBuf.itemType, 0);
        frameCtx.drawElements++;
    } else if (geometryState.positionsBuf) {
        gl.drawArrays(gl.TRIANGLES, 0, geometryState.positionsBuf.numItems);
        frameCtx.drawArrays++;
    }
};

EmphasisFillRenderer.prototype._allocate = function (mesh) {
    const scene = mesh.scene;
    const lightsState = scene._lightsState;
    const sectionPlanesState = scene._sectionPlanesState;
    const gl = scene.canvas.gl;
    this._program = new Program(gl, this._shaderSource);
    if (this._program.errors) {
        this.errors = this._program.errors;
        return;
    }
    const program = this._program;
    this._uPositionsDecodeMatrix = program.getLocation("positionsDecodeMatrix");
    this._uModelMatrix = program.getLocation("modelMatrix");
    this._uModelNormalMatrix = program.getLocation("modelNormalMatrix");
    this._uViewMatrix = program.getLocation("viewMatrix");
    this._uViewNormalMatrix = program.getLocation("viewNormalMatrix");
    this._uProjMatrix = program.getLocation("projMatrix");
    this._uLightAmbient = [];
    this._uLightColor = [];
    this._uLightDir = [];
    this._uLightPos = [];
    this._uLightAttenuation = [];
    for (let i = 0, len = lightsState.lights.length; i < len; i++) {
        const light = lightsState.lights[i];
        switch (light.type) {
            case "ambient":
                this._uLightAmbient[i] = program.getLocation("lightAmbient");
                break;
            case "dir":
                this._uLightColor[i] = program.getLocation("lightColor" + i);
                this._uLightPos[i] = null;
                this._uLightDir[i] = program.getLocation("lightDir" + i);
                break;
            case "point":
                this._uLightColor[i] = program.getLocation("lightColor" + i);
                this._uLightPos[i] = program.getLocation("lightPos" + i);
                this._uLightDir[i] = null;
                this._uLightAttenuation[i] = program.getLocation("lightAttenuation" + i);
                break;
        }
    }
    this._uSectionPlanes = [];
    for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
        this._uSectionPlanes.push({
            active: program.getLocation("sectionPlaneActive" + i),
            pos: program.getLocation("sectionPlanePos" + i),
            dir: program.getLocation("sectionPlaneDir" + i)
        });
    }
    this._uFillColor = program.getLocation("fillColor");
    this._aPosition = program.getAttribute("position");
    this._aNormal = program.getAttribute("normal");
    this._uClippable = program.getLocation("clippable");
    this._uGammaFactor = program.getLocation("gammaFactor");
    this._uOffset = program.getLocation("offset");
    if (scene.logarithmicDepthBufferEnabled ) {
        this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
    }
    this._lastMaterialId = null;
    this._lastVertexBufsId = null;
    this._lastGeometryId = null;
};

EmphasisFillRenderer.prototype._bindProgram = function (frameCtx) {
    const scene = this._scene;
    const gl = scene.canvas.gl;
    const lightsState = scene._lightsState;
    const camera = scene.camera;
    const project = camera.project;
    const program = this._program;
    program.bind();
    frameCtx.useProgram++;
    frameCtx.textureUnit = 0;
    this._lastMaterialId = null;
    this._lastVertexBufsId = null;
    this._lastGeometryId = null;
    this._lastIndicesBufId = null;
    gl.uniformMatrix4fv(this._uViewNormalMatrix, false, camera.normalMatrix);
    gl.uniformMatrix4fv(this._uProjMatrix, false, project.matrix);
    if (scene.logarithmicDepthBufferEnabled ) {
        const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2);
        gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
    }
    for (let i = 0, len = lightsState.lights.length; i < len; i++) {
        const light = lightsState.lights[i];
        if (this._uLightAmbient[i]) {
            gl.uniform4f(this._uLightAmbient[i], light.color[0], light.color[1], light.color[2], light.intensity);
        } else {
            if (this._uLightColor[i]) {
                gl.uniform4f(this._uLightColor[i], light.color[0], light.color[1], light.color[2], light.intensity);
            }
            if (this._uLightPos[i]) {
                gl.uniform3fv(this._uLightPos[i], light.pos);
                if (this._uLightAttenuation[i]) {
                    gl.uniform1f(this._uLightAttenuation[i], light.attenuation);
                }
            }
            if (this._uLightDir[i]) {
                gl.uniform3fv(this._uLightDir[i], light.dir);
            }
        }
    }
    if (this._uGammaFactor) {
        gl.uniform1f(this._uGammaFactor, scene.gammaFactor);
    }
};

/**
 * @private
 */
class EmphasisEdgesShaderSource {
    constructor(mesh) {
        this.vertex = buildVertex$4(mesh);
        this.fragment = buildFragment$4(mesh);
    }
}

function buildVertex$4(mesh) {
    const scene = mesh.scene;
    const sectionPlanesState = scene._sectionPlanesState;
    const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
    const quantizedGeometry = !!mesh._geometry._state.compressGeometry;
    const billboard = mesh._state.billboard;
    const stationary = mesh._state.stationary;
    const src = [];
    src.push("#version 300 es");
    src.push("// Edges drawing vertex shader");
    src.push("in vec3 position;");
    src.push("uniform mat4 modelMatrix;");
    src.push("uniform mat4 viewMatrix;");
    src.push("uniform mat4 projMatrix;");
    src.push("uniform vec4 edgeColor;");
    src.push("uniform vec3 offset;");
    if (quantizedGeometry) {
        src.push("uniform mat4 positionsDecodeMatrix;");
    }
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("uniform float logDepthBufFC;");
        src.push("out float vFragDepth;");
        src.push("bool isPerspectiveMatrix(mat4 m) {");
        src.push("    return (m[2][3] == - 1.0);");
        src.push("}");
        src.push("out float isPerspective;");
    }
    if (clipping) {
        src.push("out vec4 vWorldPosition;");
    }
    src.push("out vec4 vColor;");
    if (billboard === "spherical" || billboard === "cylindrical") {
        src.push("void billboard(inout mat4 mat) {");
        src.push("   mat[0][0] = 1.0;");
        src.push("   mat[0][1] = 0.0;");
        src.push("   mat[0][2] = 0.0;");
        if (billboard === "spherical") {
            src.push("   mat[1][0] = 0.0;");
            src.push("   mat[1][1] = 1.0;");
            src.push("   mat[1][2] = 0.0;");
        }
        src.push("   mat[2][0] = 0.0;");
        src.push("   mat[2][1] = 0.0;");
        src.push("   mat[2][2] =1.0;");
        src.push("}");
    }
    src.push("void main(void) {");
    src.push("vec4 localPosition = vec4(position, 1.0); ");
    src.push("vec4 worldPosition;");
    if (quantizedGeometry) {
        src.push("localPosition = positionsDecodeMatrix * localPosition;");
    }
    src.push("mat4 viewMatrix2 = viewMatrix;");
    src.push("mat4 modelMatrix2 = modelMatrix;");
    if (stationary) {
        src.push("viewMatrix2[3][0] = viewMatrix2[3][1] = viewMatrix2[3][2] = 0.0;");
    }
    if (billboard === "spherical" || billboard === "cylindrical") {
        src.push("mat4 modelViewMatrix = viewMatrix2 * modelMatrix2;");
        src.push("billboard(modelMatrix2);");
        src.push("billboard(viewMatrix2);");
        src.push("billboard(modelViewMatrix);");
        src.push("worldPosition = modelMatrix2 * localPosition;");
        src.push("worldPosition.xyz = worldPosition.xyz + offset;");
        src.push("vec4 viewPosition = modelViewMatrix * localPosition;");
    } else {
        src.push("worldPosition = modelMatrix2 * localPosition;");
        src.push("worldPosition.xyz = worldPosition.xyz + offset;");
        src.push("vec4 viewPosition  = viewMatrix2 * worldPosition; ");
    }
    src.push("vColor = edgeColor;");
    if (clipping) {
        src.push("vWorldPosition = worldPosition;");
    }
    src.push("vec4 clipPos = projMatrix * viewPosition;");
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("vFragDepth = 1.0 + clipPos.w;");
        src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
    }
    src.push("gl_Position = clipPos;");
    src.push("}");
    return src;
}

function buildFragment$4(mesh) {

    const scene = mesh.scene;
    const sectionPlanesState = mesh.scene._sectionPlanesState;
    const gammaOutput = mesh.scene.gammaOutput;
    const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
    const src = [];
    src.push("#version 300 es");
    src.push("// Edges drawing fragment shader");
    src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
    src.push("precision highp float;");
    src.push("precision highp int;");
    src.push("#else");
    src.push("precision mediump float;");
    src.push("precision mediump int;");
    src.push("#endif");
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("in float isPerspective;");
        src.push("uniform float logDepthBufFC;");
        src.push("in float vFragDepth;");
    }
    if (gammaOutput) {
        src.push("uniform float gammaFactor;");
        src.push("vec4 linearToGamma( in vec4 value, in float gammaFactor ) {");
        src.push("  return vec4( pow( value.xyz, vec3( 1.0 / gammaFactor ) ), value.w );");
        src.push("}");
    }
    if (clipping) {
        src.push("in vec4 vWorldPosition;");
        src.push("uniform bool clippable;");
        for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            src.push("uniform bool sectionPlaneActive" + i + ";");
            src.push("uniform vec3 sectionPlanePos" + i + ";");
            src.push("uniform vec3 sectionPlaneDir" + i + ";");
        }
    }
    src.push("in vec4 vColor;");
    src.push("out vec4 outColor;");
    src.push("void main(void) {");
    if (clipping) {
        src.push("if (clippable) {");
        src.push("  float dist = 0.0;");
        for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            src.push("if (sectionPlaneActive" + i + ") {");
            src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
            src.push("}");
        }
        src.push("  if (dist > 0.0) { discard; }");
        src.push("}");
    }
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
    }
    if (gammaOutput) {
        src.push("outColor = linearToGamma(vColor, gammaFactor);");
    } else {
        src.push("outColor = vColor;");
    }
    src.push("}");
    return src;
}

/**
 * @author xeolabs / https://github.com/xeolabs
 */

const ids = new Map$1$1({});

const tempVec3a$J = math.vec3();

/**
 * @private
 */
const EmphasisEdgesRenderer = function (hash, mesh) {
    this.id = ids.addItem({});
    this._hash = hash;
    this._scene = mesh.scene;
    this._useCount = 0;
    this._shaderSource = new EmphasisEdgesShaderSource(mesh);
    this._allocate(mesh);
};

const renderers$4 = {};

EmphasisEdgesRenderer.get = function (mesh) {
    const hash = [
        mesh.scene.id,
        mesh.scene.gammaOutput ? "go" : "", // Gamma input not needed
        mesh.scene._sectionPlanesState.getHash(),
        mesh._geometry._state.compressGeometry ? "cp" : "",
        mesh._state.hash
    ].join(";");
    let renderer = renderers$4[hash];
    if (!renderer) {
        renderer = new EmphasisEdgesRenderer(hash, mesh);
        renderers$4[hash] = renderer;
        stats.memory.programs++;
    }
    renderer._useCount++;
    return renderer;
};

EmphasisEdgesRenderer.prototype.put = function () {
    if (--this._useCount === 0) {
        ids.removeItem(this.id);
        if (this._program) {
            this._program.destroy();
        }
        delete renderers$4[this._hash];
        stats.memory.programs--;
    }
};

EmphasisEdgesRenderer.prototype.webglContextRestored = function () {
    this._program = null;
};

EmphasisEdgesRenderer.prototype.drawMesh = function (frameCtx, mesh, mode) {

    if (!this._program) {
        this._allocate(mesh);
    }

    const scene = this._scene;
    const camera = scene.camera;
    const gl = scene.canvas.gl;
    let materialState;
    const meshState = mesh._state;
    const geometry = mesh._geometry;
    const geometryState = geometry._state;
    const origin = mesh.origin;

    if (frameCtx.lastProgramId !== this._program.id) {
        frameCtx.lastProgramId = this._program.id;
        this._bindProgram(frameCtx);
    }

    gl.uniformMatrix4fv(this._uViewMatrix, false, origin ? frameCtx.getRTCViewMatrix(meshState.originHash, origin) : camera.viewMatrix);

    if (meshState.clippable) {
        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const renderFlags = mesh.renderFlags;
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$J);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }
    }

    switch (mode) {
        case 0:
            materialState = mesh._xrayMaterial._state;
            break;
        case 1:
            materialState = mesh._highlightMaterial._state;
            break;
        case 2:
            materialState = mesh._selectedMaterial._state;
            break;
        case 3:
        default:
            materialState = mesh._edgeMaterial._state;
            break;
    }

    if (materialState.id !== this._lastMaterialId) {
        const backfaces = materialState.backfaces;
        if (frameCtx.backfaces !== backfaces) {
            if (backfaces) {
                gl.disable(gl.CULL_FACE);
            } else {
                gl.enable(gl.CULL_FACE);
            }
            frameCtx.backfaces = backfaces;
        }
        if (frameCtx.lineWidth !== materialState.edgeWidth) {
            gl.lineWidth(materialState.edgeWidth);
            frameCtx.lineWidth = materialState.edgeWidth;
        }
        if (this._uEdgeColor) {
            const edgeColor = materialState.edgeColor;
            const edgeAlpha = materialState.edgeAlpha;
            gl.uniform4f(this._uEdgeColor, edgeColor[0], edgeColor[1], edgeColor[2], edgeAlpha);
        }
        this._lastMaterialId = materialState.id;
    }

    gl.uniformMatrix4fv(this._uModelMatrix, gl.FALSE, mesh.worldMatrix);

    if (this._uClippable) {
        gl.uniform1i(this._uClippable, meshState.clippable);
    }

    gl.uniform3fv(this._uOffset, meshState.offset);

    // Bind VBOs
    let indicesBuf;
    if (geometryState.primitive === gl.TRIANGLES) {
        indicesBuf = geometry._getEdgeIndices();
    } else if (geometryState.primitive === gl.LINES) {
        indicesBuf = geometryState.indicesBuf;
    }

    if (indicesBuf) {
        if (geometryState.id !== this._lastGeometryId) {
            if (this._uPositionsDecodeMatrix) {
                gl.uniformMatrix4fv(this._uPositionsDecodeMatrix, false, geometryState.positionsDecodeMatrix);
            }
            if (this._aPosition) {
                this._aPosition.bindArrayBuffer(geometryState.positionsBuf, geometryState.compressGeometry ? gl.UNSIGNED_SHORT : gl.FLOAT);
                frameCtx.bindArray++;
            }
            indicesBuf.bind();
            frameCtx.bindArray++;
            this._lastGeometryId = geometryState.id;
        }

        gl.drawElements(gl.LINES, indicesBuf.numItems, indicesBuf.itemType, 0);

        frameCtx.drawElements++;
    }
};

EmphasisEdgesRenderer.prototype._allocate = function (mesh) {

    const scene = mesh.scene;
    const gl = scene.canvas.gl;
    const sectionPlanesState = scene._sectionPlanesState;

    this._program = new Program(gl, this._shaderSource);

    if (this._program.errors) {
        this.errors = this._program.errors;
        return;
    }

    const program = this._program;

    this._uPositionsDecodeMatrix = program.getLocation("positionsDecodeMatrix");
    this._uModelMatrix = program.getLocation("modelMatrix");
    this._uViewMatrix = program.getLocation("viewMatrix");
    this._uProjMatrix = program.getLocation("projMatrix");
    this._uSectionPlanes = [];
    for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
        this._uSectionPlanes.push({
            active: program.getLocation("sectionPlaneActive" + i),
            pos: program.getLocation("sectionPlanePos" + i),
            dir: program.getLocation("sectionPlaneDir" + i)
        });
    }
    this._uEdgeColor = program.getLocation("edgeColor");
    this._aPosition = program.getAttribute("position");
    this._uClippable = program.getLocation("clippable");
    this._uGammaFactor = program.getLocation("gammaFactor");
    this._uOffset = program.getLocation("offset");

    if (scene.logarithmicDepthBufferEnabled ) {
        this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
    }

    this._lastMaterialId = null;
    this._lastVertexBufsId = null;
    this._lastGeometryId = null;
};

EmphasisEdgesRenderer.prototype._bindProgram = function (frameCtx) {

    const program = this._program;
    const scene = this._scene;
    const gl = scene.canvas.gl;
    const camera = scene.camera;
    const project = camera.project;

    program.bind();

    frameCtx.useProgram++;

    this._lastMaterialId = null;
    this._lastVertexBufsId = null;
    this._lastGeometryId = null;

    gl.uniformMatrix4fv(this._uProjMatrix, false, project.matrix);

    if (scene.logarithmicDepthBufferEnabled ) {
        const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2);
        gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
    }

    if (this._uGammaFactor) {
        gl.uniform1f(this._uGammaFactor, scene.gammaFactor);
    }
};

/**
 * @author xeolabs / https://github.com/xeolabs
 */

/**
 * @private
 */
class PickMeshShaderSource {
    constructor(mesh) {
        this.vertex = buildVertex$3(mesh);
        this.fragment = buildFragment$3(mesh);
    }
}

function buildVertex$3(mesh) {
    const scene = mesh.scene;
    const sectionPlanesState = scene._sectionPlanesState;
    const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
    const quantizedGeometry = !!mesh._geometry._state.compressGeometry;
    const billboard = mesh._state.billboard;
    const stationary = mesh._state.stationary;
    const src = [];
    src.push('#version 300 es');
    src.push("// Mesh picking vertex shader");
    src.push("in vec3 position;");
    src.push("uniform mat4 modelMatrix;");
    src.push("uniform mat4 viewMatrix;");
    src.push("uniform mat4 projMatrix;");
    src.push("out vec4 vViewPosition;");
    src.push("uniform vec3 offset;");
    if (quantizedGeometry) {
        src.push("uniform mat4 positionsDecodeMatrix;");
    }
    if (clipping) {
        src.push("out vec4 vWorldPosition;");
    }
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("uniform float logDepthBufFC;");
        src.push("out float vFragDepth;");
        src.push("bool isPerspectiveMatrix(mat4 m) {");
        src.push("    return (m[2][3] == - 1.0);");
        src.push("}");
        src.push("out float isPerspective;");
    }
    if (billboard === "spherical" || billboard === "cylindrical") {
        src.push("void billboard(inout mat4 mat) {");
        src.push("   mat[0][0] = 1.0;");
        src.push("   mat[0][1] = 0.0;");
        src.push("   mat[0][2] = 0.0;");
        if (billboard === "spherical") {
            src.push("   mat[1][0] = 0.0;");
            src.push("   mat[1][1] = 1.0;");
            src.push("   mat[1][2] = 0.0;");
        }
        src.push("   mat[2][0] = 0.0;");
        src.push("   mat[2][1] = 0.0;");
        src.push("   mat[2][2] =1.0;");
        src.push("}");
    }

    src.push("uniform vec2 pickClipPos;");

    src.push("vec4 remapClipPos(vec4 clipPos) {");
    src.push("    clipPos.xy /= clipPos.w;");
    src.push("    clipPos.xy -= pickClipPos;");
    src.push("    clipPos.xy *= clipPos.w;");
    src.push("    return clipPos;");
    src.push("}");

    src.push("void main(void) {");
    src.push("vec4 localPosition = vec4(position, 1.0); ");
    if (quantizedGeometry) {
        src.push("localPosition = positionsDecodeMatrix * localPosition;");
    }
    src.push("mat4 viewMatrix2 = viewMatrix;");
    src.push("mat4 modelMatrix2 = modelMatrix;");
    if (stationary) {
        src.push("viewMatrix2[3][0] = viewMatrix2[3][1] = viewMatrix2[3][2] = 0.0;");
    }
    if (billboard === "spherical" || billboard === "cylindrical") {
        src.push("mat4 modelViewMatrix = viewMatrix2 * modelMatrix2;");
        src.push("billboard(modelMatrix2);");
        src.push("billboard(viewMatrix2);");
    }
    src.push("   vec4 worldPosition = modelMatrix2 * localPosition;");
    src.push("   worldPosition.xyz = worldPosition.xyz + offset;");
    src.push("   vec4 viewPosition = viewMatrix2 * worldPosition;");
    if (clipping) {
        src.push("   vWorldPosition = worldPosition;");
    }
    src.push("vec4 clipPos = projMatrix * viewPosition;");
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("vFragDepth = 1.0 + clipPos.w;");
        src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
    }
    src.push("gl_Position = remapClipPos(clipPos);");
    src.push("}");
    return src;
}

function buildFragment$3(mesh) {
    const scene = mesh.scene;
    const sectionPlanesState = scene._sectionPlanesState;
    const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
    const src = [];
    src.push('#version 300 es');
    src.push("// Mesh picking fragment shader");
    src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
    src.push("precision highp float;");
    src.push("precision highp int;");
    src.push("#else");
    src.push("precision mediump float;");
    src.push("precision mediump int;");
    src.push("#endif");
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("in float isPerspective;");
        src.push("uniform float logDepthBufFC;");
        src.push("in float vFragDepth;");
    }
    src.push("uniform vec4 pickColor;");
    if (clipping) {
        src.push("uniform bool clippable;");
        src.push("in vec4 vWorldPosition;");
        for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
            src.push("uniform bool sectionPlaneActive" + i + ";");
            src.push("uniform vec3 sectionPlanePos" + i + ";");
            src.push("uniform vec3 sectionPlaneDir" + i + ";");
        }
    }
    src.push("out vec4 outColor;");
    src.push("void main(void) {");
    if (clipping) {
        src.push("if (clippable) {");
        src.push("  float dist = 0.0;");
        for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
            src.push("if (sectionPlaneActive" + i + ") {");
            src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
            src.push("}");
        }
        src.push("  if (dist > 0.0) { discard; }");
        src.push("}");
    }
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
    }
    src.push("   outColor = pickColor; ");
    src.push("}");
    return src;
}

/**
 * @author xeolabs / https://github.com/xeolabs
 */

const tempVec3a$I = math.vec3();

// No ID, because there is exactly one PickMeshRenderer per scene

/**
 * @private
 */
const PickMeshRenderer = function (hash, mesh) {
    this._hash = hash;
    this._shaderSource = new PickMeshShaderSource(mesh);
    this._scene = mesh.scene;
    this._useCount = 0;
    this._allocate(mesh);
};

const renderers$3 = {};

PickMeshRenderer.get = function (mesh) {
    const hash = [
        mesh.scene.canvas.canvas.id,
        mesh.scene._sectionPlanesState.getHash(),
        mesh._geometry._state.hash,
        mesh._state.hash
    ].join(";");
    let renderer = renderers$3[hash];
    if (!renderer) {
        renderer = new PickMeshRenderer(hash, mesh);
        if (renderer.errors) {
            console.log(renderer.errors.join("\n"));
            return null;
        }
        renderers$3[hash] = renderer;
        stats.memory.programs++;
    }
    renderer._useCount++;
    return renderer;
};

PickMeshRenderer.prototype.put = function () {
    if (--this._useCount === 0) {
        if (this._program) {
            this._program.destroy();
        }
        delete renderers$3[this._hash];
        stats.memory.programs--;
    }
};

PickMeshRenderer.prototype.webglContextRestored = function () {
    this._program = null;
};

PickMeshRenderer.prototype.drawMesh = function (frameCtx, mesh) {

    if (!this._program) {
        this._allocate(mesh);
    }

    const scene = this._scene;
    const gl = scene.canvas.gl;
    const meshState = mesh._state;
    const materialState = mesh._material._state;
    const geometryState = mesh._geometry._state;
    const origin = mesh.origin;

    if (frameCtx.lastProgramId !== this._program.id) {
        frameCtx.lastProgramId = this._program.id;
        this._bindProgram(frameCtx);
    }

    gl.uniformMatrix4fv(this._uViewMatrix, false, origin ? frameCtx.getRTCPickViewMatrix(meshState.originHash, origin) : frameCtx.pickViewMatrix);

    if (meshState.clippable) {
        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const renderFlags = mesh.renderFlags;
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$I);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }
    }

    if (materialState.id !== this._lastMaterialId) {
        const backfaces = materialState.backfaces;
        if (frameCtx.backfaces !== backfaces) {
            if (backfaces) {
                gl.disable(gl.CULL_FACE);
            } else {
                gl.enable(gl.CULL_FACE);
            }
            frameCtx.backfaces = backfaces;
        }
        const frontface = materialState.frontface;
        if (frameCtx.frontface !== frontface) {
            if (frontface) {
                gl.frontFace(gl.CCW);
            } else {
                gl.frontFace(gl.CW);
            }
            frameCtx.frontface = frontface;
        }
        this._lastMaterialId = materialState.id;
    }

    gl.uniformMatrix4fv(this._uProjMatrix, false, frameCtx.pickProjMatrix);
    gl.uniformMatrix4fv(this._uModelMatrix, false, mesh.worldMatrix);
    if (this._uClippable) {
        gl.uniform1i(this._uClippable, mesh._state.clippable);
    }
    gl.uniform3fv(this._uOffset, mesh._state.offset);

    if (geometryState.id !== this._lastGeometryId) {
        if (this._uPositionsDecodeMatrix) {
            gl.uniformMatrix4fv(this._uPositionsDecodeMatrix, false, geometryState.positionsDecodeMatrix);
        }
        if (this._aPosition) {
            this._aPosition.bindArrayBuffer(geometryState.positionsBuf, geometryState.compressGeometry ? gl.UNSIGNED_SHORT : gl.FLOAT);
            frameCtx.bindArray++;
        }
        if (geometryState.indicesBuf) {
            geometryState.indicesBuf.bind();
            frameCtx.bindArray++;
        }
        this._lastGeometryId = geometryState.id;
    }

    // Mesh-indexed color
    var pickID = mesh._state.pickID;
    const a = pickID >> 24 & 0xFF;
    const b = pickID >> 16 & 0xFF;
    const g = pickID >> 8 & 0xFF;
    const r = pickID & 0xFF;
    gl.uniform4f(this._uPickColor, r / 255, g / 255, b / 255, a / 255);

    gl.uniform2fv(this._uPickClipPos, frameCtx.pickClipPos);

    if (geometryState.indicesBuf) {
        gl.drawElements(geometryState.primitive, geometryState.indicesBuf.numItems, geometryState.indicesBuf.itemType, 0);
        frameCtx.drawElements++;
    } else if (geometryState.positions) {
        gl.drawArrays(gl.TRIANGLES, 0, geometryState.positions.numItems);
    }
};

PickMeshRenderer.prototype._allocate = function (mesh) {
    const scene = mesh.scene;
    const gl = scene.canvas.gl;
    this._program = new Program(gl, this._shaderSource);
    if (this._program.errors) {
        this.errors = this._program.errors;
        return;
    }
    const program = this._program;
    this._uPositionsDecodeMatrix = program.getLocation("positionsDecodeMatrix");
    this._uModelMatrix = program.getLocation("modelMatrix");
    this._uViewMatrix = program.getLocation("viewMatrix");
    this._uProjMatrix = program.getLocation("projMatrix");
    this._uSectionPlanes = [];
    const clips = scene._sectionPlanesState.sectionPlanes;
    for (let i = 0, len = clips.length; i < len; i++) {
        this._uSectionPlanes.push({
            active: program.getLocation("sectionPlaneActive" + i),
            pos: program.getLocation("sectionPlanePos" + i),
            dir: program.getLocation("sectionPlaneDir" + i)
        });
    }
    this._aPosition = program.getAttribute("position");
    this._uClippable = program.getLocation("clippable");
    this._uPickColor = program.getLocation("pickColor");
    this._uPickClipPos = program.getLocation("pickClipPos");
    this._uOffset = program.getLocation("offset");
    if (scene.logarithmicDepthBufferEnabled ) {
        this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
    }
    this._lastMaterialId = null;
    this._lastGeometryId = null;
};

PickMeshRenderer.prototype._bindProgram = function (frameCtx) {
    const scene = this._scene;
    const gl = scene.canvas.gl;
    const project = scene.camera.project;
    this._program.bind();
    frameCtx.useProgram++;
    if (scene.logarithmicDepthBufferEnabled ) {
        const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2);
        gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
    }
    this._lastMaterialId = null;
    this._lastGeometryId = null;
};

/**
 * @private
 */

class PickTriangleShaderSource {
    constructor(mesh) {
        this.vertex = buildVertex$2(mesh);
        this.fragment = buildFragment$2(mesh);
    }
}

function buildVertex$2(mesh) {
    const scene = mesh.scene;
    const sectionPlanesState = scene._sectionPlanesState;
    const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
    const quantizedGeometry = !!mesh._geometry._state.compressGeometry;
    const src = [];
    src.push('#version 300 es');
    src.push("// Surface picking vertex shader");
    src.push("in vec3 position;");
    src.push("in vec4 color;");
    src.push("uniform mat4 modelMatrix;");
    src.push("uniform mat4 viewMatrix;");
    src.push("uniform mat4 projMatrix;");
    src.push("uniform vec3 offset;");
    if (clipping) {
        src.push("uniform bool clippable;");
        src.push("out vec4 vWorldPosition;");
    }
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("uniform float logDepthBufFC;");
        src.push("out float vFragDepth;");
        src.push("bool isPerspectiveMatrix(mat4 m) {");
        src.push("    return (m[2][3] == - 1.0);");
        src.push("}");
        src.push("out float isPerspective;");
    }

    src.push("uniform vec2 pickClipPos;");

    src.push("vec4 remapClipPos(vec4 clipPos) {");
    src.push("    clipPos.xy /= clipPos.w;");
    src.push("    clipPos.xy -= pickClipPos;");
    src.push("    clipPos.xy *= clipPos.w;");
    src.push("    return clipPos;");
    src.push("}");

    src.push("out vec4 vColor;");
    if (quantizedGeometry) {
        src.push("uniform mat4 positionsDecodeMatrix;");
    }
    src.push("void main(void) {");
    src.push("vec4 localPosition = vec4(position, 1.0); ");
    if (quantizedGeometry) {
        src.push("localPosition = positionsDecodeMatrix * localPosition;");
    }
    src.push("   vec4 worldPosition = modelMatrix * localPosition; ");
    src.push("   worldPosition.xyz = worldPosition.xyz + offset;");
    src.push("   vec4 viewPosition = viewMatrix * worldPosition;");
    if (clipping) {
        src.push("   vWorldPosition = worldPosition;");
    }
    src.push("   vColor = color;");
    src.push("vec4 clipPos = projMatrix * viewPosition;");
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("vFragDepth = 1.0 + clipPos.w;");
        src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
    }
    src.push("gl_Position = remapClipPos(clipPos);");
    src.push("}");
    return src;
}

function buildFragment$2(mesh) {
    const scene = mesh.scene;
    const sectionPlanesState = scene._sectionPlanesState;
    const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
    const src = [];
    src.push('#version 300 es');
    src.push("// Surface picking fragment shader");
    src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
    src.push("precision highp float;");
    src.push("precision highp int;");
    src.push("#else");
    src.push("precision mediump float;");
    src.push("precision mediump int;");
    src.push("#endif");
    src.push("in vec4 vColor;");
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("in float isPerspective;");
        src.push("uniform float logDepthBufFC;");
        src.push("in float vFragDepth;");
    }
    if (clipping) {
        src.push("uniform bool clippable;");
        src.push("in vec4 vWorldPosition;");
        for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
            src.push("uniform bool sectionPlaneActive" + i + ";");
            src.push("uniform vec3 sectionPlanePos" + i + ";");
            src.push("uniform vec3 sectionPlaneDir" + i + ";");
        }
    }
    src.push("out vec4 outColor;");
    src.push("void main(void) {");
    if (clipping) {
        src.push("if (clippable) {");
        src.push("  float dist = 0.0;");
        for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
            src.push("if (sectionPlaneActive" + i + ") {");
            src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
            src.push("}");
        }
        src.push("  if (dist > 0.0) { discard; }");
        src.push("}");
    }
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
    }
    src.push("   outColor = vColor;");
    src.push("}");
    return src;
}

/**
 * @author xeolabs / https://github.com/xeolabs
 */

const tempVec3a$H = math.vec3();

/**
 * @private
 */
const PickTriangleRenderer = function (hash, mesh) {
    this._hash = hash;
    this._scene = mesh.scene;
    this._useCount = 0;
    this._shaderSource = new PickTriangleShaderSource(mesh);
    this._allocate(mesh);
};

const renderers$2 = {};

PickTriangleRenderer.get = function (mesh) {
    const hash = [
        mesh.scene.canvas.canvas.id,
        mesh.scene._sectionPlanesState.getHash(),
        mesh._geometry._state.compressGeometry ? "cp" : "",
        mesh._state.hash
    ].join(";");
    let renderer = renderers$2[hash];
    if (!renderer) {
        renderer = new PickTriangleRenderer(hash, mesh);
        if (renderer.errors) {
            console.log(renderer.errors.join("\n"));
            return null;
        }
        renderers$2[hash] = renderer;
        stats.memory.programs++;
    }
    renderer._useCount++;
    return renderer;
};

PickTriangleRenderer.prototype.put = function () {
    if (--this._useCount === 0) {
        if (this._program) {
            this._program.destroy();
        }
        delete renderers$2[this._hash];
        stats.memory.programs--;
    }
};

PickTriangleRenderer.prototype.webglContextRestored = function () {
    this._program = null;
};

PickTriangleRenderer.prototype.drawMesh = function (frameCtx, mesh) {

    if (!this._program) {
        this._allocate(mesh);
    }

    const scene = this._scene;
    const gl = scene.canvas.gl;
    const meshState = mesh._state;
    const materialState = mesh._material._state;
    const geometry = mesh._geometry;
    const geometryState = mesh._geometry._state;
    const origin = mesh.origin;
    const backfaces = materialState.backfaces;
    const frontface = materialState.frontface;
    const project = scene.camera.project;
    const positionsBuf = geometry._getPickTrianglePositions();
    const pickColorsBuf = geometry._getPickTriangleColors();

    this._program.bind();

    frameCtx.useProgram++;

    if (scene.logarithmicDepthBufferEnabled ) {
        const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2);
        gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
    }

    gl.uniformMatrix4fv(this._uViewMatrix, false, origin ? frameCtx.getRTCPickViewMatrix(meshState.originHash, origin) : frameCtx.pickViewMatrix);

    if (meshState.clippable) {
        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const renderFlags = mesh.renderFlags;
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$H);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }
    }

    gl.uniformMatrix4fv(this._uProjMatrix, false, frameCtx.pickProjMatrix);

    if (scene.logarithmicDepthBufferEnabled) {
        gl.uniform1f(this._uZFar, scene.camera.project.far);
    }

    if (frameCtx.backfaces !== backfaces) {
        if (backfaces) {
            gl.disable(gl.CULL_FACE);
        } else {
            gl.enable(gl.CULL_FACE);
        }
        frameCtx.backfaces = backfaces;
    }
    if (frameCtx.frontface !== frontface) {
        if (frontface) {
            gl.frontFace(gl.CCW);
        } else {
            gl.frontFace(gl.CW);
        }
        frameCtx.frontface = frontface;
    }

    gl.uniformMatrix4fv(this._uModelMatrix, false, mesh.worldMatrix);
    if (this._uClippable) {
        gl.uniform1i(this._uClippable, mesh._state.clippable);
    }
    gl.uniform3fv(this._uOffset, mesh._state.offset);
    if (this._uPositionsDecodeMatrix) {
        gl.uniformMatrix4fv(this._uPositionsDecodeMatrix, false, geometryState.positionsDecodeMatrix);
        this._aPosition.bindArrayBuffer(positionsBuf, geometryState.compressGeometry ? gl.UNSIGNED_SHORT : gl.FLOAT);
    } else {
        this._aPosition.bindArrayBuffer(positionsBuf);
    }

    gl.uniform2fv(this._uPickClipPos, frameCtx.pickClipPos);

    pickColorsBuf.bind();
    gl.enableVertexAttribArray(this._aColor.location);
    gl.vertexAttribPointer(this._aColor.location, pickColorsBuf.itemSize, pickColorsBuf.itemType, true, 0, 0); // Normalize
    gl.drawArrays(geometryState.primitive, 0, positionsBuf.numItems / 3);
};

PickTriangleRenderer.prototype._allocate = function (mesh) {
    const scene = mesh.scene;
    const gl = scene.canvas.gl;
    this._program = new Program(gl, this._shaderSource);
    this._useCount = 0;
    if (this._program.errors) {
        this.errors = this._program.errors;
        return;
    }
    const program = this._program;
    this._uPositionsDecodeMatrix = program.getLocation("positionsDecodeMatrix");
    this._uModelMatrix = program.getLocation("modelMatrix");
    this._uViewMatrix = program.getLocation("viewMatrix");
    this._uProjMatrix = program.getLocation("projMatrix");
    this._uSectionPlanes = [];
    const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
    for (let i = 0, len = sectionPlanes.length; i < len; i++) {
        this._uSectionPlanes.push({
            active: program.getLocation("sectionPlaneActive" + i),
            pos: program.getLocation("sectionPlanePos" + i),
            dir: program.getLocation("sectionPlaneDir" + i)
        });
    }
    this._aPosition = program.getAttribute("position");
    this._aColor = program.getAttribute("color");
    this._uPickClipPos = program.getLocation("pickClipPos");
    this._uClippable = program.getLocation("clippable");
    this._uOffset = program.getLocation("offset");
    if (scene.logarithmicDepthBufferEnabled ) {
        this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
    }
};

/**
 * @author xeolabs / https://github.com/xeolabs
 */

/**
 * @private
 */
class OcclusionShaderSource {
    constructor(mesh) {
        this.vertex = buildVertex$1(mesh);
        this.fragment = buildFragment$1(mesh);
    }
}

function buildVertex$1(mesh) {
    const scene = mesh.scene;
    const sectionPlanesState = scene._sectionPlanesState;
    const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
    const quantizedGeometry = !!mesh._geometry._state.compressGeometry;
    const billboard = mesh._state.billboard;
    const stationary = mesh._state.stationary;
    const src = [];
    src.push('#version 300 es');
    src.push("// Mesh occlusion vertex shader");
    src.push("in vec3 position;");
    src.push("uniform mat4 modelMatrix;");
    src.push("uniform mat4 viewMatrix;");
    src.push("uniform mat4 projMatrix;");
    src.push("uniform vec3 offset;");
    if (quantizedGeometry) {
        src.push("uniform mat4 positionsDecodeMatrix;");
    }
    if (clipping) {
        src.push("out vec4 vWorldPosition;");
    }
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("uniform float logDepthBufFC;");
        src.push("out float vFragDepth;");
        src.push("bool isPerspectiveMatrix(mat4 m) {");
        src.push("    return (m[2][3] == - 1.0);");
        src.push("}");
        src.push("out float isPerspective;");
    }
    if (billboard === "spherical" || billboard === "cylindrical") {
        src.push("void billboard(inout mat4 mat) {");
        src.push("   mat[0][0] = 1.0;");
        src.push("   mat[0][1] = 0.0;");
        src.push("   mat[0][2] = 0.0;");
        if (billboard === "spherical") {
            src.push("   mat[1][0] = 0.0;");
            src.push("   mat[1][1] = 1.0;");
            src.push("   mat[1][2] = 0.0;");
        }
        src.push("   mat[2][0] = 0.0;");
        src.push("   mat[2][1] = 0.0;");
        src.push("   mat[2][2] =1.0;");
        src.push("}");
    }
    src.push("void main(void) {");
    src.push("vec4 localPosition = vec4(position, 1.0); ");
    src.push("vec4 worldPosition;");
    if (quantizedGeometry) {
        src.push("localPosition = positionsDecodeMatrix * localPosition;");
    }
    src.push("mat4 viewMatrix2 = viewMatrix;");
    src.push("mat4 modelMatrix2 = modelMatrix;");
    if (stationary) {
        src.push("viewMatrix2[3][0] = viewMatrix2[3][1] = viewMatrix2[3][2] = 0.0;");
    }
    if (billboard === "spherical" || billboard === "cylindrical") {
        src.push("mat4 modelViewMatrix = viewMatrix2 * modelMatrix2;");
        src.push("billboard(modelMatrix2);");
        src.push("billboard(viewMatrix2);");
        src.push("billboard(modelViewMatrix);");
        src.push("worldPosition = modelMatrix2 * localPosition;");
        src.push("worldPosition.xyz = worldPosition.xyz + offset;");
        src.push("vec4 viewPosition = modelViewMatrix * localPosition;");
    } else {
        src.push("worldPosition = modelMatrix2 * localPosition;");
        src.push("worldPosition.xyz = worldPosition.xyz + offset;");
        src.push("vec4 viewPosition  = viewMatrix2 * worldPosition; ");
    }
    if (clipping) {
        src.push("   vWorldPosition = worldPosition;");
    }
    src.push("vec4 clipPos = projMatrix * viewPosition;");
    if (scene.logarithmicDepthBufferEnabled) {
        src.push("vFragDepth = 1.0 + clipPos.w;");
        src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
    }
    src.push("gl_Position = clipPos;");
    src.push("}");
    return src;
}

function buildFragment$1(mesh) {

    const scene = mesh.scene;
    const sectionPlanesState = scene._sectionPlanesState;
    const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
    const src = [];
    src.push('#version 300 es');
    src.push("// Mesh occlusion fragment shader");
    src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
    src.push("precision highp float;");
    src.push("precision highp int;");
    src.push("#else");
    src.push("precision mediump float;");
    src.push("precision mediump int;");
    src.push("#endif");

    if (scene.logarithmicDepthBufferEnabled) {
        src.push("in float isPerspective;");
        src.push("uniform float logDepthBufFC;");
        src.push("in float vFragDepth;");
    }

    if (clipping) {
        src.push("uniform bool clippable;");
        src.push("in vec4 vWorldPosition;");
        for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
            src.push("uniform bool sectionPlaneActive" + i + ";");
            src.push("uniform vec3 sectionPlanePos" + i + ";");
            src.push("uniform vec3 sectionPlaneDir" + i + ";");
        }
    }

    src.push("out vec4 outColor;");

    src.push("void main(void) {");

    if (clipping) {
        src.push("if (clippable) {");
        src.push("  float dist = 0.0;");
        for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
            src.push("if (sectionPlaneActive" + i + ") {");
            src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
            src.push("}");
        }
        src.push("  if (dist > 0.0) { discard; }");
        src.push("}");
    }

    src.push("   outColor = vec4(0.0, 0.0, 1.0, 1.0); ");

    if (scene.logarithmicDepthBufferEnabled) {
        src.push("gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
    }

    src.push("}");

    return src;
}

/**
 * @author xeolabs / https://github.com/xeolabs
 */

const tempVec3a$G = math.vec3();

// No ID, because there is exactly one PickMeshRenderer per scene

/**
 * @private
 */
const OcclusionRenderer = function (hash, mesh) {
    this._hash = hash;
    this._shaderSource = new OcclusionShaderSource(mesh);
    this._scene = mesh.scene;
    this._useCount = 0;
    this._allocate(mesh);
};

const renderers$1 = {};

OcclusionRenderer.get = function (mesh) {
    const hash = [
        mesh.scene.canvas.canvas.id,
        mesh.scene._sectionPlanesState.getHash(),
        mesh._geometry._state.hash,
        mesh._state.occlusionHash
    ].join(";");
    let renderer = renderers$1[hash];
    if (!renderer) {
        renderer = new OcclusionRenderer(hash, mesh);
        if (renderer.errors) {
            console.log(renderer.errors.join("\n"));
            return null;
        }
        renderers$1[hash] = renderer;
        stats.memory.programs++;
    }
    renderer._useCount++;
    return renderer;
};

OcclusionRenderer.prototype.put = function () {
    if (--this._useCount === 0) {
        if (this._program) {
            this._program.destroy();
        }
        delete renderers$1[this._hash];
        stats.memory.programs--;
    }
};

OcclusionRenderer.prototype.webglContextRestored = function () {
    this._program = null;
};

OcclusionRenderer.prototype.drawMesh = function (frameCtx, mesh) {

    if (!this._program) {
        this._allocate(mesh);
    }

    const scene = this._scene;
    const gl = scene.canvas.gl;
    const materialState = mesh._material._state;
    const meshState = mesh._state;
    const geometryState = mesh._geometry._state;
    const origin = mesh.origin;

    if (materialState.alpha < 1.0) {
        return;
    }

    if (frameCtx.lastProgramId !== this._program.id) {
        frameCtx.lastProgramId = this._program.id;
        this._bindProgram(frameCtx);
    }

    if (materialState.id !== this._lastMaterialId) {
        const backfaces = materialState.backfaces;
        if (frameCtx.backfaces !== backfaces) {
            if (backfaces) {
                gl.disable(gl.CULL_FACE);
            } else {
                gl.enable(gl.CULL_FACE);
            }
            frameCtx.backfaces = backfaces;
        }
        const frontface = materialState.frontface;
        if (frameCtx.frontface !== frontface) {
            if (frontface) {
                gl.frontFace(gl.CCW);
            } else {
                gl.frontFace(gl.CW);
            }
            frameCtx.frontface = frontface;
        }
        this._lastMaterialId = materialState.id;
    }

    const camera = scene.camera;

    gl.uniformMatrix4fv(this._uViewMatrix, false, origin ? frameCtx.getRTCViewMatrix(meshState.originHash, origin) : camera.viewMatrix);

    if (meshState.clippable) {
        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const renderFlags = mesh.renderFlags;
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$G);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }
    }

    gl.uniformMatrix4fv(this._uProjMatrix, false, camera._project._state.matrix);
    gl.uniformMatrix4fv(this._uModelMatrix, gl.FALSE, mesh.worldMatrix);

    if (this._uClippable) {
        gl.uniform1i(this._uClippable, mesh._state.clippable);
    }

    gl.uniform3fv(this._uOffset, mesh._state.offset);

    if (geometryState.id !== this._lastGeometryId) {
        if (this._uPositionsDecodeMatrix) {
            gl.uniformMatrix4fv(this._uPositionsDecodeMatrix, false, geometryState.positionsDecodeMatrix);
        }
        if (this._aPosition) {
            this._aPosition.bindArrayBuffer(geometryState.positionsBuf, geometryState.compressGeometry ? gl.UNSIGNED_SHORT : gl.FLOAT);
            frameCtx.bindArray++;
        }
        if (geometryState.indicesBuf) {
            geometryState.indicesBuf.bind();
            frameCtx.bindArray++;
        }
        this._lastGeometryId = geometryState.id;
    }
    if (geometryState.indicesBuf) {
        gl.drawElements(geometryState.primitive, geometryState.indicesBuf.numItems, geometryState.indicesBuf.itemType, 0);
        frameCtx.drawElements++;
    } else if (geometryState.positions) {
        gl.drawArrays(gl.TRIANGLES, 0, geometryState.positions.numItems);
    }
};

OcclusionRenderer.prototype._allocate = function (mesh) {
    const scene = mesh.scene;
    const gl = scene.canvas.gl;
    this._program = new Program(gl, this._shaderSource);
    if (this._program.errors) {
        this.errors = this._program.errors;
        return;
    }
    const program = this._program;
    this._uPositionsDecodeMatrix = program.getLocation("positionsDecodeMatrix");
    this._uModelMatrix = program.getLocation("modelMatrix");
    this._uViewMatrix = program.getLocation("viewMatrix");
    this._uProjMatrix = program.getLocation("projMatrix");
    this._uSectionPlanes = [];
    const clips = scene._sectionPlanesState.sectionPlanes;
    for (let i = 0, len = clips.length; i < len; i++) {
        this._uSectionPlanes.push({
            active: program.getLocation("sectionPlaneActive" + i),
            pos: program.getLocation("sectionPlanePos" + i),
            dir: program.getLocation("sectionPlaneDir" + i)
        });
    }
    this._aPosition = program.getAttribute("position");
    this._uClippable = program.getLocation("clippable");
    this._uOffset = program.getLocation("offset");
    if (scene.logarithmicDepthBufferEnabled ) {
        this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
    }
    this._lastMaterialId = null;
    this._lastVertexBufsId = null;
    this._lastGeometryId = null;
};

OcclusionRenderer.prototype._bindProgram = function (frameCtx) {
    const scene = this._scene;
    const project = scene.camera.project;
    const gl = scene.canvas.gl;
    this._program.bind();
    frameCtx.useProgram++;
    gl.uniformMatrix4fv(this._uProjMatrix, false, project.matrix);
    if (scene.logarithmicDepthBufferEnabled ) {
        const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2);
        gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
    }
    this._lastMaterialId = null;
    this._lastVertexBufsId = null;
    this._lastGeometryId = null;
};

/**
 * @private
 */
class ShadowShaderSource {
    constructor(mesh) {
        this.vertex = buildVertex(mesh);
        this.fragment = buildFragment(mesh);
    }
}

function buildVertex(mesh) {
    const scene = mesh.scene;
    const clipping = scene._sectionPlanesState.sectionPlanes.length > 0;
    const quantizedGeometry = !!mesh._geometry._state.compressGeometry;
    const src = [];
    src.push("// Mesh shadow vertex shader");
    src.push("in vec3 position;");
    src.push("uniform mat4 modelMatrix;");
    src.push("uniform mat4 shadowViewMatrix;");
    src.push("uniform mat4 shadowProjMatrix;");
    src.push("uniform vec3 offset;");
    if (quantizedGeometry) {
        src.push("uniform mat4 positionsDecodeMatrix;");
    }
    if (clipping) {
        src.push("out vec4 vWorldPosition;");
    }
    src.push("void main(void) {");
    src.push("vec4 localPosition = vec4(position, 1.0); ");
    src.push("vec4 worldPosition;");
    if (quantizedGeometry) {
        src.push("localPosition = positionsDecodeMatrix * localPosition;");
    }
    src.push("worldPosition = modelMatrix * localPosition;");
    src.push("worldPosition.xyz = worldPosition.xyz + offset;");
    src.push("vec4 viewPosition  = shadowViewMatrix * worldPosition; ");
    if (clipping) {
        src.push("vWorldPosition = worldPosition;");
    }
    src.push("   gl_Position = shadowProjMatrix * viewPosition;");
    src.push("}");
    return src;
}

function buildFragment(mesh) {
    const scene = mesh.scene;
    scene.canvas.gl;
    const sectionPlanesState = scene._sectionPlanesState;
    const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
    const src = [];
    src.push("// Mesh shadow fragment shader");

    src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
    src.push("precision highp float;");
    src.push("precision highp int;");
    src.push("#else");
    src.push("precision mediump float;");
    src.push("precision mediump int;");
    src.push("#endif");

    if (clipping) {
        src.push("uniform bool clippable;");
        src.push("in vec4 vWorldPosition;");
        for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
            src.push("uniform bool sectionPlaneActive" + i + ";");
            src.push("uniform vec3 sectionPlanePos" + i + ";");
            src.push("uniform vec3 sectionPlaneDir" + i + ";");
        }
    }

    src.push("vec4 encodeFloat( const in float depth ) {");
    src.push("  const vec4 bitShift = vec4(256 * 256 * 256, 256 * 256, 256, 1.0);");
    src.push("  const vec4 bitMask = vec4(0, 1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0);");
    src.push("  vec4 comp = fract(depth * bitShift);");
    src.push("  comp -= comp.xxyz * bitMask;");
    src.push("  return comp;");
    src.push("}");

    src.push("out vec4 outColor;");

    src.push("void main(void) {");
    if (clipping) {
        src.push("if (clippable) {");
        src.push("  float dist = 0.0;");
        for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
            src.push("if (sectionPlaneActive" + i + ") {");
            src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
            src.push("}");
        }
        src.push("  if (dist > 0.0) { discard; }");
        src.push("}");
    }
    src.push("outColor = encodeFloat(gl_FragCoord.z);");
    src.push("}");
    return src;
}

/**
 * @private
 */
const ShadowRenderer = function (hash, mesh) {
    this._hash = hash;
    this._shaderSource = new ShadowShaderSource(mesh);
    this._scene = mesh.scene;
    this._useCount = 0;
    this._allocate(mesh);
};

const renderers = {};

ShadowRenderer.get = function (mesh) {
    const scene = mesh.scene;
    const hash = [scene.canvas.canvas.id, scene._sectionPlanesState.getHash(), mesh._geometry._state.hash, mesh._state.hash].join(";");
    let renderer = renderers[hash];
    if (!renderer) {
        renderer = new ShadowRenderer(hash, mesh);
        if (renderer.errors) {
            console.log(renderer.errors.join("\n"));
            return null;
        }
        renderers[hash] = renderer;
        stats.memory.programs++;
    }
    renderer._useCount++;
    return renderer;
};

ShadowRenderer.prototype.put = function () {
    if (--this._useCount === 0) {
        if (this._program) {
            this._program.destroy();
        }
        delete renderers[this._hash];
        stats.memory.programs--;
    }
};

ShadowRenderer.prototype.webglContextRestored = function () {
    this._program = null;
};

ShadowRenderer.prototype.drawMesh = function (frame, mesh) {
    if (!this._program) {
        this._allocate(mesh);
    }
    const scene = this._scene;
    const gl = scene.canvas.gl;
    const materialState = mesh._material._state;
    const geometryState = mesh._geometry._state;
    if (frame.lastProgramId !== this._program.id) {
        frame.lastProgramId = this._program.id;
        this._bindProgram(frame);
    }
    if (materialState.id !== this._lastMaterialId) {
        const backfaces = materialState.backfaces;
        if (frame.backfaces !== backfaces) {
            if (backfaces) {
                gl.disable(gl.CULL_FACE);
            } else {
                gl.enable(gl.CULL_FACE);
            }
            frame.backfaces = backfaces;
        }
        const frontface = materialState.frontface;
        if (frame.frontface !== frontface) {
            if (frontface) {
                gl.frontFace(gl.CCW);
            } else {
                gl.frontFace(gl.CW);
            }
            frame.frontface = frontface;
        }
        if (frame.lineWidth !== materialState.lineWidth) {
            gl.lineWidth(materialState.lineWidth);
            frame.lineWidth = materialState.lineWidth;
        }
        if (this._uPointSize) {
            gl.uniform1i(this._uPointSize, materialState.pointSize);
        }
        this._lastMaterialId = materialState.id;
    }
    gl.uniformMatrix4fv(this._uModelMatrix, gl.FALSE, mesh.worldMatrix);
    if (geometryState.combineGeometry) {
        const vertexBufs = mesh.vertexBufs;
        if (vertexBufs.id !== this._lastVertexBufsId) {
            if (vertexBufs.positionsBuf && this._aPosition) {
                this._aPosition.bindArrayBuffer(vertexBufs.positionsBuf, vertexBufs.compressGeometry ? gl.UNSIGNED_SHORT : gl.FLOAT);
                frame.bindArray++;
            }
            this._lastVertexBufsId = vertexBufs.id;
        }
    }
    if (this._uClippable) {
        gl.uniform1i(this._uClippable, mesh._state.clippable);
    }
    gl.uniform3fv(this._uOffset, mesh._state.offset);
    if (geometryState.id !== this._lastGeometryId) {
        if (this._uPositionsDecodeMatrix) {
            gl.uniformMatrix4fv(this._uPositionsDecodeMatrix, false, geometryState.positionsDecodeMatrix);
        }
        if (geometryState.combineGeometry) { // VBOs were bound by the preceding VertexBufs chunk
            if (geometryState.indicesBufCombined) {
                geometryState.indicesBufCombined.bind();
                frame.bindArray++;
            }
        } else {
            if (this._aPosition) {
                this._aPosition.bindArrayBuffer(geometryState.positionsBuf, geometryState.compressGeometry ? gl.UNSIGNED_SHORT : gl.FLOAT);
                frame.bindArray++;
            }
            if (geometryState.indicesBuf) {
                geometryState.indicesBuf.bind();
                frame.bindArray++;
            }
        }
        this._lastGeometryId = geometryState.id;
    }
    if (geometryState.combineGeometry) {
        if (geometryState.indicesBufCombined) {
            gl.drawElements(geometryState.primitive, geometryState.indicesBufCombined.numItems, geometryState.indicesBufCombined.itemType, 0);
            frame.drawElements++;
        }
    } else {
        if (geometryState.indicesBuf) {
            gl.drawElements(geometryState.primitive, geometryState.indicesBuf.numItems, geometryState.indicesBuf.itemType, 0);
            frame.drawElements++;
        } else if (geometryState.positions) {
            gl.drawArrays(gl.TRIANGLES, 0, geometryState.positions.numItems);
            frame.drawArrays++;
        }
    }
};

ShadowRenderer.prototype._allocate = function (mesh) {
    const scene = mesh.scene;
    const gl = scene.canvas.gl;
    this._program = new Program(gl, this._shaderSource);
    this._scene = scene;
    this._useCount = 0;
    if (this._program.errors) {
        this.errors = this._program.errors;
        return;
    }
    const program = this._program;
    this._uPositionsDecodeMatrix = program.getLocation("positionsDecodeMatrix");
    this._uModelMatrix = program.getLocation("modelMatrix");
    this._uShadowViewMatrix = program.getLocation("shadowViewMatrix");
    this._uShadowProjMatrix = program.getLocation("shadowProjMatrix");
    this._uSectionPlanes = {};
    const clips = scene._sectionPlanesState.sectionPlanes;
    for (let i = 0, len = clips.length; i < len; i++) {
        this._uSectionPlanes.push({
            active: program.getLocation("sectionPlaneActive" + i),
            pos: program.getLocation("sectionPlanePos" + i),
            dir: program.getLocation("sectionPlaneDir" + i)
        });
    }
    this._aPosition = program.getAttribute("position");
    this._uClippable = program.getLocation("clippable");
    this._uOffset = program.getLocation("offset");
    this._lastMaterialId = null;
    this._lastVertexBufsId = null;
    this._lastGeometryId = null;
};

ShadowRenderer.prototype._bindProgram = function (frame) {
    if (!this._program) {
        this._allocate(mesh);
    }
    const scene = this._scene;
    const gl = scene.canvas.gl;
    const sectionPlanesState = scene._sectionPlanesState;
    this._program.bind();
    frame.useProgram++;
    gl.uniformMatrix4fv(this._uShadowViewMatrix, false, frame.shadowViewMatrix);
    gl.uniformMatrix4fv(this._uShadowProjMatrix, false, frame.shadowProjMatrix);
    this._lastMaterialId = null;
    this._lastVertexBufsId = null;
    this._lastGeometryId = null;
    if (sectionPlanesState.getNumAllocatedSectionPlanes() > 0) {
        let sectionPlaneUniforms;
        let uSectionPlaneActive;
        let sectionPlane;
        let uSectionPlanePos;
        let uSectionPlaneDir;
        for (let i = 0, len = this._uSectionPlanes.length; i < len; i++) {
            sectionPlaneUniforms = this._uSectionPlanes[i];
            uSectionPlaneActive = sectionPlaneUniforms.active;
            sectionPlane = sectionPlanesState.sectionPlanes[i];
            if (uSectionPlaneActive) {
                gl.uniform1i(uSectionPlaneActive, sectionPlane.active);
            }
            uSectionPlanePos = sectionPlaneUniforms.pos;
            if (uSectionPlanePos) {
                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
            }
            uSectionPlaneDir = sectionPlaneUniforms.dir;
            if (uSectionPlaneDir) {
                gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
            }
        }
    }
};

/**
 * Indicates what rendering needs to be done for the layers within a {@link Drawable}.
 *
 * Each Drawable has a RenderFlags in {@link Drawable#renderFlags}.
 *
 * Before rendering each frame, {@link Renderer} will call {@link Drawable#rebuildRenderFlags} on each {@link Drawable}.
 *
 * Then, when rendering a frame, Renderer will apply rendering passes to each Drawable according on what flags are set in {@link Drawable#renderFlags}.
 *
 * @private
 */
class RenderFlags {

    /**
     * @private
     */
    constructor() {

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate which layers are visible within the {@link Drawable}.
         *
         * This is a list of IDs of visible layers within the {@link Drawable}. The IDs will be whatever the
         * {@link Drawable} uses to identify its layers, usually integers.
         *
         * @property visibleLayers
         * @type {Number[]}
         */
        this.visibleLayers = [];


        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate which {@link SectionPlane}s are active within each layer of the {@link Drawable}.
         *
         * Layout is as follows:
         *
         * ````[
         *      false, false, true, // Layer 0, SectionPlanes 0, 1, 2
         *      false, true, true,  // Layer 1, SectionPlanes 0, 1, 2
         *      true, false, true   // Layer 2, SectionPlanes 0, 1, 2
         * ]````
         *
         * @property sectionPlanesActivePerLayer
         * @type {Boolean[]}
         */
        this.sectionPlanesActivePerLayer = [];

        this.reset();
    }

    /**
     * @private
     */
    reset() {

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate whether the {@link Drawable} is culled.
         * 
         * When this is ````false````, then all of the other properties on ````RenderFlags```` will remain at their default values.
         * 
         * @property culled
         * @type {Boolean}
         */
        this.culled = false;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate whether the {@link Drawable} is sliced by any {@link SectionPlane}s.
         *
         * @property sectioned
         * @type {Boolean}
         */
        this.sectioned  = false;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate the number of layers within the {@link Drawable}.
         *
         * @property numLayers
         * @type {Number}
         */
        this.numLayers = 0;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate the number of visible layers within the {@link Drawable}.
         *
         * @property numVisibleLayers
         * @type {Number}
         */
        this.numVisibleLayers = 0;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs {@link Drawable#drawColorOpaque}.
         * @property colorOpaque
         * @type {boolean}
         */
        this.colorOpaque = false;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs {@link Drawable#drawColorTransparent}.
         * @property colorTransparent
         * @type {boolean}
         */
        this.colorTransparent = false;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs {@link Drawable#drawEdgesColorOpaque}.
         * @property edgesOpaque
         * @type {boolean}
         */
        this.edgesOpaque = false;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs {@link Drawable#drawEdgesColorTransparent}.
         * @property edgesTransparent
         * @type {boolean}
         */
        this.edgesTransparent = false;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs an opaque {@link Drawable#drawSilhouetteXRayed}.
         * @property xrayedSilhouetteOpaque
         * @type {boolean}
         */
        this.xrayedSilhouetteOpaque = false;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs an opaque {@link Drawable#drawEdgesXRayed}.
         * @property xrayedEdgesOpaque
         * @type {boolean}
         */
        this.xrayedEdgesOpaque = false;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs a transparent {@link Drawable#drawSilhouetteXRayed}.
         * @property xrayedSilhouetteTransparent
         * @type {boolean}
         */
        this.xrayedSilhouetteTransparent = false;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs a transparent {@link Drawable#drawEdgesXRayed}.
         * @property xrayedEdgesTransparent
         * @type {boolean}
         */
        this.xrayedEdgesTransparent = false;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs an opaque {@link Drawable#drawSilhouetteHighlighted}.
         * @property highlightedSilhouetteOpaque
         * @type {boolean}
         */
        this.highlightedSilhouetteOpaque = false;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs an opaque {@link Drawable#drawEdgesHighlighted}.
         * @property highlightedEdgesOpaque
         * @type {boolean}
         */
        this.highlightedEdgesOpaque = false;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs a transparent {@link Drawable#drawSilhouetteHighlighted}.
         * @property highlightedSilhouetteTransparent
         * @type {boolean}
         */
        this.highlightedSilhouetteTransparent = false;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs a transparent {@link Drawable#drawEdgesHighlighted}.
         * @property highlightedEdgesTransparent
         * @type {boolean}
         */
        this.highlightedEdgesTransparent = false;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs an opaque {@link Drawable#drawSilhouetteSelected}.
         * @property selectedSilhouetteOpaque
         * @type {boolean}
         */
        this.selectedSilhouetteOpaque = false;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs an opaque {@link Drawable#drawEdgesSelected}.
         * @property selectedEdgesOpaque
         * @type {boolean}
         */
        this.selectedEdgesOpaque = false;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs a transparent {@link Drawable#drawSilhouetteSelected}.
         * @property selectedSilhouetteTransparent
         * @type {boolean}
         */
        this.selectedSilhouetteTransparent = false;

        /**
         * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs a transparent {@link Drawable#drawEdgesSelected}.
         * @property selectedEdgesTransparent
         * @type {boolean}
         */
        this.selectedEdgesTransparent = false;
    }
}

/**
 Fired when this Mesh is picked via a call to {@link Scene/pick:method"}}Scene#pick(){{/crossLink}}.

 The event parameters will be the hit result returned by the {@link Scene/pick:method"}}Scene#pick(){{/crossLink}} method.
 @event picked
 */

const obb = math.OBB3();
const angleAxis$1 = math.vec4();
const q1$1 = math.vec4();
const q2$1 = math.vec4();
const xAxis$1 = math.vec3([1, 0, 0]);
const yAxis$1 = math.vec3([0, 1, 0]);
const zAxis$1 = math.vec3([0, 0, 1]);

const veca = math.vec3(3);
const vecb = math.vec3(3);

const identityMat$1 = math.identityMat4();

/**
 * @desc An {@link Entity} that is a drawable element, with a {@link Geometry} and a {@link Material}, that can be
 * connected into a scene graph using {@link Node}s.
 *
 * ## Usage
 *
 * The example below is the same as the one given for {@link Node}, since the two classes work together.  In this example,
 * we'll create a scene graph in which a root {@link Node} represents a group and the Meshes are leaves.
 *
 * Since {@link Node} implements {@link Entity}, we can designate the root {@link Node} as a model, causing it to be registered by its
 * ID in {@link Scene#models}.
 *
 * Since Mesh also implements {@link Entity}, we can designate the leaf Meshes as objects, causing them to
 * be registered by their IDs in {@link Scene#objects}.
 *
 * We can then find those {@link Entity} types in {@link Scene#models} and {@link Scene#objects}.
 *
 * We can also update properties of our object-Meshes via calls to {@link Scene#setObjectsHighlighted} etc.
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#sceneGraph)]
 *
 * ````javascript
 * import {Viewer, Mesh, Node, PhongMaterial, buildBoxGeometry, ReadableGeometry} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas"
 * });
 *
 * viewer.scene.camera.eye = [-21.80, 4.01, 6.56];
 * viewer.scene.camera.look = [0, -5.75, 0];
 * viewer.scene.camera.up = [0.37, 0.91, -0.11];
 *
 * const boxGeometry = new ReadableGeometry(viewer.scene, buildBoxGeometry({
 *      xSize: 1,
 *      ySize: 1,
 *      zSize: 1
 * }));
 *
 * new Node(viewer.scene, {
 *      id: "table",
 *      isModel: true, // <---------- Node represents a model, so is registered by ID in viewer.scene.models
 *      rotation: [0, 50, 0],
 *      position: [0, 0, 0],
 *      scale: [1, 1, 1],
 *
 *      children: [
 *
 *          new Mesh(viewer.scene, { // Red table leg
 *              id: "redLeg",
 *              isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects
 *              position: [-4, -6, -4],
 *              scale: [1, 3, 1],
 *              rotation: [0, 0, 0],
 *              material: new PhongMaterial(viewer.scene, {
 *                  diffuse: [1, 0.3, 0.3]
 *              }),
 *              geometry: boxGeometry
 *          }),
 *
 *          new Mesh(viewer.scene, { // Green table leg
 *              id: "greenLeg",
 *              isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects
 *              position: [4, -6, -4],
 *              scale: [1, 3, 1],
 *              rotation: [0, 0, 0],
 *              material: new PhongMaterial(viewer.scene, {
 *                  diffuse: [0.3, 1.0, 0.3]
 *              }),
 *              geometry: boxGeometry
 *          }),
 *
 *          new Mesh(viewer.scene, {// Blue table leg
 *              id: "blueLeg",
 *              isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects
 *              position: [4, -6, 4],
 *              scale: [1, 3, 1],
 *              rotation: [0, 0, 0],
 *              material: new PhongMaterial(viewer.scene, {
 *                  diffuse: [0.3, 0.3, 1.0]
 *              }),
 *              geometry: boxGeometry
 *          }),
 *
 *          new Mesh(viewer.scene, {  // Yellow table leg
 *              id: "yellowLeg",
 *              isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects
 *              position: [-4, -6, 4],
 *              scale: [1, 3, 1],
 *              rotation: [0, 0, 0],
 *              material: new PhongMaterial(viewer.scene, {
 *                   diffuse: [1.0, 1.0, 0.0]
 *              }),
 *              geometry: boxGeometry
 *          }),
 *
 *          new Mesh(viewer.scene, { // Purple table top
 *              id: "tableTop",
 *              isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects
 *              position: [0, -3, 0],
 *              scale: [6, 0.5, 6],
 *              rotation: [0, 0, 0],
 *              material: new PhongMaterial(viewer.scene, {
 *                  diffuse: [1.0, 0.3, 1.0]
 *              }),
 *              geometry: boxGeometry
 *          })
 *      ]
 *  });
 *
 * // Find Nodes and Meshes by their IDs
 *
 * var table = viewer.scene.models["table"];                // Since table Node has isModel == true
 *
 * var redLeg = viewer.scene.objects["redLeg"];             // Since the Meshes have isObject == true
 * var greenLeg = viewer.scene.objects["greenLeg"];
 * var blueLeg = viewer.scene.objects["blueLeg"];
 *
 * // Highlight one of the table leg Meshes
 *
 * viewer.scene.setObjectsHighlighted(["redLeg"], true);    // Since the Meshes have isObject == true
 *
 * // Periodically update transforms on our Nodes and Meshes
 *
 * viewer.scene.on("tick", function () {
 *
 *       // Rotate legs
 *       redLeg.rotateY(0.5);
 *       greenLeg.rotateY(0.5);
 *       blueLeg.rotateY(0.5);
 *
 *       // Rotate table
 *       table.rotateY(0.5);
 *       table.rotateX(0.3);
 *   });
 * ````
 *
 * ## Metadata
 *
 * As mentioned, we can also associate {@link MetaModel}s and {@link MetaObject}s with our {@link Node}s and Meshes,
 * within a {@link MetaScene}. See {@link MetaScene} for an example.
 *
 * @implements {Entity}
 * @implements {Drawable}
 */
class Mesh extends Component {

    /**
     * @constructor
     * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well.
     * @param {*} [cfg] Configs
     * @param {String} [cfg.id] Optional ID, unique among all components in the parent scene, generated automatically when omitted.
     * @param {String} [cfg.originalSystemId] ID of the corresponding object within the originating system, if any.
     * @param {Boolean} [cfg.isModel] Specify ````true```` if this Mesh represents a model, in which case the Mesh will be registered by {@link Mesh#id} in {@link Scene#models} and may also have a corresponding {@link MetaModel} with matching {@link MetaModel#id}, registered by that ID in {@link MetaScene#metaModels}.
     * @param {Boolean} [cfg.isObject] Specify ````true```` if this Mesh represents an object, in which case the Mesh will be registered by {@link Mesh#id} in {@link Scene#objects} and may also have a corresponding {@link MetaObject} with matching {@link MetaObject#id}, registered by that ID in {@link MetaScene#metaObjects}.
     * @param {Node} [cfg.parent] The parent Node.
     * @param {Number[]} [cfg.origin] World-space origin for this Mesh. When this is given, then ````matrix````, ````position```` and ````geometry```` are all assumed to be relative to this center.
     * @param {Number[]} [cfg.rtcCenter] Deprecated - renamed to ````origin````.
     * @param {Number[]} [cfg.position=[0,0,0]] 3D position of this Mesh, relative to ````origin````.
     * @param {Number[]} [cfg.scale=[1,1,1]] Local scale.
     * @param {Number[]} [cfg.rotation=[0,0,0]] Local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis.
     * @param {Number[]} [cfg.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]] Local modelling transform matrix. Overrides the position, scale and rotation parameters.
     * @param {Number[]} [cfg.offset=[0,0,0]] World-space 3D translation offset. Translates the Mesh in World space, after modelling transforms.
     * @param {Boolean} [cfg.occluder=true] Indicates if the Mesh is able to occlude {@link Marker}s.
     * @param {Boolean} [cfg.visible=true] Indicates if the Mesh is initially visible.
     * @param {Boolean} [cfg.culled=false] Indicates if the Mesh is initially culled from view.
     * @param {Boolean} [cfg.pickable=true] Indicates if the Mesh is initially pickable.
     * @param {Boolean} [cfg.clippable=true] Indicates if the Mesh is initially clippable.
     * @param {Boolean} [cfg.collidable=true] Indicates if the Mesh is initially included in boundary calculations.
     * @param {Boolean} [cfg.castsShadow=true] Indicates if the Mesh initially casts shadows.
     * @param {Boolean} [cfg.receivesShadow=true]  Indicates if the Mesh initially receives shadows.
     * @param {Boolean} [cfg.xrayed=false] Indicates if the Mesh is initially xrayed.
     * @param {Boolean} [cfg.highlighted=false] Indicates if the Mesh is initially highlighted.
     * @param {Boolean} [cfg.selected=false] Indicates if the Mesh is initially selected.
     * @param {Boolean} [cfg.edges=false] Indicates if the Mesh's edges are initially emphasized.
     * @param {Boolean} [cfg.background=false] Indicates if the Mesh should act as background, e.g., it can be used for a skybox.
     * @param {Number[]} [cfg.colorize=[1.0,1.0,1.0]] Mesh's initial RGB colorize color, multiplies by the rendered fragment colors.
     * @param {Number} [cfg.opacity=1.0] Mesh's initial opacity factor, multiplies by the rendered fragment alpha.
     * @param {String} [cfg.billboard="none"] Mesh's billboarding behaviour. Options are "none" for no billboarding, "spherical" to always directly face {@link Camera.eye}, rotating both vertically and horizontally, or "cylindrical" to face the {@link Camera#eye} while rotating only about its vertically axis (use that mode for things like trees on a landscape).
     * @param {Geometry} [cfg.geometry] {@link Geometry} to define the shape of this Mesh. Inherits {@link Scene#geometry} by default.
     * @param {Material} [cfg.material] {@link Material} to define the normal rendered appearance for this Mesh. Inherits {@link Scene#material} by default.
     * @param {EmphasisMaterial} [cfg.xrayMaterial] {@link EmphasisMaterial} to define the xrayed appearance for this Mesh. Inherits {@link Scene#xrayMaterial} by default.
     * @param {EmphasisMaterial} [cfg.highlightMaterial] {@link EmphasisMaterial} to define the xrayed appearance for this Mesh. Inherits {@link Scene#highlightMaterial} by default.
     * @param {EmphasisMaterial} [cfg.selectedMaterial] {@link EmphasisMaterial} to define the selected appearance for this Mesh. Inherits {@link Scene#selectedMaterial} by default.
     * @param {EmphasisMaterial} [cfg.edgeMaterial] {@link EdgeMaterial} to define the appearance of enhanced edges for this Mesh. Inherits {@link Scene#edgeMaterial} by default.
     * @param {Number} [cfg.renderOrder=0] Specifies the rendering order for this mESH. This is used to control the order in which
     * mESHES are drawn when they have transparent objects, to give control over the order in which those objects are blended within the transparent
     * render pass.
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this.renderOrder = cfg.renderOrder || 0;

        /**
         * ID of the corresponding object within the originating system, if any.
         *
         * @type {String}
         * @abstract
         */
        this.originalSystemId = (cfg.originalSystemId || this.id);

        /** @private **/
        this.renderFlags = new RenderFlags();

        this._state = new RenderState({ // NOTE: Renderer gets modeling and normal matrices from Mesh#matrix and Mesh.#normalWorldMatrix
            visible: true,
            culled: false,
            pickable: null,
            clippable: null,
            collidable: null,
            occluder: (cfg.occluder !== false),
            castsShadow: null,
            receivesShadow: null,
            xrayed: false,
            highlighted: false,
            selected: false,
            edges: false,
            stationary: !!cfg.stationary,
            background: !!cfg.background,
            billboard: this._checkBillboard(cfg.billboard),
            layer: null,
            colorize: null,
            pickID: this.scene._renderer.getPickID(this),
            drawHash: "",
            pickHash: "",
            offset: math.vec3(),
            origin: null,
            originHash: null,
            isUI: cfg.isUI
        });

        this._drawRenderer = null;
        this._shadowRenderer = null;
        this._emphasisFillRenderer = null;
        this._emphasisEdgesRenderer = null;
        this._pickMeshRenderer = null;
        this._pickTriangleRenderer = null;
        this._occlusionRenderer = null;

        this._geometry = cfg.geometry ? this._checkComponent2(["ReadableGeometry", "VBOGeometry"], cfg.geometry) : this.scene.geometry;
        this._material = cfg.material ? this._checkComponent2(["PhongMaterial", "MetallicMaterial", "SpecularMaterial", "LambertMaterial"], cfg.material) : this.scene.material;
        this._xrayMaterial = cfg.xrayMaterial ? this._checkComponent("EmphasisMaterial", cfg.xrayMaterial) : this.scene.xrayMaterial;
        this._highlightMaterial = cfg.highlightMaterial ? this._checkComponent("EmphasisMaterial", cfg.highlightMaterial) : this.scene.highlightMaterial;
        this._selectedMaterial = cfg.selectedMaterial ? this._checkComponent("EmphasisMaterial", cfg.selectedMaterial) : this.scene.selectedMaterial;
        this._edgeMaterial = cfg.edgeMaterial ? this._checkComponent("EdgeMaterial", cfg.edgeMaterial) : this.scene.edgeMaterial;

        this._parentNode = null;

        this._aabb = null;
        this._aabbDirty = true;

        this._numTriangles = (this._geometry ? this._geometry.numTriangles : 0);

        this.scene._aabbDirty = true;

        this._scale = math.vec3();
        this._quaternion = math.identityQuaternion();
        this._rotation = math.vec3();
        this._position = math.vec3();

        this._worldMatrix = math.identityMat4();
        this._worldNormalMatrix = math.identityMat4();

        this._localMatrixDirty = true;
        this._worldMatrixDirty = true;
        this._worldNormalMatrixDirty = true;

        const origin = cfg.origin || cfg.rtcCenter;
        if (origin) {
            this._state.origin = math.vec3(origin);
            this._state.originHash = origin.join();
        }

        if (cfg.matrix) {
            this.matrix = cfg.matrix;
        } else {
            this.scale = cfg.scale;
            this.position = cfg.position;
            if (cfg.quaternion) ; else {
                this.rotation = cfg.rotation;
            }
        }

        this._isObject = cfg.isObject;
        if (this._isObject) {
            this.scene._registerObject(this);
        }

        this._isModel = cfg.isModel;
        if (this._isModel) {
            this.scene._registerModel(this);
        }

        this.visible = cfg.visible;
        this.culled = cfg.culled;
        this.pickable = cfg.pickable;
        this.clippable = cfg.clippable;
        this.collidable = cfg.collidable;
        this.castsShadow = cfg.castsShadow;
        this.receivesShadow = cfg.receivesShadow;
        this.xrayed = cfg.xrayed;
        this.highlighted = cfg.highlighted;
        this.selected = cfg.selected;
        this.edges = cfg.edges;
        this.layer = cfg.layer;
        this.colorize = cfg.colorize;
        this.opacity = cfg.opacity;
        this.offset = cfg.offset;

        if (cfg.parentId) {
            const parentNode = this.scene.components[cfg.parentId];
            if (!parentNode) {
                this.error("Parent not found: '" + cfg.parentId + "'");
            } else if (!parentNode.isNode) {
                this.error("Parent is not a Node: '" + cfg.parentId + "'");
            } else {
                parentNode.addChild(this);
            }
            this._parentNode = parentNode;
        } else if (cfg.parent) {
            if (!cfg.parent.isNode) {
                this.error("Parent is not a Node");
            }
            cfg.parent.addChild(this);
            this._parentNode = cfg.parent;
        }

        this.compile();
    }

    /**
     @private
     */
    get type() {
        return "Mesh";
    }

    //------------------------------------------------------------------------------------------------------------------
    // Mesh members
    //------------------------------------------------------------------------------------------------------------------

    /**
     * Returns true to indicate that this Component is a Mesh.
     * @final
     * @type {Boolean}
     */
    get isMesh() {
        return true;
    }

    /**
     * The parent Node.
     *
     * The parent Node may also be set by passing the Mesh to the parent's {@link Node#addChild} method.
     *
     * @type {Node}
     */
    get parent() {
        return this._parentNode;
    }

    /**
     * Defines the shape of this Mesh.
     *
     * Set to {@link Scene#geometry} by default.
     *
     * @type {Geometry}
     */
    get geometry() {
        return this._geometry;
    }

    /**
     * Defines the appearance of this Mesh when rendering normally, ie. when not xrayed, highlighted or selected.
     *
     * Set to {@link Scene#material} by default.
     *
     * @type {Material}
     */
    get material() {
        return this._material;
    }

    /**
     * Gets the Mesh's local translation.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    get position() {
        return this._position;
    }

    /**
     * Sets the Mesh's local translation.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    set position(value) {
        this._position.set(value || [0, 0, 0]);
        this._setLocalMatrixDirty();
        this._setAABBDirty();
        this.glRedraw();
    }

    /**
     * Gets the Mesh's local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    get rotation() {
        return this._rotation;
    }

    /**
     * Sets the Mesh's local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    set rotation(value) {
        this._rotation.set(value || [0, 0, 0]);
        math.eulerToQuaternion(this._rotation, "XYZ", this._quaternion);
        this._setLocalMatrixDirty();
        this._setAABBDirty();
        this.glRedraw();
    }

    /**
     * Gets the Mesh's local rotation quaternion.
     *
     * Default value is ````[0,0,0,1]````.
     *
     * @type {Number[]}
     */
    get quaternion() {
        return this._quaternion;
    }

    /**
     * Sets the Mesh's local rotation quaternion.
     *
     * Default value is ````[0,0,0,1]````.
     *
     * @type {Number[]}
     */
    set quaternion(value) {
        this._quaternion.set(value || [0, 0, 0, 1]);
        math.quaternionToEuler(this._quaternion, "XYZ", this._rotation);
        this._setLocalMatrixDirty();
        this._setAABBDirty();
        this.glRedraw();
    }

    /**
     * Gets the Mesh's local scale.
     *
     * Default value is ````[1,1,1]````.
     *
     * @type {Number[]}
     */
    get scale() {
        return this._scale;
    }

    /**
     * Sets the Mesh's local scale.
     *
     * Default value is ````[1,1,1]````.
     *
     * @type {Number[]}
     */
    set scale(value) {
        this._scale.set(value || [1, 1, 1]);
        this._setLocalMatrixDirty();
        this._setAABBDirty();
        this.glRedraw();
    }

    /**
     * Gets the Mesh's local modeling transform matrix.
     *
     * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````.
     *
     * @type {Number[]}
     */
    get matrix() {
        if (this._localMatrixDirty) {
            if (!this.__localMatrix) {
                this.__localMatrix = math.identityMat4();
            }
            math.composeMat4(this._position, this._quaternion, this._scale, this.__localMatrix);
            this._localMatrixDirty = false;
        }
        return this.__localMatrix;
    }

    /**
     * Sets the Mesh's local modeling transform matrix.
     *
     * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````.
     *
     * @type {Number[]}
     */
    set matrix(value) {
        if (!this.__localMatrix) {
            this.__localMatrix = math.identityMat4();
        }
        this.__localMatrix.set(value || identityMat$1);
        math.decomposeMat4(this.__localMatrix, this._position, this._quaternion, this._scale);
        this._localMatrixDirty = false;
        this._setWorldMatrixDirty();
        this._setAABBDirty();
        this.glRedraw();
    }

    /**
     * Gets the Mesh's World matrix.
     *
     * @property worldMatrix
     * @type {Number[]}
     */
    get worldMatrix() {
        if (this._worldMatrixDirty) {
            this._buildWorldMatrix();
        }
        return this._worldMatrix;
    }

    /**
     * Gets the Mesh's World normal matrix.
     *
     * @type {Number[]}
     */
    get worldNormalMatrix() {
        if (this._worldNormalMatrixDirty) {
            this._buildWorldNormalMatrix();
        }
        return this._worldNormalMatrix;
    }

    /**
     * Returns true to indicate that Mesh implements {@link Entity}.
     *
     * @returns {Boolean}
     */
    get isEntity() {
        return true;
    }

    /**
     * Returns ````true```` if this Mesh represents a model.
     *
     * When this returns ````true````, the Mesh will be registered by {@link Mesh#id} in {@link Scene#models} and
     * may also have a corresponding {@link MetaModel}.
     *
     * @type {Boolean}
     */
    get isModel() {
        return this._isModel;
    }

    /**
     * Returns ````true```` if this Mesh represents an object.
     *
     * When this returns ````true````, the Mesh will be registered by {@link Mesh#id} in {@link Scene#objects} and
     * may also have a corresponding {@link MetaObject}.
     *
     * @type {Boolean}
     */
    get isObject() {
        return this._isObject;
    }

    /**
     * Returns ````true```` if this Mesh is an UI object.
     *
     * @type {Boolean}
     */
    get isUI() {
        return this._state.isUI;
    }

    /**
     * Gets the Mesh's World-space 3D axis-aligned bounding box.
     *
     * Represented by a six-element Float64Array containing the min/max extents of the
     * axis-aligned volume, ie. ````[xmin, ymin,zmin,xmax,ymax, zmax]````.
     *
     * @type {Number[]}
     */
    get aabb() {
        if (this._aabbDirty) {
            this._updateAABB();
        }
        return this._aabb;
    }

    /**
     * Gets the 3D origin of the Mesh's {@link Geometry}'s vertex positions.
     *
     * When this is given, then {@link Mesh#matrix}, {@link Mesh#position} and {@link Mesh#geometry} are all assumed to be relative to this center position.
     *
     * @type {Float64Array}
     */
    get origin() {
        return this._state.origin;
    }

    /**
     * Sets the 3D origin of the Mesh's {@link Geometry}'s vertex positions.
     *
     * When this is given, then {@link Mesh#matrix}, {@link Mesh#position} and {@link Mesh#geometry} are all assumed to be relative to this center position.
     *
     * @type {Float64Array}
     */
    set origin(origin) {
        if (origin) {
            if (!this._state.origin) {
                this._state.origin = math.vec3();
            }
            this._state.origin.set(origin);
            this._state.originHash = origin.join();
            this._setAABBDirty();
            this.scene._aabbDirty = true;
        } else {
            if (this._state.origin) {
                this._state.origin = null;
                this._state.originHash = null;
                this._setAABBDirty();
                this.scene._aabbDirty = true;
            }
        }
    }

    /**
     * Gets the World-space origin for this Mesh.
     *
     * Deprecated and replaced by {@link Mesh#origin}.
     *
     * @deprecated
     * @type {Float64Array}
     */
    get rtcCenter() {
        return this.origin;
    }

    /**
     * Sets the World-space origin for this Mesh.
     *
     * Deprecated and replaced by {@link Mesh#origin}.
     *
     * @deprecated
     * @type {Float64Array}
     */
    set rtcCenter(rtcCenter) {
        this.origin = rtcCenter;
    }

    /**
     * The approximate number of triangles in this Mesh.
     *
     * @type {Number}
     */
    get numTriangles() {
        return this._numTriangles;
    }

    /**
     * Gets if this Mesh is visible.
     *
     * Only rendered when {@link Mesh#visible} is ````true```` and {@link Mesh#culled} is ````false````.
     *
     * When {@link Mesh#isObject} and {@link Mesh#visible} are both ````true```` the Mesh will be
     * registered by {@link Mesh#id} in {@link Scene#visibleObjects}.
     *
     * @type {Boolean}
     */
    get visible() {
        return this._state.visible;
    }

    /**
     * Sets if this Mesh is visible.
     *
     * Only rendered when {@link Mesh#visible} is ````true```` and {@link Mesh#culled} is ````false````.
     *
     * When {@link Mesh#isObject} and {@link Mesh#visible} are both ````true```` the Mesh will be
     * registered by {@link Mesh#id} in {@link Scene#visibleObjects}.
     *
     * @type {Boolean}
     */
    set visible(visible) {
        visible = visible !== false;
        this._state.visible = visible;
        if (this._isObject) {
            this.scene._objectVisibilityUpdated(this, visible);
        }
        this.glRedraw();
    }

    /**
     * Gets if this Mesh is xrayed.
     *
     * XRayed appearance is configured by the {@link EmphasisMaterial} referenced by {@link Mesh#xrayMaterial}.
     *
     * When {@link Mesh#isObject} and {@link Mesh#xrayed} are both ````true``` the Mesh will be
     * registered by {@link Mesh#id} in {@link Scene#xrayedObjects}.
     *
     * @type {Boolean}
     */
    get xrayed() {
        return this._state.xrayed;
    }

    /**
     * Sets if this Mesh is xrayed.
     *
     * XRayed appearance is configured by the {@link EmphasisMaterial} referenced by {@link Mesh#xrayMaterial}.
     *
     * When {@link Mesh#isObject} and {@link Mesh#xrayed} are both ````true``` the Mesh will be
     * registered by {@link Mesh#id} in {@link Scene#xrayedObjects}.
     *
     * @type {Boolean}
     */
    set xrayed(xrayed) {
        xrayed = !!xrayed;
        if (this._state.xrayed === xrayed) {
            return;
        }
        this._state.xrayed = xrayed;
        if (this._isObject) {
            this.scene._objectXRayedUpdated(this, xrayed);
        }
        this.glRedraw();
    }

    /**
     * Gets if this Mesh is highlighted.
     *
     * Highlighted appearance is configured by the {@link EmphasisMaterial} referenced by {@link Mesh#highlightMaterial}.
     *
     * When {@link Mesh#isObject} and {@link Mesh#highlighted} are both ````true```` the Mesh will be
     * registered by {@link Mesh#id} in {@link Scene#highlightedObjects}.
     *
     * @type {Boolean}
     */
    get highlighted() {
        return this._state.highlighted;
    }

    /**
     * Sets if this Mesh is highlighted.
     *
     * Highlighted appearance is configured by the {@link EmphasisMaterial} referenced by {@link Mesh#highlightMaterial}.
     *
     * When {@link Mesh#isObject} and {@link Mesh#highlighted} are both ````true```` the Mesh will be
     * registered by {@link Mesh#id} in {@link Scene#highlightedObjects}.
     *
     * @type {Boolean}
     */
    set highlighted(highlighted) {
        highlighted = !!highlighted;
        if (highlighted === this._state.highlighted) {
            return;
        }
        this._state.highlighted = highlighted;
        if (this._isObject) {
            this.scene._objectHighlightedUpdated(this, highlighted);
        }
        this.glRedraw();
    }

    /**
     * Gets if this Mesh is selected.
     *
     * Selected appearance is configured by the {@link EmphasisMaterial} referenced by {@link Mesh#selectedMaterial}.
     *
     * When {@link Mesh#isObject} and {@link Mesh#selected} are both ````true``` the Mesh will be
     * registered by {@link Mesh#id} in {@link Scene#selectedObjects}.
     *
     * @type {Boolean}
     */
    get selected() {
        return this._state.selected;
    }

    /**
     * Sets if this Mesh is selected.
     *
     * Selected appearance is configured by the {@link EmphasisMaterial} referenced by {@link Mesh#selectedMaterial}.
     *
     * When {@link Mesh#isObject} and {@link Mesh#selected} are both ````true``` the Mesh will be
     * registered by {@link Mesh#id} in {@link Scene#selectedObjects}.
     *
     * @type {Boolean}
     */
    set selected(selected) {
        selected = !!selected;
        if (selected === this._state.selected) {
            return;
        }
        this._state.selected = selected;
        if (this._isObject) {
            this.scene._objectSelectedUpdated(this, selected);
        }
        this.glRedraw();
    }

    /**
     * Gets if this Mesh is edge-enhanced.
     *
     * Edge appearance is configured by the {@link EdgeMaterial} referenced by {@link Mesh#edgeMaterial}.
     *
     * @type {Boolean}
     */
    get edges() {
        return this._state.edges;
    }

    /**
     * Sets if this Mesh is edge-enhanced.
     *
     * Edge appearance is configured by the {@link EdgeMaterial} referenced by {@link Mesh#edgeMaterial}.
     *
     * @type {Boolean}
     */
    set edges(edges) {
        edges = !!edges;
        if (edges === this._state.edges) {
            return;
        }
        this._state.edges = edges;
        this.glRedraw();
    }

    /**
     * Gets if this Mesh is culled.
     *
     * Only rendered when {@link Mesh#visible} is ````true```` and {@link Mesh#culled} is ````false````.
     *
     * @type {Boolean}
     */
    get culled() {
        return this._state.culled;
    }

    /**
     * Sets if this Mesh is culled.
     *
     * Only rendered when {@link Mesh#visible} is ````true```` and {@link Mesh#culled} is ````false````.
     *
     * @type {Boolean}
     */
    set culled(value) {
        this._state.culled = !!value;
        this.glRedraw();
    }

    /**
     * Gets if this Mesh is clippable.
     *
     * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}.
     *
     * @type {Boolean}
     */
    get clippable() {
        return this._state.clippable;
    }

    /**
     * Sets if this Mesh is clippable.
     *
     * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}.
     *
     * @type {Boolean}
     */
    set clippable(value) {
        value = value !== false;
        if (this._state.clippable === value) {
            return;
        }
        this._state.clippable = value;
        this.glRedraw();
    }

    /**
     * Gets if this Mesh included in boundary calculations.
     *
     * @type {Boolean}
     */
    get collidable() {
        return this._state.collidable;
    }

    /**
     * Sets if this Mesh included in boundary calculations.
     *
     * @type {Boolean}
     */
    set collidable(value) {
        value = value !== false;
        if (value === this._state.collidable) {
            return;
        }
        this._state.collidable = value;
        this._setAABBDirty();
        this.scene._aabbDirty = true;

    }

    //------------------------------------------------------------------------------------------------------------------
    // Entity members
    //------------------------------------------------------------------------------------------------------------------

    /**
     * Gets if this Mesh is pickable.
     *
     * Picking is done via calls to {@link Scene#pick}.
     *
     * @type {Boolean}
     */
    get pickable() {
        return this._state.pickable;
    }

    /**
     * Sets if this Mesh is pickable.
     *
     * Picking is done via calls to {@link Scene#pick}.
     *
     * @type {Boolean}
     */
    set pickable(value) {
        value = value !== false;
        if (this._state.pickable === value) {
            return;
        }
        this._state.pickable = value;
        // No need to trigger a render;
        // state is only used when picking
    }

    /**
     * Gets if this Mesh casts shadows.
     *
     * @type {Boolean}
     */
    get castsShadow() {
        return this._state.castsShadow;
    }

    /**
     * Sets if this Mesh casts shadows.
     *
     * @type {Boolean}
     */
    set castsShadow(value) {
        value = value !== false;
        if (value === this._state.castsShadow) {
            return;
        }
        this._state.castsShadow = value;
        this.glRedraw();
    }

    /**
     * Gets if this Mesh can have shadows cast upon it.
     *
     * @type {Boolean}
     */
    get receivesShadow() {
        return this._state.receivesShadow;
    }

    /**
     * Sets if this Mesh can have shadows cast upon it.
     *
     * @type {Boolean}
     */
    set receivesShadow(value) {
        value = value !== false;
        if (value === this._state.receivesShadow) {
            return;
        }
        this._state.receivesShadow = value;
        this._state.hash = value ? "/mod/rs;" : "/mod;";
        this.fire("dirty", this); // Now need to (re)compile objectRenderers to include/exclude shadow mapping
    }

    /**
     * Gets if this Mesh can have Scalable Ambient Obscurance (SAO) applied to it.
     *
     * SAO is configured by {@link SAO}.
     *
     * @type {Boolean}
     * @abstract
     */
    get saoEnabled() {
        return false; // TODO: Support SAO on Meshes
    }

    /**
     * Gets the RGB colorize color for this Mesh.
     *
     * Multiplies by rendered fragment colors.
     *
     * Each element of the color is in range ````[0..1]````.
     *
     * @type {Number[]}
     */
    get colorize() {
        return this._state.colorize;
    }

    /**
     * Sets the RGB colorize color for this Mesh.
     *
     * Multiplies by rendered fragment colors.
     *
     * Each element of the color is in range ````[0..1]````.
     *
     * @type {Number[]}
     */
    set colorize(value) {
        let colorize = this._state.colorize;
        if (!colorize) {
            colorize = this._state.colorize = new Float32Array(4);
            colorize[3] = 1;
        }
        if (value) {
            colorize[0] = value[0];
            colorize[1] = value[1];
            colorize[2] = value[2];
        } else {
            colorize[0] = 1;
            colorize[1] = 1;
            colorize[2] = 1;
        }
        const colorized = (!!value);
        this.scene._objectColorizeUpdated(this, colorized);
        this.glRedraw();
    }

    /**
     * Gets the opacity factor for this Mesh.
     *
     * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas.
     *
     * @type {Number}
     */
    get opacity() {
        return this._state.colorize[3];
    }

    /**
     * Sets the opacity factor for this Mesh.
     *
     * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas.
     *
     * @type {Number}
     */
    set opacity(opacity) {
        let colorize = this._state.colorize;
        if (!colorize) {
            colorize = this._state.colorize = new Float32Array(4);
            colorize[0] = 1;
            colorize[1] = 1;
            colorize[2] = 1;
        }
        const opacityUpdated = (opacity !== null && opacity !== undefined);
        colorize[3] = opacityUpdated ? opacity : 1.0;
        this.scene._objectOpacityUpdated(this, opacityUpdated);
        this.glRedraw();
    }

    /**
     * Gets if this Mesh is transparent.
     * @returns {Boolean}
     */
    get transparent() {
        return this._material.alphaMode === 2 /* blend */ || this._state.colorize[3] < 1
    }

    /**
     * Gets the Mesh's rendering order relative to other Meshes.
     *
     * Default value is ````0````.
     *
     * This can be set on multiple transparent Meshes, to make them render in a specific order for correct alpha blending.
     *
     * @type {Number}
     */
    get layer() {
        return this._state.layer;
    }

    /**
     * Sets the Mesh's rendering order relative to other Meshes.
     *
     * Default value is ````0````.
     *
     * This can be set on multiple transparent Meshes, to make them render in a specific order for correct alpha blending.
     *
     * @type {Number}
     */
    set layer(value) {
        // TODO: Only accept rendering layer in range [0...MAX_layer]
        value = value || 0;
        value = Math.round(value);
        if (value === this._state.layer) {
            return;
        }
        this._state.layer = value;
        this._renderer.needStateSort();
    }

    /**
     * Gets if the Node's position is stationary.
     *
     * When true, will disable the effect of {@link Camera} translations for this Mesh, while still allowing it to rotate. This is useful for skyboxes.
     *
     * @type {Boolean}
     */
    get stationary() {
        return this._state.stationary;
    }

    /**
     * Gets the Node's billboarding behaviour.
     *
     * Options are:
     * * ````"none"```` -  (default) - No billboarding.
     * * ````"spherical"```` - Mesh is billboarded to face the viewpoint, rotating both vertically and horizontally.
     * * ````"cylindrical"```` - Mesh is billboarded to face the viewpoint, rotating only about its vertically axis. Use this mode for things like trees on a landscape.
     * @type {String}
     */
    get billboard() {
        return this._state.billboard;
    }

    /**
     * Gets the Mesh's 3D World-space offset.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    get offset() {
        return this._state.offset;
    }

    /**
     * Sets the Mesh's 3D World-space offset.
     *
     * The offset dynamically translates the Mesh in World-space.
     *
     * Default value is ````[0, 0, 0]````.
     *
     * Provide a null or undefined value to reset to the default value.
     *
     * @type {Number[]}
     */
    set offset(value) {
        this._state.offset.set(value || [0, 0, 0]);
        this._setAABBDirty();
        this.glRedraw();
    }

    /**
     * Returns true to indicate that Mesh implements {@link Drawable}.
     * @final
     * @type {Boolean}
     */
    get isDrawable() {
        return true;
    }

    /**
     * Property with final value ````true```` to indicate that xeokit should render this Mesh in sorted order, relative to other Meshes.
     *
     * The sort order is determined by {@link Mesh#stateSortCompare}.
     *
     * Sorting is essential for rendering performance, so that xeokit is able to avoid applying runs of the same state changes to the GPU, ie. can collapse them.
     *
     * @type {Boolean}
     */
    get isStateSortable() {
        return true;
    }

    /**
     * Defines the appearance of this Mesh when xrayed.
     *
     * Mesh is xrayed when {@link Mesh#xrayed} is ````true````.
     *
     * Set to {@link Scene#xrayMaterial} by default.
     *
     * @type {EmphasisMaterial}
     */
    get xrayMaterial() {
        return this._xrayMaterial;
    }

    /**
     * Defines the appearance of this Mesh when highlighted.
     *
     * Mesh is xrayed when {@link Mesh#highlighted} is ````true````.
     *
     * Set to {@link Scene#highlightMaterial} by default.
     *
     * @type {EmphasisMaterial}
     */
    get highlightMaterial() {
        return this._highlightMaterial;
    }

    /**
     * Defines the appearance of this Mesh when selected.
     *
     * Mesh is xrayed when {@link Mesh#selected} is ````true````.
     *
     * Set to {@link Scene#selectedMaterial} by default.
     *
     * @type {EmphasisMaterial}
     */
    get selectedMaterial() {
        return this._selectedMaterial;
    }

    /**
     * Defines the appearance of this Mesh when edges are enhanced.
     *
     * Mesh is xrayed when {@link Mesh#edges} is ````true````.
     *
     * Set to {@link Scene#edgeMaterial} by default.
     *
     * @type {EdgeMaterial}
     */
    get edgeMaterial() {
        return this._edgeMaterial;
    }

    _checkBillboard(value) {
        value = value || "none";
        if (value !== "spherical" && value !== "cylindrical" && value !== "none") {
            this.error("Unsupported value for 'billboard': " + value + " - accepted values are " +
                "'spherical', 'cylindrical' and 'none' - defaulting to 'none'.");
            value = "none";
        }
        return value;
    }

    /**
     * Called by xeokit to compile shaders for this Mesh.
     * @private
     */
    compile() {
        const drawHash = this._makeDrawHash();
        if (this._state.drawHash !== drawHash) {
            this._state.drawHash = drawHash;
            this._putDrawRenderers();
            this._drawRenderer = DrawRenderer.get(this);
            // this._shadowRenderer = ShadowRenderer.get(this);
            this._emphasisFillRenderer = EmphasisFillRenderer.get(this);
            this._emphasisEdgesRenderer = EmphasisEdgesRenderer.get(this);
        }
        const pickHash = this._makePickHash();
        if (this._state.pickHash !== pickHash) {
            this._state.pickHash = pickHash;
            this._putPickRenderers();
            this._pickMeshRenderer = PickMeshRenderer.get(this);
        }
        if (this._state.occluder) {
            const occlusionHash = this._makeOcclusionHash();
            if (this._state.occlusionHash !== occlusionHash) {
                this._state.occlusionHash = occlusionHash;
                this._putOcclusionRenderer();
                this._occlusionRenderer = OcclusionRenderer.get(this);
            }
        }
    }

    _setLocalMatrixDirty() {
        this._localMatrixDirty = true;
        this._setWorldMatrixDirty();
    }

    _setWorldMatrixDirty() {
        this._worldMatrixDirty = true;
        this._worldNormalMatrixDirty = true;
    }

    _buildWorldMatrix() {
        const localMatrix = this.matrix;
        if (!this._parentNode) {
            for (let i = 0, len = localMatrix.length; i < len; i++) {
                this._worldMatrix[i] = localMatrix[i];
            }
        } else {
            math.mulMat4(this._parentNode.worldMatrix, localMatrix, this._worldMatrix);
        }
        this._worldMatrixDirty = false;
    }

    _buildWorldNormalMatrix() {
        if (this._worldMatrixDirty) {
            this._buildWorldMatrix();
        }
        if (!this._worldNormalMatrix) {
            this._worldNormalMatrix = math.mat4();
        }
        // Note: order of inverse and transpose doesn't matter
        math.transposeMat4(this._worldMatrix, this._worldNormalMatrix);
        math.inverseMat4(this._worldNormalMatrix);
        this._worldNormalMatrixDirty = false;
    }

    _setAABBDirty() {
        if (this.collidable) {
            for (let node = this; node; node = node._parentNode) {
                node._aabbDirty = true;
            }
        }
    }

    _updateAABB() {
        this.scene._aabbDirty = true;
        if (!this._aabb) {
            this._aabb = math.AABB3();
        }
        this._buildAABB(this.worldMatrix, this._aabb); // Mesh or VBOSceneModel
        this._aabbDirty = false;
    }

    _webglContextRestored() {
        if (this._drawRenderer) {
            this._drawRenderer.webglContextRestored();
        }
        if (this._shadowRenderer) {
            this._shadowRenderer.webglContextRestored();
        }
        if (this._emphasisFillRenderer) {
            this._emphasisFillRenderer.webglContextRestored();
        }
        if (this._emphasisEdgesRenderer) {
            this._emphasisEdgesRenderer.webglContextRestored();
        }
        if (this._pickMeshRenderer) {
            this._pickMeshRenderer.webglContextRestored();
        }
        if (this._pickTriangleRenderer) {
            this._pickMeshRenderer.webglContextRestored();
        }
        if (this._occlusionRenderer) {
            this._occlusionRenderer.webglContextRestored();
        }
    }

    _makeDrawHash() {
        const scene = this.scene;
        const hash = [
            scene.canvas.canvas.id,
            (scene.gammaInput ? "gi;" : ";") + (scene.gammaOutput ? "go" : ""),
            scene._lightsState.getHash(),
            scene._sectionPlanesState.getHash()
        ];
        const state = this._state;
        if (state.stationary) {
            hash.push("/s");
        }
        if (state.billboard === "none") {
            hash.push("/n");
        } else if (state.billboard === "spherical") {
            hash.push("/s");
        } else if (state.billboard === "cylindrical") {
            hash.push("/c");
        }
        if (state.receivesShadow) {
            hash.push("/rs");
        }
        hash.push(";");
        return hash.join("");
    }

    _makePickHash() {
        const scene = this.scene;
        const hash = [
            scene.canvas.canvas.id,
            scene._sectionPlanesState.getHash()
        ];
        const state = this._state;
        if (state.stationary) {
            hash.push("/s");
        }
        if (state.billboard === "none") {
            hash.push("/n");
        } else if (state.billboard === "spherical") {
            hash.push("/s");
        } else if (state.billboard === "cylindrical") {
            hash.push("/c");
        }
        hash.push(";");
        return hash.join("");
    }

    _makeOcclusionHash() {
        const scene = this.scene;
        const hash = [
            scene.canvas.canvas.id,
            scene._sectionPlanesState.getHash()
        ];
        const state = this._state;
        if (state.stationary) {
            hash.push("/s");
        }
        if (state.billboard === "none") {
            hash.push("/n");
        } else if (state.billboard === "spherical") {
            hash.push("/s");
        } else if (state.billboard === "cylindrical") {
            hash.push("/c");
        }
        hash.push(";");
        return hash.join("");
    }

    _buildAABB(worldMatrix, aabb) {

        math.transformOBB3(worldMatrix, this._geometry.obb, obb);
        math.OBB3ToAABB3(obb, aabb);

        const offset = this._state.offset;

        aabb[0] += offset[0];
        aabb[1] += offset[1];
        aabb[2] += offset[2];
        aabb[3] += offset[0];
        aabb[4] += offset[1];
        aabb[5] += offset[2];

        if (this._state.origin) {
            const origin = this._state.origin;
            aabb[0] += origin[0];
            aabb[1] += origin[1];
            aabb[2] += origin[2];
            aabb[3] += origin[0];
            aabb[4] += origin[1];
            aabb[5] += origin[2];
        }
    }

    /**
     * Rotates the Mesh about the given local axis by the given increment.
     *
     * @param {Number[]} axis Local axis about which to rotate.
     * @param {Number} angle Angle increment in degrees.
     */
    rotate(axis, angle) {
        angleAxis$1[0] = axis[0];
        angleAxis$1[1] = axis[1];
        angleAxis$1[2] = axis[2];
        angleAxis$1[3] = angle * math.DEGTORAD;
        math.angleAxisToQuaternion(angleAxis$1, q1$1);
        math.mulQuaternions(this.quaternion, q1$1, q2$1);
        this.quaternion = q2$1;
        this._setLocalMatrixDirty();
        this._setAABBDirty();
        this.glRedraw();
        return this;
    }

    /**
     * Rotates the Mesh about the given World-space axis by the given increment.
     *
     * @param {Number[]} axis Local axis about which to rotate.
     * @param {Number} angle Angle increment in degrees.
     */
    rotateOnWorldAxis(axis, angle) {
        angleAxis$1[0] = axis[0];
        angleAxis$1[1] = axis[1];
        angleAxis$1[2] = axis[2];
        angleAxis$1[3] = angle * math.DEGTORAD;
        math.angleAxisToQuaternion(angleAxis$1, q1$1);
        math.mulQuaternions(q1$1, this.quaternion, q1$1);
        //this.quaternion.premultiply(q1);
        return this;
    }

    /**
     * Rotates the Mesh about the local X-axis by the given increment.
     *
     * @param {Number} angle Angle increment in degrees.
     */
    rotateX(angle) {
        return this.rotate(xAxis$1, angle);
    }

    /**
     * Rotates the Mesh about the local Y-axis by the given increment.
     *
     * @param {Number} angle Angle increment in degrees.
     */
    rotateY(angle) {
        return this.rotate(yAxis$1, angle);
    }

    /**
     * Rotates the Mesh about the local Z-axis by the given increment.
     *
     * @param {Number} angle Angle increment in degrees.
     */
    rotateZ(angle) {
        return this.rotate(zAxis$1, angle);
    }

    /**
     * Translates the Mesh along local space vector by the given increment.
     *
     * @param {Number[]} axis Normalized local space 3D vector along which to translate.
     * @param {Number} distance Distance to translate along  the vector.
     */
    translate(axis, distance) {
        math.vec3ApplyQuaternion(this.quaternion, axis, veca);
        math.mulVec3Scalar(veca, distance, vecb);
        math.addVec3(this.position, vecb, this.position);
        this._setLocalMatrixDirty();
        this._setAABBDirty();
        this.glRedraw();
        return this;
    }

    //------------------------------------------------------------------------------------------------------------------
    // Drawable members
    //------------------------------------------------------------------------------------------------------------------

    /**
     * Translates the Mesh along the local X-axis by the given increment.
     *
     * @param {Number} distance Distance to translate along  the X-axis.
     */
    translateX(distance) {
        return this.translate(xAxis$1, distance);
    }

    /**
     * Translates the Mesh along the local Y-axis by the given increment.
     *
     * @param {Number} distance Distance to translate along  the Y-axis.
     */
    translateY(distance) {
        return this.translate(yAxis$1, distance);
    }

    /**
     * Translates the Mesh along the local Z-axis by the given increment.
     *
     * @param {Number} distance Distance to translate along  the Z-axis.
     */
    translateZ(distance) {
        return this.translate(zAxis$1, distance);
    }

    _putDrawRenderers() {
        if (this._drawRenderer) {
            this._drawRenderer.put();
            this._drawRenderer = null;
        }
        if (this._shadowRenderer) {
            this._shadowRenderer.put();
            this._shadowRenderer = null;
        }
        if (this._emphasisFillRenderer) {
            this._emphasisFillRenderer.put();
            this._emphasisFillRenderer = null;
        }
        if (this._emphasisEdgesRenderer) {
            this._emphasisEdgesRenderer.put();
            this._emphasisEdgesRenderer = null;
        }
    }

    _putPickRenderers() {
        if (this._pickMeshRenderer) {
            this._pickMeshRenderer.put();
            this._pickMeshRenderer = null;
        }
        if (this._pickTriangleRenderer) {
            this._pickTriangleRenderer.put();
            this._pickTriangleRenderer = null;
        }
    }

    _putOcclusionRenderer() {
        if (this._occlusionRenderer) {
            this._occlusionRenderer.put();
            this._occlusionRenderer = null;
        }
    }

    /**
     * Comparison function used by the renderer to determine the order in which xeokit should render the Mesh, relative to to other Meshes.
     *
     * xeokit requires this method because Mesh implements {@link Drawable}.
     *
     * Sorting is essential for rendering performance, so that xeokit is able to avoid needlessly applying runs of the same rendering state changes to the GPU, ie. can collapse them.
     *
     * @param {Mesh} mesh1
     * @param {Mesh} mesh2
     * @returns {number}
     */
    stateSortCompare(mesh1, mesh2) {
        return (mesh1._state.layer - mesh2._state.layer)
            || (mesh1._drawRenderer.id - mesh2._drawRenderer.id) // Program state
            || (mesh1._material._state.id - mesh2._material._state.id) // Material state
            || (mesh1._geometry._state.id - mesh2._geometry._state.id); // Geometry state
    }

    /** @private */
    rebuildRenderFlags() {
        this.renderFlags.reset();
        if (!this._getActiveSectionPlanes()) {
            this.renderFlags.culled = true;
            return;
        }
        this.renderFlags.numLayers = 1;
        this.renderFlags.numVisibleLayers = 1;
        this.renderFlags.visibleLayers[0] = 0;
        this._updateRenderFlags();
    }

    /**
     * @private
     */
    _updateRenderFlags() {

        const renderFlags = this.renderFlags;
        const state = this._state;

        if (state.xrayed) {
            const xrayMaterial = this._xrayMaterial._state;
            if (xrayMaterial.fill) {
                if (xrayMaterial.fillAlpha < 1.0) {
                    renderFlags.xrayedSilhouetteTransparent = true;
                } else {
                    renderFlags.xrayedSilhouetteOpaque = true;
                }
            }
            if (xrayMaterial.edges) {
                if (xrayMaterial.edgeAlpha < 1.0) {
                    renderFlags.xrayedEdgesTransparent = true;
                } else {
                    renderFlags.xrayedEdgesOpaque = true;
                }
            }
        } else {
            const normalMaterial = this._material._state;
            if (normalMaterial.alpha < 1.0 || state.colorize[3] < 1.0) {
                renderFlags.colorTransparent = true;
            } else {
                renderFlags.colorOpaque = true;
            }
            if (state.edges) {
                const edgeMaterial = this._edgeMaterial._state;
                if (edgeMaterial.alpha < 1.0) {
                    renderFlags.edgesTransparent = true;
                } else {
                    renderFlags.edgesOpaque = true;
                }
            }
            if (state.selected) {
                const selectedMaterial = this._selectedMaterial._state;
                if (selectedMaterial.fill) {
                    if (selectedMaterial.fillAlpha < 1.0) {
                        renderFlags.selectedSilhouetteTransparent = true;
                    } else {
                        renderFlags.selectedSilhouetteOpaque = true;
                    }
                }
                if (selectedMaterial.edges) {
                    if (selectedMaterial.edgeAlpha < 1.0) {
                        renderFlags.selectedEdgesTransparent = true;
                    } else {
                        renderFlags.selectedEdgesOpaque = true;
                    }
                }
            } else if (state.highlighted) {
                const highlightMaterial = this._highlightMaterial._state;
                if (highlightMaterial.fill) {
                    if (highlightMaterial.fillAlpha < 1.0) {
                        renderFlags.highlightedSilhouetteTransparent = true;
                    } else {
                        renderFlags.highlightedSilhouetteOpaque = true;
                    }
                }
                if (highlightMaterial.edges) {
                    if (highlightMaterial.edgeAlpha < 1.0) {
                        renderFlags.highlightedEdgesTransparent = true;
                    } else {
                        renderFlags.highlightedEdgesOpaque = true;
                    }
                }
            }
        }
    }

    _getActiveSectionPlanes() {

        if (this._state.clippable) {

            const sectionPlanes = this.scene._sectionPlanesState.sectionPlanes;
            const numSectionPlanes = sectionPlanes.length;

            if (numSectionPlanes > 0) {
                for (let i = 0; i < numSectionPlanes; i++) {

                    const sectionPlane = sectionPlanes[i];
                    const renderFlags = this.renderFlags;

                    if (!sectionPlane.active) {
                        renderFlags.sectionPlanesActivePerLayer[i] = false;

                    } else {

                        if (this._state.origin) {

                            const intersect = math.planeAABB3Intersect(sectionPlane.dir, sectionPlane.dist, this.aabb);
                            const outside = (intersect === -1);

                            if (outside) {
                                return false;
                            }

                            const intersecting = (intersect === 0);
                            renderFlags.sectionPlanesActivePerLayer[i] = intersecting;

                        } else {
                            renderFlags.sectionPlanesActivePerLayer[i] = true;
                        }
                    }
                }
            }
        }

        return true;
    }

    // ---------------------- NORMAL RENDERING -----------------------------------

    /** @private  */
    drawColorOpaque(frameCtx) {
        if (this._drawRenderer || (this._drawRenderer = DrawRenderer.get(this))) {
            this._drawRenderer.drawMesh(frameCtx, this);
        }
    }

    /** @private  */
    drawColorTransparent(frameCtx) {
        if (this._drawRenderer || (this._drawRenderer = DrawRenderer.get(this))) {
            this._drawRenderer.drawMesh(frameCtx, this);
        }
    }

    // ---------------------- RENDERING SAO POST EFFECT TARGETS --------------

    // TODO

    // ---------------------- EMPHASIS RENDERING -----------------------------------

    /** @private  */
    drawSilhouetteXRayed(frameCtx) {
        if (this._emphasisFillRenderer || (this._emphasisFillRenderer = EmphasisFillRenderer.get(this))) {
            this._emphasisFillRenderer.drawMesh(frameCtx, this, 0); // 0 == xray
        }
    }

    /** @private  */
    drawSilhouetteHighlighted(frameCtx) {
        if (this._emphasisFillRenderer || (this._emphasisFillRenderer = EmphasisFillRenderer.get(this))) {
            this._emphasisFillRenderer.drawMesh(frameCtx, this, 1); // 1 == highlight
        }
    }

    /** @private  */
    drawSilhouetteSelected(frameCtx) {
        if (this._emphasisFillRenderer || (this._emphasisFillRenderer = EmphasisFillRenderer.get(this))) {
            this._emphasisFillRenderer.drawMesh(frameCtx, this, 2); // 2 == selected
        }
    }

    // ---------------------- EDGES RENDERING -----------------------------------

    /** @private  */
    drawEdgesColorOpaque(frameCtx) {
        if (this._emphasisEdgesRenderer || (this._emphasisEdgesRenderer = EmphasisEdgesRenderer.get(this))) {
            this._emphasisEdgesRenderer.drawMesh(frameCtx, this, 3); // 3 == edges
        }
    }

    /** @private  */
    drawEdgesColorTransparent(frameCtx) {
        if (this._emphasisEdgesRenderer || (this._emphasisEdgesRenderer = EmphasisEdgesRenderer.get(this))) {
            this._emphasisEdgesRenderer.drawMesh(frameCtx, this, 3); // 3 == edges
        }
    }

    /** @private  */
    drawEdgesXRayed(frameCtx) {
        if (this._emphasisEdgesRenderer || (this._emphasisEdgesRenderer = EmphasisEdgesRenderer.get(this))) {
            this._emphasisEdgesRenderer.drawMesh(frameCtx, this, 0); // 0 == xray
        }
    }

    /** @private  */
    drawEdgesHighlighted(frameCtx) {
        if (this._emphasisEdgesRenderer || (this._emphasisEdgesRenderer = EmphasisEdgesRenderer.get(this))) {
            this._emphasisEdgesRenderer.drawMesh(frameCtx, this, 1); // 1 == highlight
        }
    }

    /** @private  */
    drawEdgesSelected(frameCtx) {
        if (this._emphasisEdgesRenderer || (this._emphasisEdgesRenderer = EmphasisEdgesRenderer.get(this))) {
            this._emphasisEdgesRenderer.drawMesh(frameCtx, this, 2); // 2 == selected
        }
    }

    // ---------------------- OCCLUSION CULL RENDERING -----------------------------------

    /** @private  */
    drawOcclusion(frameCtx) {
        if (this._state.occluder && this._occlusionRenderer || (this._occlusionRenderer = OcclusionRenderer.get(this))) {
            this._occlusionRenderer.drawMesh(frameCtx, this);
        }
    }

    // ---------------------- SHADOW BUFFER RENDERING -----------------------------------

    /** @private  */
    drawShadow(frameCtx) {
        if (this._shadowRenderer || (this._shadowRenderer = ShadowRenderer.get(this))) {
            this._shadowRenderer.drawMesh(frameCtx, this);
        }
    }

    // ---------------------- PICKING RENDERING ----------------------------------

    /** @private  */
    drawPickMesh(frameCtx) {
        if (this._pickMeshRenderer || (this._pickMeshRenderer = PickMeshRenderer.get(this))) {
            this._pickMeshRenderer.drawMesh(frameCtx, this);
        }
    }

    /** @private
     */
    canPickTriangle() {
        return this._geometry.isReadableGeometry; // VBOGeometry does not support surface picking because it has no geometry data in browser memory
    }

    /** @private  */
    drawPickTriangles(frameCtx) {
        if (this._pickTriangleRenderer || (this._pickTriangleRenderer = PickTriangleRenderer.get(this))) {
            this._pickTriangleRenderer.drawMesh(frameCtx, this);
        }
    }

    /** @private */
    pickTriangleSurface(pickViewMatrix, pickProjMatrix, projection, pickResult) {
        pickTriangleSurface(this, pickViewMatrix, pickProjMatrix, projection, pickResult);
    }

    /** @private  */
    drawPickVertices(frameCtx) {

    }

    /**
     * @private
     * @returns {PerformanceNode}
     */
    delegatePickedEntity() {
        return this;
    }

    //------------------------------------------------------------------------------------------------------------------
    // Component members
    //------------------------------------------------------------------------------------------------------------------

    /**
     * Destroys this Mesh.
     */
    destroy() {
        super.destroy(); // xeokit.Object
        this._putDrawRenderers();
        this._putPickRenderers();
        this._putOcclusionRenderer();
        this.scene._renderer.putPickID(this._state.pickID); // TODO: somehow puch this down into xeokit framework?
        if (this._isObject) {
            this.scene._deregisterObject(this);
            if (this._visible) {
                this.scene._objectVisibilityUpdated(this, false, false);
            }
            if (this._xrayed) {
                this.scene._objectXRayedUpdated(this, false, false);
            }
            if (this._selected) {
                this.scene._objectSelectedUpdated(this, false, false);
            }
            if (this._highlighted) {
                this.scene._objectHighlightedUpdated(this, false, false);
            }
            this.scene._objectColorizeUpdated(this, false);
            this.scene._objectOpacityUpdated(this, false);
            if (this.offset.some((v) => v !== 0))
                this.scene._objectOffsetUpdated(this, false);
        }
        if (this._isModel) {
            this.scene._deregisterModel(this);
        }
        this.glRedraw();
    }

}


const pickTriangleSurface = (function () {

    // Cached vars to avoid garbage collection

    const localRayOrigin = math.vec3();
    const localRayDir = math.vec3();
    const positionA = math.vec3();
    const positionB = math.vec3();
    const positionC = math.vec3();
    const triangleVertices = math.vec3();
    const position = math.vec4();
    const worldPos = math.vec3();
    const viewPos = math.vec3();
    const bary = math.vec3();
    const normalA = math.vec3();
    const normalB = math.vec3();
    const normalC = math.vec3();
    const uva = math.vec3();
    const uvb = math.vec3();
    const uvc = math.vec3();
    const tempVec4a = math.vec4();
    const tempVec4b = math.vec4();
    const tempVec4c = math.vec4();
    const tempVec3 = math.vec3();
    const tempVec3b = math.vec3();
    const tempVec3c = math.vec3();
    const tempVec3d = math.vec3();
    const tempVec3e = math.vec3();
    const tempVec3f = math.vec3();
    const tempVec3g = math.vec3();
    const tempVec3h = math.vec3();
    const tempVec3i = math.vec3();
    const tempVec3j = math.vec3();
    const tempVec3k = math.vec3();

    return function (mesh, pickViewMatrix, pickProjMatrix, projection, pickResult) {

        var primIndex = pickResult.primIndex;

        if (primIndex !== undefined && primIndex !== null && primIndex > -1) {

            const geometry = mesh.geometry._state;
            const scene = mesh.scene;
            const camera = scene.camera;
            const canvas = scene.canvas;

            if (geometry.primitiveName === "triangles") {

                // Triangle picked; this only happens when the
                // Mesh has a Geometry that has primitives of type "triangle"

                pickResult.primitive = "triangle";

                // Get the World-space positions of the triangle's vertices

                const i = primIndex; // Indicates the first triangle index in the indices array

                const indices = geometry.indices; // Indices into geometry arrays, not into shared VertexBufs
                const positions = geometry.positions;

                let ia3;
                let ib3;
                let ic3;

                if (indices) {

                    var ia = indices[i + 0];
                    var ib = indices[i + 1];
                    var ic = indices[i + 2];

                    triangleVertices[0] = ia;
                    triangleVertices[1] = ib;
                    triangleVertices[2] = ic;

                    pickResult.indices = triangleVertices;

                    ia3 = ia * 3;
                    ib3 = ib * 3;
                    ic3 = ic * 3;

                } else {

                    ia3 = i * 3;
                    ib3 = ia3 + 3;
                    ic3 = ib3 + 3;
                }

                positionA[0] = positions[ia3 + 0];
                positionA[1] = positions[ia3 + 1];
                positionA[2] = positions[ia3 + 2];

                positionB[0] = positions[ib3 + 0];
                positionB[1] = positions[ib3 + 1];
                positionB[2] = positions[ib3 + 2];

                positionC[0] = positions[ic3 + 0];
                positionC[1] = positions[ic3 + 1];
                positionC[2] = positions[ic3 + 2];

                if (geometry.compressGeometry) {

                    // Decompress vertex positions

                    const positionsDecodeMatrix = geometry.positionsDecodeMatrix;
                    if (positionsDecodeMatrix) {
                        geometryCompressionUtils.decompressPosition(positionA, positionsDecodeMatrix, positionA);
                        geometryCompressionUtils.decompressPosition(positionB, positionsDecodeMatrix, positionB);
                        geometryCompressionUtils.decompressPosition(positionC, positionsDecodeMatrix, positionC);
                    }
                }

                // Attempt to ray-pick the triangle in local space

                if (pickResult.canvasPos) {
                    math.canvasPosToLocalRay(canvas.canvas, mesh.origin ? createRTCViewMat(pickViewMatrix, mesh.origin) : pickViewMatrix, pickProjMatrix, projection, mesh.worldMatrix, pickResult.canvasPos, localRayOrigin, localRayDir);

                } else if (pickResult.origin && pickResult.direction) {
                    math.worldRayToLocalRay(mesh.worldMatrix, pickResult.origin, pickResult.direction, localRayOrigin, localRayDir);
                }

                math.normalizeVec3(localRayDir);
                math.rayPlaneIntersect(localRayOrigin, localRayDir, positionA, positionB, positionC, position);

                // Get Local-space cartesian coordinates of the ray-triangle intersection

                pickResult.localPos = position;
                pickResult.position = position;

                // Get interpolated World-space coordinates

                // Need to transform homogeneous coords

                tempVec4a[0] = position[0];
                tempVec4a[1] = position[1];
                tempVec4a[2] = position[2];
                tempVec4a[3] = 1;

                // Get World-space cartesian coordinates of the ray-triangle intersection

                math.transformVec4(mesh.worldMatrix, tempVec4a, tempVec4b);

                worldPos[0] = tempVec4b[0];
                worldPos[1] = tempVec4b[1];
                worldPos[2] = tempVec4b[2];

                if (pickResult.canvasPos && mesh.origin) {
                    worldPos[0] += mesh.origin[0];
                    worldPos[1] += mesh.origin[1];
                    worldPos[2] += mesh.origin[2];
                }

                pickResult.worldPos = worldPos;

                // Get View-space cartesian coordinates of the ray-triangle intersection

                math.transformVec4(camera.matrix, tempVec4b, tempVec4c);

                viewPos[0] = tempVec4c[0];
                viewPos[1] = tempVec4c[1];
                viewPos[2] = tempVec4c[2];

                pickResult.viewPos = viewPos;

                // Get barycentric coordinates of the ray-triangle intersection

                math.cartesianToBarycentric(position, positionA, positionB, positionC, bary);

                pickResult.bary = bary;

                // Get interpolated normal vector

                const normals = geometry.normals;

                if (normals) {

                    if (geometry.compressGeometry) {

                        // Decompress vertex normals

                        const ia2 = ia * 3;
                        const ib2 = ib * 3;
                        const ic2 = ic * 3;

                        geometryCompressionUtils.decompressNormal(normals.subarray(ia2, ia2 + 2), normalA);
                        geometryCompressionUtils.decompressNormal(normals.subarray(ib2, ib2 + 2), normalB);
                        geometryCompressionUtils.decompressNormal(normals.subarray(ic2, ic2 + 2), normalC);

                    } else {

                        normalA[0] = normals[ia3];
                        normalA[1] = normals[ia3 + 1];
                        normalA[2] = normals[ia3 + 2];

                        normalB[0] = normals[ib3];
                        normalB[1] = normals[ib3 + 1];
                        normalB[2] = normals[ib3 + 2];

                        normalC[0] = normals[ic3];
                        normalC[1] = normals[ic3 + 1];
                        normalC[2] = normals[ic3 + 2];
                    }

                    const normal = math.addVec3(math.addVec3(
                            math.mulVec3Scalar(normalA, bary[0], tempVec3),
                            math.mulVec3Scalar(normalB, bary[1], tempVec3b), tempVec3c),
                        math.mulVec3Scalar(normalC, bary[2], tempVec3d), tempVec3e);

                    pickResult.worldNormal = math.normalizeVec3(math.transformVec3(mesh.worldNormalMatrix, normal, tempVec3f));
                }

                // Get interpolated UV coordinates

                const uvs = geometry.uv;

                if (uvs) {

                    uva[0] = uvs[(ia * 2)];
                    uva[1] = uvs[(ia * 2) + 1];

                    uvb[0] = uvs[(ib * 2)];
                    uvb[1] = uvs[(ib * 2) + 1];

                    uvc[0] = uvs[(ic * 2)];
                    uvc[1] = uvs[(ic * 2) + 1];

                    if (geometry.compressGeometry) {

                        // Decompress vertex UVs

                        const uvDecodeMatrix = geometry.uvDecodeMatrix;
                        if (uvDecodeMatrix) {
                            geometryCompressionUtils.decompressUV(uva, uvDecodeMatrix, uva);
                            geometryCompressionUtils.decompressUV(uvb, uvDecodeMatrix, uvb);
                            geometryCompressionUtils.decompressUV(uvc, uvDecodeMatrix, uvc);
                        }
                    }

                    pickResult.uv = math.addVec3(
                        math.addVec3(
                            math.mulVec2Scalar(uva, bary[0], tempVec3g),
                            math.mulVec2Scalar(uvb, bary[1], tempVec3h), tempVec3i),
                        math.mulVec2Scalar(uvc, bary[2], tempVec3j), tempVec3k);
                }
            }
        }
    }
})();

/**
 * @desc A **Material** defines the surface appearance of attached {@link Mesh}es.
 *
 * Material is the base class for:
 *
 * * {@link MetallicMaterial} - physically-based material for metallic surfaces. Use this one for things made of metal.
 * * {@link SpecularMaterial} - physically-based material for non-metallic (dielectric) surfaces. Use this one for insulators, such as ceramics, plastics, wood etc.
 * * {@link PhongMaterial} - material for classic Blinn-Phong shading. This is less demanding of graphics hardware than the physically-based materials.
 * * {@link LambertMaterial} - material for fast, flat-shaded CAD rendering without textures. Use this for navigating huge CAD or BIM models interactively. This material gives the best rendering performance and uses the least memory.
 * * {@link EmphasisMaterial} - defines the appearance of Meshes when "xrayed" or "highlighted".
 * * {@link EdgeMaterial} - defines the appearance of Meshes when edges are emphasized.
 *
 * A {@link Scene} is allowed to contain a mixture of these material types.
 *
 */

class Material extends Component {

    /**
     @private
     */
    get type() {
        return "Material";
    }

    constructor(owner, cfg={}) {
        super(owner, cfg);
        stats.memory.materials++;
    }

    destroy() {
        super.destroy();
        stats.memory.materials--;
    }
}

const alphaModes$1 = {"opaque": 0, "mask": 1, "blend": 2};
const alphaModeNames$1 = ["opaque", "mask", "blend"];

/**
 * @desc Configures the normal rendered appearance of {@link Mesh}es using the non-physically-correct Blinn-Phong shading model.
 *
 * * Useful for non-realistic objects like gizmos.
 * * {@link SpecularMaterial} is best for insulators, such as wood, ceramics and plastic.
 * * {@link MetallicMaterial} is best for conductive materials, such as metal.
 * * {@link LambertMaterial} is appropriate for high-detail models that need to render as efficiently as possible.
 *
 * ## Usage
 *
 * In the example below, we'll create a {@link Mesh} with a PhongMaterial with a diffuse {@link Texture} and a specular {@link Fresnel}, using a {@link buildTorusGeometry} to create the {@link Geometry}.
 *
 * [[Run this example](/examples/index.html#materials_PhongMaterial)]
 *
 *  ```` javascript
 * import {Viewer, Mesh, buildTorusGeometry,
 *     ReadableGeometry, PhongMaterial, Texture, Fresnel} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *        canvasId: "myCanvas"
 *    });
 *
 * viewer.scene.camera.eye = [0, 0, 5];
 * viewer.scene.camera.look = [0, 0, 0];
 * viewer.scene.camera.up = [0, 1, 0];
 *
 * new Mesh(viewer.scene, {
 *      geometry: new ReadableGeometry(viewer.scene, buildTorusGeometry({
 *          center: [0, 0, 0],
 *          radius: 1.5,
 *          tube: 0.5,
 *          radialSegments: 32,
 *          tubeSegments: 24,
 *          arc: Math.PI * 2.0
 *      }),
 *      material: new PhongMaterial(viewer.scene, {
 *          ambient: [0.9, 0.3, 0.9],
 *          shininess: 30,
 *          diffuseMap: new Texture(viewer.scene, {
 *              src: "textures/diffuse/uvGrid2.jpg"
 *          }),
 *          specularFresnel: new Fresnel(viewer.scene, {
 *              leftColor: [1.0, 1.0, 1.0],
 *              rightColor: [0.0, 0.0, 0.0],
 *              power: 4
 *          })
 *     })
 * });
 * ````
 *
 * ## PhongMaterial Properties
 *
 *  The following table summarizes PhongMaterial properties:
 *
 * | Property | Type | Range | Default Value | Space | Description |
 * |:--------:|:----:|:-----:|:-------------:|:-----:|:-----------:|
 * | {@link PhongMaterial#ambient} | Array | [0, 1] for all components | [1,1,1,1] | linear | The RGB components of the ambient light reflected by the material. |
 * | {@link PhongMaterial#diffuse} | Array | [0, 1] for all components | [1,1,1,1] | linear | The RGB components of the diffuse light reflected by the material. |
 * | {@link PhongMaterial#specular} | Array | [0, 1] for all components | [1,1,1,1] | linear | The RGB components of the specular light reflected by the material. |
 * | {@link PhongMaterial#emissive} | Array | [0, 1] for all components | [0,0,0] | linear | The RGB components of the light emitted by the material. |
 * | {@link PhongMaterial#alpha} | Number | [0, 1] | 1 | linear | The transparency of the material surface (0 fully transparent, 1 fully opaque). |
 * | {@link PhongMaterial#shininess} | Number | [0, 128] | 80 | linear | Determines the size and sharpness of specular highlights. |
 * | {@link PhongMaterial#reflectivity} | Number | [0, 1] | 1 | linear | Determines the amount of reflectivity. |
 * | {@link PhongMaterial#diffuseMap} | {@link Texture} |  | null | sRGB | Texture RGB components multiplying by {@link PhongMaterial#diffuse}. If the fourth component (A) is present, it multiplies by {@link PhongMaterial#alpha}. |
 * | {@link PhongMaterial#specularMap} | {@link Texture} |  | null | sRGB | Texture RGB components multiplying by {@link PhongMaterial#specular}. If the fourth component (A) is present, it multiplies by {@link PhongMaterial#alpha}. |
 * | {@link PhongMaterial#emissiveMap} | {@link Texture} |  | null | linear | Texture with RGB components multiplying by {@link PhongMaterial#emissive}. |
 * | {@link PhongMaterial#alphaMap} | {@link Texture} |  | null | linear | Texture with first component multiplying by {@link PhongMaterial#alpha}. |
 * | {@link PhongMaterial#occlusionMap} | {@link Texture} |  | null | linear | Ambient occlusion texture multiplying by {@link PhongMaterial#ambient}, {@link PhongMaterial#diffuse} and {@link PhongMaterial#specular}. |
 * | {@link PhongMaterial#normalMap} | {@link Texture} |  | null | linear | Tangent-space normal map. |
 * | {@link PhongMaterial#diffuseFresnel} | {@link Fresnel} |  | null |  | Fresnel term applied to {@link PhongMaterial#diffuse}. |
 * | {@link PhongMaterial#specularFresnel} | {@link Fresnel} |  | null |  | Fresnel term applied to {@link PhongMaterial#specular}. |
 * | {@link PhongMaterial#emissiveFresnel} | {@link Fresnel} |  | null |  | Fresnel term applied to {@link PhongMaterial#emissive}. |
 * | {@link PhongMaterial#reflectivityFresnel} | {@link Fresnel} |  | null |  | Fresnel term applied to {@link PhongMaterial#reflectivity}. |
 * | {@link PhongMaterial#alphaFresnel} | {@link Fresnel} |  | null |  | Fresnel term applied to {@link PhongMaterial#alpha}. |
 * | {@link PhongMaterial#lineWidth} | Number | [0..100] | 1 |  | Line width in pixels. |
 * | {@link PhongMaterial#pointSize} | Number | [0..100] | 1 |  | Point size in pixels. |
 * | {@link PhongMaterial#alphaMode} | String | "opaque", "blend", "mask" | "blend" |  | Alpha blend mode. |
 * | {@link PhongMaterial#alphaCutoff} | Number | [0..1] | 0.5 |  | Alpha cutoff value. |
 * | {@link PhongMaterial#backfaces} | Boolean |  | false |  | Whether to render geometry backfaces. |
 * | {@link PhongMaterial#frontface} | String | "ccw", "cw" | "ccw" |  | The winding order for geometry frontfaces - "cw" for clockwise, or "ccw" for counter-clockwise. |
 */
class PhongMaterial extends Material {

    /**
     @private
     */
    get type() {
        return "PhongMaterial";
    }

    /**
     * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well.
     * @param {*} [cfg] The PhongMaterial configuration
     * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted.
     * @param {Number[]} [cfg.ambient=[1.0, 1.0, 1.0 ]]  PhongMaterial ambient color.
     * @param {Number[]} [cfg.diffuse=[ 1.0, 1.0, 1.0 ]] PhongMaterial diffuse color.
     * @param {Number[]} [cfg.specular=[ 1.0, 1.0, 1.0 ]]  PhongMaterial specular color.
     * @param {Number[]} [cfg.emissive=[ 0.0, 0.0, 0.0 ]] PhongMaterial emissive color.
     * @param {Number} [cfg.alpha=1] Scalar in range 0-1 that controls alpha, where 0 is completely transparent and 1 is completely opaque.
     * @param {Number} [cfg.shininess=80] Scalar in range 0-128 that determines the size and sharpness of specular highlights.
     * @param {Number} [cfg.reflectivity=1] Scalar in range 0-1 that controls how much {@link ReflectionMap} is reflected.
     * @param {Number} [cfg.lineWidth=1] Scalar that controls the width of lines.
     * @param {Number} [cfg.pointSize=1] Scalar that controls the size of points.
     * @param {Texture} [cfg.ambientMap=null] A ambient map {@link Texture}, which will multiply by the diffuse property. Must be within the same {@link Scene} as this PhongMaterial.
     * @param {Texture} [cfg.diffuseMap=null] A diffuse map {@link Texture}, which will override the effect of the diffuse property. Must be within the same {@link Scene} as this PhongMaterial.
     * @param {Texture} [cfg.specularMap=null] A specular map {@link Texture}, which will override the effect of the specular property. Must be within the same {@link Scene} as this PhongMaterial.
     * @param {Texture} [cfg.emissiveMap=undefined] An emissive map {@link Texture}, which will override the effect of the emissive property. Must be within the same {@link Scene} as this PhongMaterial.
     * @param {Texture} [cfg.normalMap=undefined] A normal map {@link Texture}. Must be within the same {@link Scene} as this PhongMaterial.
     * @param {Texture} [cfg.alphaMap=undefined] An alpha map {@link Texture}, which will override the effect of the alpha property. Must be within the same {@link Scene} as this PhongMaterial.
     * @param {Texture} [cfg.reflectivityMap=undefined] A reflectivity control map {@link Texture}, which will override the effect of the reflectivity property. Must be within the same {@link Scene} as this PhongMaterial.
     * @param {Texture} [cfg.occlusionMap=null] An occlusion map {@link Texture}. Must be within the same {@link Scene} as this PhongMaterial.
     * @param {Fresnel} [cfg.diffuseFresnel=undefined] A diffuse {@link Fresnel"}}Fresnel{{/crossLink}}. Must be within the same {@link Scene} as this PhongMaterial.
     * @param {Fresnel} [cfg.specularFresnel=undefined] A specular {@link Fresnel"}}Fresnel{{/crossLink}}. Must be within the same {@link Scene} as this PhongMaterial.
     * @param {Fresnel} [cfg.emissiveFresnel=undefined] An emissive {@link Fresnel"}}Fresnel{{/crossLink}}. Must be within the same {@link Scene} as this PhongMaterial.
     * @param {Fresnel} [cfg.alphaFresnel=undefined] An alpha {@link Fresnel"}}Fresnel{{/crossLink}}. Must be within the same {@link Scene} as this PhongMaterial.
     * @param {Fresnel} [cfg.reflectivityFresnel=undefined] A reflectivity {@link Fresnel"}}Fresnel{{/crossLink}}. Must be within the same {@link Scene} as this PhongMaterial.
     * @param {String} [cfg.alphaMode="opaque"] The alpha blend mode - accepted values are "opaque", "blend" and "mask". See the {@link PhongMaterial#alphaMode} property for more info.
     * @param {Number} [cfg.alphaCutoff=0.5] The alpha cutoff value. See the {@link PhongMaterial#alphaCutoff} property for more info.
     * @param {Boolean} [cfg.backfaces=false] Whether to render geometry backfaces.
     * @param {Boolean} [cfg.frontface="ccw"] The winding order for geometry front faces - "cw" for clockwise, or "ccw" for counter-clockwise.
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._state = new RenderState({
            type: "PhongMaterial",
            ambient: math.vec3([1.0, 1.0, 1.0]),
            diffuse: math.vec3([1.0, 1.0, 1.0]),
            specular: math.vec3([1.0, 1.0, 1.0]),
            emissive: math.vec3([0.0, 0.0, 0.0]),
            alpha: null,
            shininess: null,
            reflectivity: null,
            alphaMode: null,
            alphaCutoff: null,
            lineWidth: null,
            pointSize: null,
            backfaces: null,
            frontface: null, // Boolean for speed; true == "ccw", false == "cw"
            hash: null
        });

        this.ambient = cfg.ambient;
        this.diffuse = cfg.diffuse;
        this.specular = cfg.specular;
        this.emissive = cfg.emissive;
        this.alpha = cfg.alpha;
        this.shininess = cfg.shininess;
        this.reflectivity = cfg.reflectivity;
        this.lineWidth = cfg.lineWidth;
        this.pointSize = cfg.pointSize;

        if (cfg.ambientMap) {
            this._ambientMap = this._checkComponent("Texture", cfg.ambientMap);
        }
        if (cfg.diffuseMap) {
            this._diffuseMap = this._checkComponent("Texture", cfg.diffuseMap);
        }
        if (cfg.specularMap) {
            this._specularMap = this._checkComponent("Texture", cfg.specularMap);
        }
        if (cfg.emissiveMap) {
            this._emissiveMap = this._checkComponent("Texture", cfg.emissiveMap);
        }
        if (cfg.alphaMap) {
            this._alphaMap = this._checkComponent("Texture", cfg.alphaMap);
        }
        if (cfg.reflectivityMap) {
            this._reflectivityMap = this._checkComponent("Texture", cfg.reflectivityMap);
        }
        if (cfg.normalMap) {
            this._normalMap = this._checkComponent("Texture", cfg.normalMap);
        }
        if (cfg.occlusionMap) {
            this._occlusionMap = this._checkComponent("Texture", cfg.occlusionMap);
        }
        if (cfg.diffuseFresnel) {
            this._diffuseFresnel = this._checkComponent("Fresnel", cfg.diffuseFresnel);
        }
        if (cfg.specularFresnel) {
            this._specularFresnel = this._checkComponent("Fresnel", cfg.specularFresnel);
        }
        if (cfg.emissiveFresnel) {
            this._emissiveFresnel = this._checkComponent("Fresnel", cfg.emissiveFresnel);
        }
        if (cfg.alphaFresnel) {
            this._alphaFresnel = this._checkComponent("Fresnel", cfg.alphaFresnel);
        }
        if (cfg.reflectivityFresnel) {
            this._reflectivityFresnel = this._checkComponent("Fresnel", cfg.reflectivityFresnel);
        }

        this.alphaMode = cfg.alphaMode;
        this.alphaCutoff = cfg.alphaCutoff;
        this.backfaces = cfg.backfaces;
        this.frontface = cfg.frontface;

        this._makeHash();
    }

    _makeHash() {
        const state = this._state;
        const hash = ["/p"]; // 'P' for Phong
        if (this._normalMap) {
            hash.push("/nm");
            if (this._normalMap.hasMatrix) {
                hash.push("/mat");
            }
        }
        if (this._ambientMap) {
            hash.push("/am");
            if (this._ambientMap.hasMatrix) {
                hash.push("/mat");
            }
            hash.push("/" + this._ambientMap.encoding);
        }
        if (this._diffuseMap) {
            hash.push("/dm");
            if (this._diffuseMap.hasMatrix) {
                hash.push("/mat");
            }
            hash.push("/" + this._diffuseMap.encoding);
        }
        if (this._specularMap) {
            hash.push("/sm");
            if (this._specularMap.hasMatrix) {
                hash.push("/mat");
            }
        }
        if (this._emissiveMap) {
            hash.push("/em");
            if (this._emissiveMap.hasMatrix) {
                hash.push("/mat");
            }
            hash.push("/" + this._emissiveMap.encoding);
        }
        if (this._alphaMap) {
            hash.push("/opm");
            if (this._alphaMap.hasMatrix) {
                hash.push("/mat");
            }
        }
        if (this._reflectivityMap) {
            hash.push("/rm");
            if (this._reflectivityMap.hasMatrix) {
                hash.push("/mat");
            }
        }
        if (this._occlusionMap) {
            hash.push("/ocm");
            if (this._occlusionMap.hasMatrix) {
                hash.push("/mat");
            }
        }
        if (this._diffuseFresnel) {
            hash.push("/df");
        }
        if (this._specularFresnel) {
            hash.push("/sf");
        }
        if (this._emissiveFresnel) {
            hash.push("/ef");
        }
        if (this._alphaFresnel) {
            hash.push("/of");
        }
        if (this._reflectivityFresnel) {
            hash.push("/rf");
        }
        hash.push(";");
        state.hash = hash.join("");
    }

    /**
     * Sets the PhongMaterial's ambient color.
     *
     * Default value is ````[0.3, 0.3, 0.3]````.
     *
     * @type {Number[]}
     */
    set ambient(value) {
        let ambient = this._state.ambient;
        if (!ambient) {
            ambient = this._state.ambient = new Float32Array(3);
        } else if (value && ambient[0] === value[0] && ambient[1] === value[1] && ambient[2] === value[2]) {
            return;
        }
        if (value) {
            ambient[0] = value[0];
            ambient[1] = value[1];
            ambient[2] = value[2];
        } else {
            ambient[0] = .2;
            ambient[1] = .2;
            ambient[2] = .2;
        }
        this.glRedraw();
    }

    /**
     * Gets the PhongMaterial's ambient color.
     *
     * Default value is ````[0.3, 0.3, 0.3]````.
     *
     * @type {Number[]}
     */
    get ambient() {
        return this._state.ambient;
    }

    /**
     * Sets the PhongMaterial's diffuse color.
     *
     * Multiplies by {@link PhongMaterial#diffuseMap}.
     *
     * Default value is ````[1.0, 1.0, 1.0]````.
     *
     * @type {Number[]}
     */
    set diffuse(value) {
        let diffuse = this._state.diffuse;
        if (!diffuse) {
            diffuse = this._state.diffuse = new Float32Array(3);
        } else if (value && diffuse[0] === value[0] && diffuse[1] === value[1] && diffuse[2] === value[2]) {
            return;
        }
        if (value) {
            diffuse[0] = value[0];
            diffuse[1] = value[1];
            diffuse[2] = value[2];
        } else {
            diffuse[0] = 1;
            diffuse[1] = 1;
            diffuse[2] = 1;
        }
        this.glRedraw();
    }

    /**
     * Sets the PhongMaterial's diffuse color.
     *
     * Multiplies by {@link PhongMaterial#diffuseMap}.
     *
     * Default value is ````[1.0, 1.0, 1.0]````.
     *
     * @type {Number[]}
     */
    get diffuse() {
        return this._state.diffuse;
    }

    /**
     * Sets the PhongMaterial's specular color.
     *
     * Multiplies by {@link PhongMaterial#specularMap}.
     * Default value is ````[1.0, 1.0, 1.0]````.
     * @type {Number[]}
     */
    set specular(value) {
        let specular = this._state.specular;
        if (!specular) {
            specular = this._state.specular = new Float32Array(3);
        } else if (value && specular[0] === value[0] && specular[1] === value[1] && specular[2] === value[2]) {
            return;
        }
        if (value) {
            specular[0] = value[0];
            specular[1] = value[1];
            specular[2] = value[2];
        } else {
            specular[0] = 1;
            specular[1] = 1;
            specular[2] = 1;
        }
        this.glRedraw();
    }

    /**
     * Gets the PhongMaterial's specular color.
     *
     * Multiplies by {@link PhongMaterial#specularMap}.
     * Default value is ````[1.0, 1.0, 1.0]````.
     * @type {Number[]}
     */
    get specular() {
        return this._state.specular;
    }

    /**
     * Sets the PhongMaterial's emissive color.
     *
     * Multiplies by {@link PhongMaterial#emissiveMap}.
     *
     * Default value is ````[0.0, 0.0, 0.0]````.
     * @type {Number[]}
     */
    set emissive(value) {
        let emissive = this._state.emissive;
        if (!emissive) {
            emissive = this._state.emissive = new Float32Array(3);
        } else if (value && emissive[0] === value[0] && emissive[1] === value[1] && emissive[2] === value[2]) {
            return;
        }
        if (value) {
            emissive[0] = value[0];
            emissive[1] = value[1];
            emissive[2] = value[2];
        } else {
            emissive[0] = 0;
            emissive[1] = 0;
            emissive[2] = 0;
        }
        this.glRedraw();
    }

    /**
     * Gets the PhongMaterial's emissive color.
     *
     * Multiplies by {@link PhongMaterial#emissiveMap}.
     *
     * Default value is ````[0.0, 0.0, 0.0]````.
     * @type {Number[]}
     */
    get emissive() {
        return this._state.emissive;
    }

    /**
     * Sets the PhongMaterial alpha.
     *
     * This is a factor in the range [0..1] indicating how transparent the PhongMaterial is.
     *
     * A value of 0.0 indicates fully transparent, 1.0 is fully opaque.
     *
     * Multiplies by {@link PhongMaterial#alphaMap}.
     *
     * Default value is ````1.0````.
     *
     * @type {Number}
     */
    set alpha(value) {
        value = (value !== undefined && value !== null) ? value : 1.0;
        if (this._state.alpha === value) {
            return;
        }
        this._state.alpha = value;
        this.glRedraw();
    }

    /**
     * Gets the PhongMaterial alpha.
     *
     * This is a factor in the range [0..1] indicating how transparent the PhongMaterial is.
     *
     * A value of 0.0 indicates fully transparent, 1.0 is fully opaque.
     *
     * Multiplies by {@link PhongMaterial#alphaMap}.
     *
     * Default value is ````1.0````.
     *
     * @type {Number}
     */
    get alpha() {
        return this._state.alpha;
    }

    /**
     * Sets the PhongMaterial shininess.
     *
     * This is a factor in range [0..128] that determines the size and sharpness of the specular highlights create by this PhongMaterial.
     *
     * Larger values produce smaller, sharper highlights. A value of 0.0 gives very large highlights that are almost never
     * desirable. Try values close to 10 for a larger, fuzzier highlight and values of 100 or more for a small, sharp
     * highlight.
     *
     * Default value is ```` 80.0````.
     *
     * @type {Number}
     */
    set shininess(value) {
        this._state.shininess = value !== undefined ? value : 80;
        this.glRedraw();
    }

    /**
     * Gets the PhongMaterial shininess.
     *
     * This is a factor in range [0..128] that determines the size and sharpness of the specular highlights create by this PhongMaterial.
     *
     * Larger values produce smaller, sharper highlights. A value of 0.0 gives very large highlights that are almost never
     * desirable. Try values close to 10 for a larger, fuzzier highlight and values of 100 or more for a small, sharp
     * highlight.
     *
     * Default value is ```` 80.0````.
     *
     * @type {Number}
     */
    get shininess() {
        return this._state.shininess;
    }

    /**
     * Sets the PhongMaterial's line width.
     *
     * This is not supported by WebGL implementations based on DirectX [2019].
     *
     * Default value is ````1.0````.
     *
     * @type {Number}
     */
    set lineWidth(value) {
        this._state.lineWidth = value || 1.0;
        this.glRedraw();
    }

    /**
     * Gets the PhongMaterial's line width.
     *
     * This is not supported by WebGL implementations based on DirectX [2019].
     *
     * Default value is ````1.0````.
     *
     * @type {Number}
     */
    get lineWidth() {
        return this._state.lineWidth;
    }

    /**
     * Sets the PhongMaterial's point size.
     *
     * Default value is 1.0.
     *
     * @type {Number}
     */
    set pointSize(value) {
        this._state.pointSize = value || 1.0;
        this.glRedraw();
    }

    /**
     * Gets the PhongMaterial's point size.
     *
     * Default value is 1.0.
     *
     * @type {Number}
     */
    get pointSize() {
        return this._state.pointSize;
    }

    /**
     * Sets how much {@link ReflectionMap} is reflected by this PhongMaterial.
     *
     * This is a scalar in range ````[0-1]````. Default value is ````1.0````.
     *
     * The surface will be non-reflective when this is ````0````, and completely mirror-like when it is ````1.0````.
     *
     * Multiplies by {@link PhongMaterial#reflectivityMap}.
     *
     * @type {Number}
     */
    set reflectivity(value) {
        this._state.reflectivity = value !== undefined ? value : 1.0;
        this.glRedraw();
    }

    /**
     * Gets how much {@link ReflectionMap} is reflected by this PhongMaterial.
     *
     * This is a scalar in range ````[0-1]````. Default value is ````1.0````.
     *
     * The surface will be non-reflective when this is ````0````, and completely mirror-like when it is ````1.0````.
     *
     * Multiplies by {@link PhongMaterial#reflectivityMap}.
     *
     * @type {Number}
     */
    get reflectivity() {
        return this._state.reflectivity;
    }

    /**
     * Gets the PhongMaterials's normal map {@link Texture}.
     *
     * @type {Texture}
     */
    get normalMap() {
        return this._normalMap;
    }

    /**
     * Gets the PhongMaterials's ambient {@link Texture}.
     *
     * Multiplies by {@link PhongMaterial#ambient}.
     *
     * @type {Texture}
     */
    get ambientMap() {
        return this._ambientMap;
    }

    /**
     * Gets the PhongMaterials's diffuse {@link Texture}.
     *
     * Multiplies by {@link PhongMaterial#diffuse}.
     *
     * @type {Texture}
     */
    get diffuseMap() {
        return this._diffuseMap;
    }

    /**
     * Gets the PhongMaterials's specular {@link Texture}.
     *
     * Multiplies by {@link PhongMaterial#specular}.
     *
     * @type {Texture}
     */
    get specularMap() {
        return this._specularMap;
    }

    /**
     * Gets the PhongMaterials's emissive {@link Texture}.
     *
     * Multiplies by {@link PhongMaterial#emissive}.
     *
     * @type {Texture}
     */
    get emissiveMap() {
        return this._emissiveMap;
    }

    /**
     * Gets the PhongMaterials's alpha {@link Texture}.
     *
     * Multiplies by {@link PhongMaterial#alpha}.
     *
     * @type {Texture}
     */
    get alphaMap() {
        return this._alphaMap;
    }

    /**
     * Gets the PhongMaterials's reflectivity {@link Texture}.
     *
     * Multiplies by {@link PhongMaterial#reflectivity}.
     *
     * @type {Texture}
     */
    get reflectivityMap() {
        return this._reflectivityMap;
    }

    /**
     * Gets the PhongMaterials's ambient occlusion {@link Texture}.
     *
     * @type {Texture}
     */
    get occlusionMap() {
        return this._occlusionMap;
    }

    /**
     * Gets the PhongMaterials's diffuse {@link Fresnel}.
     *
     * Applies to {@link PhongMaterial#diffuse}.
     *
     * @type {Fresnel}
     */
    get diffuseFresnel() {
        return this._diffuseFresnel;
    }

    /**
     * Gets the PhongMaterials's specular {@link Fresnel}.
     *
     * Applies to {@link PhongMaterial#specular}.
     *
     * @type {Fresnel}
     */
    get specularFresnel() {
        return this._specularFresnel;
    }

    /**
     * Gets the PhongMaterials's emissive {@link Fresnel}.
     *
     * Applies to {@link PhongMaterial#emissive}.
     *
     * @type {Fresnel}
     */
    get emissiveFresnel() {
        return this._emissiveFresnel;
    }

    /**
     * Gets the PhongMaterials's alpha {@link Fresnel}.
     *
     * Applies to {@link PhongMaterial#alpha}.
     *
     * @type {Fresnel}
     */
    get alphaFresnel() {
        return this._alphaFresnel;
    }

    /**
     * Gets the PhongMaterials's reflectivity {@link Fresnel}.
     *
     * Applies to {@link PhongMaterial#reflectivity}.
     *
     * @type {Fresnel}
     */
    get reflectivityFresnel() {
        return this._reflectivityFresnel;
    }

    /**
     * Sets the PhongMaterial's alpha rendering mode.
     *
     * This governs how alpha is treated. Alpha is the combined result of {@link PhongMaterial#alpha} and {@link PhongMaterial#alphaMap}.
     *
     * Supported values are:
     *
     * * "opaque" - The alpha value is ignored and the rendered output is fully opaque (default).
     * * "mask" - The rendered output is either fully opaque or fully transparent depending on the alpha value and the specified alpha cutoff value.
     * * "blend" - The alpha value is used to composite the source and destination areas. The rendered output is combined with the background using the normal painting operation (i.e. the Porter and Duff over operator).
     *
     *@type {String}
     */
    set alphaMode(alphaMode) {
        alphaMode = alphaMode || "opaque";
        let value = alphaModes$1[alphaMode];
        if (value === undefined) {
            this.error("Unsupported value for 'alphaMode': " + alphaMode + " - defaulting to 'opaque'");
            value = "opaque";
        }
        if (this._state.alphaMode === value) {
            return;
        }
        this._state.alphaMode = value;
        this.glRedraw();
    }

    /**
     * Gets the PhongMaterial's alpha rendering mode.
     *
     *@type {String}
     */
    get alphaMode() {
        return alphaModeNames$1[this._state.alphaMode];
    }

    /**
     * Sets the PhongMaterial's alpha cutoff value.
     *
     * This specifies the cutoff threshold when {@link PhongMaterial#alphaMode} equals "mask". If the alpha is greater than or equal to this value then it is rendered as fully
     * opaque, otherwise, it is rendered as fully transparent. A value greater than 1.0 will render the entire material as fully transparent. This value is ignored for other modes.
     *
     * Alpha is the combined result of {@link PhongMaterial#alpha} and {@link PhongMaterial#alphaMap}.
     *
     * Default value is ````0.5````.
     *
     * @type {Number}
     */
    set alphaCutoff(alphaCutoff) {
        if (alphaCutoff === null || alphaCutoff === undefined) {
            alphaCutoff = 0.5;
        }
        if (this._state.alphaCutoff === alphaCutoff) {
            return;
        }
        this._state.alphaCutoff = alphaCutoff;
    }

    /**
     * Gets the PhongMaterial's alpha cutoff value.
     *
     * @type {Number}
     */
    get alphaCutoff() {
        return this._state.alphaCutoff;
    }

    /**
     * Sets whether backfaces are visible on attached {@link Mesh}es.
     *
     * The backfaces will belong to {@link Geometry} compoents that are also attached to the {@link Mesh}es.
     *
     * Default is ````false````.
     *
     * @type {Boolean}
     */
    set backfaces(value) {
        value = !!value;
        if (this._state.backfaces === value) {
            return;
        }
        this._state.backfaces = value;
        this.glRedraw();
    }

    /**
     * Gets whether backfaces are visible on attached {@link Mesh}es.
     *
     * Default is ````false````.
     *
     * @type {Boolean}
     */
    get backfaces() {
        return this._state.backfaces;
    }

    /**
     * Sets the winding direction of geometry front faces.
     *
     * Default is ````"ccw"````.
     * @type {String}
     */
    set frontface(value) {
        value = value !== "cw";
        if (this._state.frontface === value) {
            return;
        }
        this._state.frontface = value;
        this.glRedraw();
    }

    /**
     * Gets the winding direction of front faces on attached {@link Mesh}es.
     *
     * Default is ````"ccw"````.
     * @type {String}
     */
    get frontface() {
        return this._state.frontface ? "ccw" : "cw";
    }

    /**
     * Destroys this PhongMaterial.
     */
    destroy() {
        super.destroy();
        this._state.destroy();
    }
}

/**
 * @private
 */
function getExtension (gl, name) {
    if (gl._cachedExtensions === undefined) {
        gl._cachedExtensions = {};
    }
    if (gl._cachedExtensions[name] !== undefined) {
        return gl._cachedExtensions[name];
    }
    let extension;
    switch (name) {
        case 'WEBGL_depth_texture':
            extension = gl.getExtension('WEBGL_depth_texture') || gl.getExtension('MOZ_WEBGL_depth_texture') || gl.getExtension('WEBKIT_WEBGL_depth_texture');
            break;
        case 'EXT_texture_filter_anisotropic':
            extension = gl.getExtension('EXT_texture_filter_anisotropic') || gl.getExtension('MOZ_EXT_texture_filter_anisotropic') || gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic');
            break;
        case 'WEBGL_compressed_texture_s3tc':
            extension = gl.getExtension('WEBGL_compressed_texture_s3tc') || gl.getExtension('MOZ_WEBGL_compressed_texture_s3tc') || gl.getExtension('WEBKIT_WEBGL_compressed_texture_s3tc');
            break;
        case 'WEBGL_compressed_texture_pvrtc':
            extension = gl.getExtension('WEBGL_compressed_texture_pvrtc') || gl.getExtension('WEBKIT_WEBGL_compressed_texture_pvrtc');
            break;
        default:
            extension = gl.getExtension(name);
    }
    gl._cachedExtensions[name] = extension;
    return extension;
}

/**
 * @private
 */
function convertConstant(gl, constantVal, encoding = null) {

    let extension;
    const p = constantVal;

    if (p === UnsignedByteType) return gl.UNSIGNED_BYTE;
    if (p === UnsignedShort4444Type) return gl.UNSIGNED_SHORT_4_4_4_4;
    if (p === UnsignedShort5551Type) return gl.UNSIGNED_SHORT_5_5_5_1;

    if (p === ByteType) return gl.BYTE;
    if (p === ShortType) return gl.SHORT;
    if (p === UnsignedShortType) return gl.UNSIGNED_SHORT;
    if (p === IntType) return gl.INT;
    if (p === UnsignedIntType) return gl.UNSIGNED_INT;
    if (p === FloatType) return gl.FLOAT;

    if (p === HalfFloatType) {
        return gl.HALF_FLOAT;
    }

    if (p === AlphaFormat) return gl.ALPHA;
    if (p === RGBAFormat) return gl.RGBA;
    if (p === LuminanceFormat) return gl.LUMINANCE;
    if (p === LuminanceAlphaFormat) return gl.LUMINANCE_ALPHA;
    if (p === DepthFormat) return gl.DEPTH_COMPONENT;
    if (p === DepthStencilFormat) return gl.DEPTH_STENCIL;
    if (p === RedFormat) return gl.RED;

    if (p === RGBFormat) {
        return gl.RGBA;
    }

    // WebGL2 formats.

    if (p === RedIntegerFormat) return gl.RED_INTEGER;
    if (p === RGFormat) return gl.RG;
    if (p === RGIntegerFormat) return gl.RG_INTEGER;
    if (p === RGBAIntegerFormat) return gl.RGBA_INTEGER;

    // S3TC

    if (p === RGB_S3TC_DXT1_Format || p === RGBA_S3TC_DXT1_Format || p === RGBA_S3TC_DXT3_Format || p === RGBA_S3TC_DXT5_Format) {
        if (encoding === sRGBEncoding) {
            const extension = getExtension(gl, 'WEBGL_compressed_texture_s3tc_srgb');
            if (extension !== null) {
                if (p === RGB_S3TC_DXT1_Format) return extension.COMPRESSED_SRGB_S3TC_DXT1_EXT;
                if (p === RGBA_S3TC_DXT1_Format) return extension.COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;
                if (p === RGBA_S3TC_DXT3_Format) return extension.COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;
                if (p === RGBA_S3TC_DXT5_Format) return extension.COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT;
            } else {
                return null;
            }
        } else {
            extension = getExtension(gl, 'WEBGL_compressed_texture_s3tc');
            if (extension !== null) {
                if (p === RGB_S3TC_DXT1_Format) return extension.COMPRESSED_RGB_S3TC_DXT1_EXT;
                if (p === RGBA_S3TC_DXT1_Format) return extension.COMPRESSED_RGBA_S3TC_DXT1_EXT;
                if (p === RGBA_S3TC_DXT3_Format) return extension.COMPRESSED_RGBA_S3TC_DXT3_EXT;
                if (p === RGBA_S3TC_DXT5_Format) return extension.COMPRESSED_RGBA_S3TC_DXT5_EXT;
            } else {
                return null;
            }
        }
    }

    // PVRTC

    if (p === RGB_PVRTC_4BPPV1_Format || p === RGB_PVRTC_2BPPV1_Format || p === RGBA_PVRTC_4BPPV1_Format || p === RGBA_PVRTC_2BPPV1_Format) {
        const extension = getExtension(gl, 'WEBGL_compressed_texture_pvrtc');
        if (extension !== null) {
            if (p === RGB_PVRTC_4BPPV1_Format) return extension.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
            if (p === RGB_PVRTC_2BPPV1_Format) return extension.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;
            if (p === RGBA_PVRTC_4BPPV1_Format) return extension.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
            if (p === RGBA_PVRTC_2BPPV1_Format) return extension.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG;
        } else {
            return null;
        }
    }

    // ETC1

    if (p === RGB_ETC1_Format) {
        const extension = getExtension(gl, 'WEBGL_compressed_texture_etc1');
        if (extension !== null) {
            return extension.COMPRESSED_RGB_ETC1_WEBGL;
        } else {
            return null;
        }
    }

    // ETC2

    if (p === RGB_ETC2_Format || p === RGBA_ETC2_EAC_Format) {
        const extension = getExtension(gl, 'WEBGL_compressed_texture_etc');
        if (extension !== null) {
            if (p === RGB_ETC2_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ETC2 : extension.COMPRESSED_RGB8_ETC2;
            if (p === RGBA_ETC2_EAC_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ETC2_EAC : extension.COMPRESSED_RGBA8_ETC2_EAC;
        } else {
            return null;
        }
    }

    // ASTC

    if (p === RGBA_ASTC_4x4_Format || p === RGBA_ASTC_5x4_Format || p === RGBA_ASTC_5x5_Format ||
        p === RGBA_ASTC_6x5_Format || p === RGBA_ASTC_6x6_Format || p === RGBA_ASTC_8x5_Format ||
        p === RGBA_ASTC_8x6_Format || p === RGBA_ASTC_8x8_Format || p === RGBA_ASTC_10x5_Format ||
        p === RGBA_ASTC_10x6_Format || p === RGBA_ASTC_10x8_Format || p === RGBA_ASTC_10x10_Format ||
        p === RGBA_ASTC_12x10_Format || p === RGBA_ASTC_12x12_Format) {
        const extension = getExtension(gl, 'WEBGL_compressed_texture_astc');
        if (extension !== null) {
            if (p === RGBA_ASTC_4x4_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR : extension.COMPRESSED_RGBA_ASTC_4x4_KHR;
            if (p === RGBA_ASTC_5x4_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR : extension.COMPRESSED_RGBA_ASTC_5x4_KHR;
            if (p === RGBA_ASTC_5x5_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR : extension.COMPRESSED_RGBA_ASTC_5x5_KHR;
            if (p === RGBA_ASTC_6x5_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR : extension.COMPRESSED_RGBA_ASTC_6x5_KHR;
            if (p === RGBA_ASTC_6x6_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR : extension.COMPRESSED_RGBA_ASTC_6x6_KHR;
            if (p === RGBA_ASTC_8x5_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR : extension.COMPRESSED_RGBA_ASTC_8x5_KHR;
            if (p === RGBA_ASTC_8x6_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR : extension.COMPRESSED_RGBA_ASTC_8x6_KHR;
            if (p === RGBA_ASTC_8x8_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR : extension.COMPRESSED_RGBA_ASTC_8x8_KHR;
            if (p === RGBA_ASTC_10x5_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR : extension.COMPRESSED_RGBA_ASTC_10x5_KHR;
            if (p === RGBA_ASTC_10x6_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR : extension.COMPRESSED_RGBA_ASTC_10x6_KHR;
            if (p === RGBA_ASTC_10x8_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR : extension.COMPRESSED_RGBA_ASTC_10x8_KHR;
            if (p === RGBA_ASTC_10x10_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR : extension.COMPRESSED_RGBA_ASTC_10x10_KHR;
            if (p === RGBA_ASTC_12x10_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR : extension.COMPRESSED_RGBA_ASTC_12x10_KHR;
            if (p === RGBA_ASTC_12x12_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR : extension.COMPRESSED_RGBA_ASTC_12x12_KHR;
        } else {
            return null;
        }
    }

    // BPTC

    if (p === RGBA_BPTC_Format) {
        const extension = getExtension(gl, 'EXT_texture_compression_bptc');
        if (extension !== null) {
            if (p === RGBA_BPTC_Format) {
                return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT : extension.COMPRESSED_RGBA_BPTC_UNORM_EXT;
            }
        } else {
            return null;
        }
    }

    //

    if (p === UnsignedInt248Type) {
        return gl.UNSIGNED_INT_24_8;
    }
    if (p === RepeatWrapping) {
        return gl.REPEAT;
    }
    if (p === ClampToEdgeWrapping) {
        return gl.CLAMP_TO_EDGE;
    }
    if (p === NearestMipMapNearestFilter) {
        return gl.NEAREST_MIPMAP_LINEAR;
    }
    if (p === NearestMipMapLinearFilter) {
        return gl.NEAREST_MIPMAP_LINEAR;
    }
    if (p === LinearMipMapNearestFilter) {
        return gl.LINEAR_MIPMAP_NEAREST;
    }
    if (p === LinearMipMapLinearFilter) {
        return gl.LINEAR_MIPMAP_LINEAR;
    }
    if (p === NearestFilter) {
        return gl.NEAREST;
    }
    if (p === LinearFilter) {
        return gl.LINEAR;
    }

    return null;
}

const color$4 = new Uint8Array([0, 0, 0, 1]);

/**
 * @desc A low-level component that represents a 2D WebGL texture.
 *
 * @private
 */
class Texture2D {

    constructor({gl, target, format, type, wrapS, wrapT, wrapR, encoding, preloadColor, premultiplyAlpha, flipY}) {

        this.gl = gl;

        this.target = target || gl.TEXTURE_2D;
        this.format = format || RGBAFormat;
        this.type = type || UnsignedByteType;
        this.internalFormat = null;
        this.premultiplyAlpha = !!premultiplyAlpha;
        this.flipY = !!flipY;
        this.unpackAlignment = 4;
        this.wrapS = wrapS || RepeatWrapping;
        this.wrapT = wrapT || RepeatWrapping;
        this.wrapR = wrapR || RepeatWrapping;
        this.encoding = encoding || sRGBEncoding;
        this.texture = gl.createTexture();

        if (preloadColor) {
            this.setPreloadColor(preloadColor); // Prevents "there is no texture bound to the unit 0" error
        }

        this.allocated = true;
    }

    setPreloadColor(value) {
        if (!value) {
            color$4[0] = 0;
            color$4[1] = 0;
            color$4[2] = 0;
            color$4[3] = 255;
        } else {
            color$4[0] = Math.floor(value[0] * 255);
            color$4[1] = Math.floor(value[1] * 255);
            color$4[2] = Math.floor(value[2] * 255);
            color$4[3] = Math.floor((value[3] !== undefined ? value[3] : 1) * 255);
        }
        const gl = this.gl;
        gl.bindTexture(this.target, this.texture);
        if (this.target === gl.TEXTURE_CUBE_MAP) {
            const faces = [
                gl.TEXTURE_CUBE_MAP_POSITIVE_X,
                gl.TEXTURE_CUBE_MAP_NEGATIVE_X,
                gl.TEXTURE_CUBE_MAP_POSITIVE_Y,
                gl.TEXTURE_CUBE_MAP_NEGATIVE_Y,
                gl.TEXTURE_CUBE_MAP_POSITIVE_Z,
                gl.TEXTURE_CUBE_MAP_NEGATIVE_Z
            ];
            for (let i = 0, len = faces.length; i < len; i++) {
                gl.texImage2D(faces[i], 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, color$4);
            }
        } else {
            gl.texImage2D(this.target, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, color$4);
        }
        gl.bindTexture(this.target, null);
    }

    setTarget(target) {
        this.target = target || this.gl.TEXTURE_2D;
    }

    setImage(image, props = {}) {

        const gl = this.gl;

        if (props.format !== undefined) {
            this.format = props.format;
        }
        if (props.internalFormat !== undefined) {
            this.internalFormat = props.internalFormat;
        }
        if (props.encoding !== undefined) {
            this.encoding = props.encoding;
        }
        if (props.type !== undefined) {
            this.type = props.type;
        }
        if (props.flipY !== undefined) {
            this.flipY = props.flipY;
        }
        if (props.premultiplyAlpha !== undefined) {
            this.premultiplyAlpha = props.premultiplyAlpha;
        }
        if (props.unpackAlignment !== undefined) {
            this.unpackAlignment = props.unpackAlignment;
        }
        if (props.minFilter !== undefined) {
            this.minFilter = props.minFilter;
        }
        if (props.magFilter !== undefined) {
            this.magFilter = props.magFilter;
        }
        if (props.wrapS !== undefined) {
            this.wrapS = props.wrapS;
        }
        if (props.wrapT !== undefined) {
            this.wrapT = props.wrapT;
        }
        if (props.wrapR !== undefined) {
            this.wrapR = props.wrapR;
        }

        let generateMipMap = false;

        gl.bindTexture(this.target, this.texture);

        const bak1 = gl.getParameter(gl.UNPACK_FLIP_Y_WEBGL);
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, this.flipY);

        const bak2 = gl.getParameter(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL);
        gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, this.premultiplyAlpha);

        const bak3 = gl.getParameter(gl.UNPACK_ALIGNMENT);
        gl.pixelStorei(gl.UNPACK_ALIGNMENT, this.unpackAlignment);

        const bak4 = gl.getParameter(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL);        gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);

        const minFilter = convertConstant(gl, this.minFilter);
        gl.texParameteri(this.target, gl.TEXTURE_MIN_FILTER, minFilter);

        if (minFilter === gl.NEAREST_MIPMAP_NEAREST
            || minFilter === gl.LINEAR_MIPMAP_NEAREST
            || minFilter === gl.NEAREST_MIPMAP_LINEAR
            || minFilter === gl.LINEAR_MIPMAP_LINEAR) {
            generateMipMap = true;
        }

        const magFilter = convertConstant(gl, this.magFilter);
        if (magFilter) {
            gl.texParameteri(this.target, gl.TEXTURE_MAG_FILTER, magFilter);
        }

        const wrapS = convertConstant(gl, this.wrapS);
        if (wrapS) {
            gl.texParameteri(this.target, gl.TEXTURE_WRAP_S, wrapS);
        }

        const wrapT = convertConstant(gl, this.wrapT);
        if (wrapT) {
            gl.texParameteri(this.target, gl.TEXTURE_WRAP_T, wrapT);
        }

        const glFormat = convertConstant(gl, this.format, this.encoding);
        const glType = convertConstant(gl, this.type);
        const glInternalFormat = getInternalFormat(gl, this.internalFormat, glFormat, glType, this.encoding, false);

        if (this.target === gl.TEXTURE_CUBE_MAP) {
            if (utils.isArray(image)) {
                const images = image;
                const faces = [
                    gl.TEXTURE_CUBE_MAP_POSITIVE_X,
                    gl.TEXTURE_CUBE_MAP_NEGATIVE_X,
                    gl.TEXTURE_CUBE_MAP_POSITIVE_Y,
                    gl.TEXTURE_CUBE_MAP_NEGATIVE_Y,
                    gl.TEXTURE_CUBE_MAP_POSITIVE_Z,
                    gl.TEXTURE_CUBE_MAP_NEGATIVE_Z
                ];
                for (let i = 0, len = faces.length; i < len; i++) {
                    gl.texImage2D(faces[i], 0, glInternalFormat, glFormat, glType, images[i]);
                }
            }
        } else {
            gl.texImage2D(gl.TEXTURE_2D, 0, glInternalFormat, glFormat, glType, image);
        }

        if (generateMipMap) {
            gl.generateMipmap(this.target);
        }

        gl.bindTexture(this.target, null);

        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, bak1);
        gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, bak2);
        gl.pixelStorei(gl.UNPACK_ALIGNMENT, bak3);
        gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, bak4);
    }

    setCompressedData({mipmaps, props = {}}) {

        const gl = this.gl;
        const levels = mipmaps.length;

        // Cache props

        if (props.format !== undefined) {
            this.format = props.format;
        }
        if (props.internalFormat !== undefined) {
            this.internalFormat = props.internalFormat;
        }
        if (props.encoding !== undefined) {
            this.encoding = props.encoding;
        }
        if (props.type !== undefined) {
            this.type = props.type;
        }
        if (props.flipY !== undefined) {
            this.flipY = props.flipY;
        }
        if (props.premultiplyAlpha !== undefined) {
            this.premultiplyAlpha = props.premultiplyAlpha;
        }
        if (props.unpackAlignment !== undefined) {
            this.unpackAlignment = props.unpackAlignment;
        }
        if (props.minFilter !== undefined) {
            this.minFilter = props.minFilter;
        }
        if (props.magFilter !== undefined) {
            this.magFilter = props.magFilter;
        }
        if (props.wrapS !== undefined) {
            this.wrapS = props.wrapS;
        }
        if (props.wrapT !== undefined) {
            this.wrapT = props.wrapT;
        }
        if (props.wrapR !== undefined) {
            this.wrapR = props.wrapR;
        }

        gl.activeTexture(gl.TEXTURE0 + 0);
        gl.bindTexture(this.target, this.texture);

        let supportsMips = mipmaps.length > 1;

        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, this.flipY);
        gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, this.premultiplyAlpha);
        gl.pixelStorei(gl.UNPACK_ALIGNMENT, this.unpackAlignment);
        gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);

        const wrapS = convertConstant(gl, this.wrapS);
        if (wrapS) {
            gl.texParameteri(this.target, gl.TEXTURE_WRAP_S, wrapS);
        }

        const wrapT = convertConstant(gl, this.wrapT);
        if (wrapT) {
            gl.texParameteri(this.target, gl.TEXTURE_WRAP_T, wrapT);
        }

        if (this.type === gl.TEXTURE_3D || this.type === gl.TEXTURE_2D_ARRAY) {
            const wrapR = convertConstant(gl, this.wrapR);
            if (wrapR) {
                gl.texParameteri(this.target, gl.TEXTURE_WRAP_R, wrapR);
            }
            gl.texParameteri(this.type, gl.TEXTURE_WRAP_R, wrapR);
        }

        if (supportsMips) {
            gl.texParameteri(this.target, gl.TEXTURE_MIN_FILTER, filterFallback(gl, this.minFilter));
            gl.texParameteri(this.target, gl.TEXTURE_MAG_FILTER, filterFallback(gl, this.magFilter));

        } else {
            gl.texParameteri(this.target, gl.TEXTURE_MIN_FILTER, convertConstant(gl, this.minFilter));
            gl.texParameteri(this.target, gl.TEXTURE_MAG_FILTER, convertConstant(gl, this.magFilter));
        }

        const glFormat = convertConstant(gl, this.format, this.encoding);
        const glType = convertConstant(gl, this.type);
        const glInternalFormat = getInternalFormat(gl, this.internalFormat, glFormat, glType, this.encoding, false);

        gl.texStorage2D(gl.TEXTURE_2D, levels, glInternalFormat, mipmaps[0].width, mipmaps[0].height);

        for (let i = 0, len = mipmaps.length; i < len; i++) {

            const mipmap = mipmaps[i];

            if (this.format !== RGBAFormat) {
                if (glFormat !== null) {
                    gl.compressedTexSubImage2D(gl.TEXTURE_2D, i, 0, 0, mipmap.width, mipmap.height, glFormat, mipmap.data);
                } else {
                    console.warn('Attempt to load unsupported compressed texture format in .setCompressedData()');
                }
            } else {
                gl.texSubImage2D(gl.TEXTURE_2D, i, 0, 0, mipmap.width, mipmap.height, glFormat, glType, mipmap.data);
            }
        }

        //    if (generateMipMap) {
        // //       gl.generateMipmap(this.target); // Only for roughness textures?
        //    }

        gl.bindTexture(this.target, null);
    }

    setProps(props) {
        const gl = this.gl;
        gl.bindTexture(this.target, this.texture);
        this._uploadProps(props);
        gl.bindTexture(this.target, null);
    }

    _uploadProps(props) {
        const gl = this.gl;
        if (props.format !== undefined) {
            this.format = props.format;
        }
        if (props.internalFormat !== undefined) {
            this.internalFormat = props.internalFormat;
        }
        if (props.encoding !== undefined) {
            this.encoding = props.encoding;
        }
        if (props.type !== undefined) {
            this.type = props.type;
        }
        if (props.minFilter !== undefined) {
            const minFilter = convertConstant(gl, props.minFilter);
            if (minFilter) {
                this.minFilter = props.minFilter;
                gl.texParameteri(this.target, gl.TEXTURE_MIN_FILTER, minFilter);
                if (minFilter === gl.NEAREST_MIPMAP_NEAREST || minFilter === gl.LINEAR_MIPMAP_NEAREST || minFilter === gl.NEAREST_MIPMAP_LINEAR || minFilter === gl.LINEAR_MIPMAP_LINEAR) {
                    gl.generateMipmap(this.target);
                }
            }
        }
        if (props.magFilter !== undefined) {
            const magFilter = convertConstant(gl, props.magFilter);
            if (magFilter) {
                this.magFilter = props.magFilter;
                gl.texParameteri(this.target, gl.TEXTURE_MAG_FILTER, magFilter);
            }
        }
        if (props.wrapS !== undefined) {
            const wrapS = convertConstant(gl, props.wrapS);
            if (wrapS) {
                this.wrapS = props.wrapS;
                gl.texParameteri(this.target, gl.TEXTURE_WRAP_S, wrapS);
            }
        }
        if (props.wrapT !== undefined) {
            const wrapT = convertConstant(gl, props.wrapT);
            if (wrapT) {
                this.wrapT = props.wrapT;
                gl.texParameteri(this.target, gl.TEXTURE_WRAP_T, wrapT);
            }
        }
    }

    bind(unit) {
        if (!this.allocated) {
            return;
        }
        if (this.texture) {
            const gl = this.gl;
            gl.activeTexture(gl["TEXTURE" + unit]);
            gl.bindTexture(this.target, this.texture);
            return true;
        }
        return false;
    }

    unbind(unit) {
        if (!this.allocated) {
            return;
        }
        if (this.texture) {
            const gl = this.gl;
            gl.activeTexture(gl["TEXTURE" + unit]);
            gl.bindTexture(this.target, null);
        }
    }

    destroy() {
        if (!this.allocated) {
            return;
        }
        if (this.texture) {
            this.gl.deleteTexture(this.texture);
            this.texture = null;
        }
    }
}

function getInternalFormat(gl, internalFormatName, glFormat, glType, encoding, isVideoTexture = false) {
    if (internalFormatName !== null) {
        if (gl[internalFormatName] !== undefined) {
            return gl[internalFormatName];
        }
        console.warn('Attempt to use non-existing WebGL internal format \'' + internalFormatName + '\'');
    }
    let internalFormat = glFormat;
    if (glFormat === gl.RED) {
        if (glType === gl.FLOAT) internalFormat = gl.R32F;
        if (glType === gl.HALF_FLOAT) internalFormat = gl.R16F;
        if (glType === gl.UNSIGNED_BYTE) internalFormat = gl.R8;
    }
    if (glFormat === gl.RG) {
        if (glType === gl.FLOAT) internalFormat = gl.RG32F;
        if (glType === gl.HALF_FLOAT) internalFormat = gl.RG16F;
        if (glType === gl.UNSIGNED_BYTE) internalFormat = gl.RG8;
    }
    if (glFormat === gl.RGBA) {
        if (glType === gl.FLOAT) internalFormat = gl.RGBA32F;
        if (glType === gl.HALF_FLOAT) internalFormat = gl.RGBA16F;
        if (glType === gl.UNSIGNED_BYTE) internalFormat = (encoding === sRGBEncoding && isVideoTexture === false) ? gl.SRGB8_ALPHA8 : gl.RGBA8;
        if (glType === gl.UNSIGNED_SHORT_4_4_4_4) internalFormat = gl.RGBA4;
        if (glType === gl.UNSIGNED_SHORT_5_5_5_1) internalFormat = gl.RGB5_A1;
    }
    if (internalFormat === gl.R16F || internalFormat === gl.R32F ||
        internalFormat === gl.RG16F || internalFormat === gl.RG32F ||
        internalFormat === gl.RGBA16F || internalFormat === gl.RGBA32F) {
        getExtension(gl, 'EXT_color_buffer_float');
    }
    return internalFormat;
}

function filterFallback(gl, f) {
    if (f === NearestFilter || f === NearestMipmapNearestFilter || f === NearestMipmapLinearFilter) {
        return gl.NEAREST;
    }
    return gl.LINEAR;

}

function ensureImageSizePowerOfTwo$1(image) {
    if (!isPowerOfTwo$1(image.width) || !isPowerOfTwo$1(image.height)) {
        const canvas = document.createElement("canvas");
        canvas.width = nextHighestPowerOfTwo$1(image.width);
        canvas.height = nextHighestPowerOfTwo$1(image.height);
        const ctx = canvas.getContext("2d");
        ctx.drawImage(image,
            0, 0, image.width, image.height,
            0, 0, canvas.width, canvas.height);
        image = canvas;
    }
    return image;
}

function isPowerOfTwo$1(x) {
    return (x & (x - 1)) === 0;
}

function nextHighestPowerOfTwo$1(x) {
    --x;
    for (let i = 1; i < 32; i <<= 1) {
        x = x | x >> i;
    }
    return x + 1;
}

/**
 * @desc A 2D texture map.
 *
 * * Textures are attached to {@link Material}s, which are attached to {@link Mesh}es.
 * * To create a Texture from an image file, set {@link Texture#src} to the image file path.
 * * To create a Texture from an HTMLImageElement, set the Texture's {@link Texture#image} to the HTMLImageElement.
 *
 * ## Usage
 *
 * In this example we have a Mesh with a {@link PhongMaterial} which applies diffuse {@link Texture}, and a {@link buildTorusGeometry} which builds a {@link ReadableGeometry}.
 *
 * Note that xeokit will ignore {@link PhongMaterial#diffuse} and {@link PhongMaterial#specular}, since we override those
 * with {@link PhongMaterial#diffuseMap} and {@link PhongMaterial#specularMap}. The {@link Texture} pixel colors directly
 * provide the diffuse and specular components for each fragment across the {@link ReadableGeometry} surface.
 *
 * [[Run this example](/examples/index.html#materials_Texture)]
 *
 * ```` javascript
 * import {Viewer, Mesh, buildTorusGeometry,
 *      ReadableGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *      canvasId: "myCanvas"
 * });
 *
 * viewer.camera.eye = [0, 0, 5];
 * viewer.camera.look = [0, 0, 0];
 * viewer.camera.up = [0, 1, 0];
 *
 * new Mesh(viewer.scene, {
 *      geometry: new ReadableGeometry(viewer.scene, buildTorusGeometry({
 *          center: [0, 0, 0],
 *          radius: 1.5,
 *          tube: 0.5,
 *          radialSegments: 32,
 *          tubeSegments: 24,
 *          arc: Math.PI * 2.0
 *      }),
 *      material: new PhongMaterial(viewer.scene, {
 *          ambient: [0.9, 0.3, 0.9],
 *          shininess: 30,
 *          diffuseMap: new Texture(viewer.scene, {
 *              src: "textures/diffuse/uvGrid2.jpg"
 *          })
 *      })
 * });
 *````
 */
class Texture extends Component {

    /**
     @private
     */
    get type() {
        return "Texture";
    }

    /**
     * @constructor
     * @param {Component} owner Owner component. When destroyed, the owner will destroy this Texture as well.
     * @param {*} [cfg] Configs
     * @param {String} [cfg.id] Optional ID for this Texture, unique among all components in the parent scene, generated automatically when omitted.
     * @param {String} [cfg.src=null] Path to image file to load into this Texture. See the {@link Texture#src} property for more info.
     * @param {HTMLImageElement} [cfg.image=null] HTML Image object to load into this Texture. See the {@link Texture#image} property for more info.
     * @param {Number} [cfg.minFilter=LinearMipmapLinearFilter] How the texture is sampled when a texel covers less than one pixel.
     * Supported values are {@link LinearMipmapLinearFilter}, {@link LinearMipMapNearestFilter}, {@link NearestMipMapNearestFilter}, {@link NearestMipMapLinearFilter} and {@link LinearMipMapLinearFilter}.
     * @param {Number} [cfg.magFilter=LinearFilter] How the texture is sampled when a texel covers more than one pixel. Supported values are {@link LinearFilter} and {@link NearestFilter}.
     * @param {Number} [cfg.wrapS=RepeatWrapping] Wrap parameter for texture coordinate *S*. Supported values are {@link ClampToEdgeWrapping}, {@link MirroredRepeatWrapping} and {@link RepeatWrapping}.
     * @param {Number} [cfg.wrapT=RepeatWrapping] Wrap parameter for texture coordinate *T*. Supported values are {@link ClampToEdgeWrapping}, {@link MirroredRepeatWrapping} and {@link RepeatWrapping}..
     * @param {Boolean} [cfg.flipY=false] Flips this Texture's source data along its vertical axis when ````true````.
     * @param  {Number} [cfg.encoding=LinearEncoding] Encoding format. Supported values are {@link LinearEncoding} and {@link sRGBEncoding}.
     * @param {Number[]} [cfg.translate=[0,0]] 2D translation vector that will be added to texture's *S* and *T* coordinates.
     * @param {Number[]} [cfg.scale=[1,1]] 2D scaling vector that will be applied to texture's *S* and *T* coordinates.
     * @param {Number} [cfg.rotate=0] Rotation, in degrees, that will be applied to texture's *S* and *T* coordinates.
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._state = new RenderState({
            texture: new Texture2D({gl: this.scene.canvas.gl}),
            matrix: math.identityMat4(),
            hasMatrix: (cfg.translate && (cfg.translate[0] !== 0 || cfg.translate[1] !== 0)) || (!!cfg.rotate) || (cfg.scale && (cfg.scale[0] !== 0 || cfg.scale[1] !== 0)),
            minFilter: this._checkMinFilter(cfg.minFilter),
            magFilter: this._checkMagFilter(cfg.magFilter),
            wrapS: this._checkWrapS(cfg.wrapS),
            wrapT: this._checkWrapT(cfg.wrapT),
            flipY: this._checkFlipY(cfg.flipY),
            encoding: this._checkEncoding(cfg.encoding)
        });

        // Data source

        this._src = null;
        this._image = null;

        // Transformation

        this._translate = math.vec2([0, 0]);
        this._scale = math.vec2([1, 1]);
        this._rotate = math.vec2([0, 0]);

        this._matrixDirty = false;

        // Transform

        this.translate = cfg.translate;
        this.scale = cfg.scale;
        this.rotate = cfg.rotate;

        // Data source

        if (cfg.src) {
            this.src = cfg.src; // Image file
        } else if (cfg.image) {
            this.image = cfg.image; // Image object
        }

        stats.memory.textures++;
    }

    _checkMinFilter(value) {
        value = value || LinearMipMapLinearFilter;
        if (value !== LinearFilter &&
            value !== LinearMipMapNearestFilter &&
            value !== LinearMipMapLinearFilter &&
            value !== NearestMipMapLinearFilter &&
            value !== NearestMipMapNearestFilter) {
            this.error("Unsupported value for 'minFilter' - supported values are LinearFilter, LinearMipMapNearestFilter, NearestMipMapNearestFilter, " +
                "NearestMipMapLinearFilter and LinearMipMapLinearFilter. Defaulting to LinearMipMapLinearFilter.");
            value = LinearMipMapLinearFilter;
        }
        return value;
    }

    _checkMagFilter(value) {
        value = value || LinearFilter;
        if (value !== LinearFilter && value !== NearestFilter) {
            this.error("Unsupported value for 'magFilter' - supported values are LinearFilter and NearestFilter. Defaulting to LinearFilter.");
            value = LinearFilter;
        }
        return value;
    }

    _checkWrapS(value) {
        value = value || RepeatWrapping;
        if (value !== ClampToEdgeWrapping && value !== MirroredRepeatWrapping && value !== RepeatWrapping) {
            this.error("Unsupported value for 'wrapS' - supported values are ClampToEdgeWrapping, MirroredRepeatWrapping and RepeatWrapping. Defaulting to RepeatWrapping.");
            value = RepeatWrapping;
        }
        return value;
    }

    _checkWrapT(value) {
        value = value || RepeatWrapping;
        if (value !== ClampToEdgeWrapping && value !== MirroredRepeatWrapping && value !== RepeatWrapping) {
            this.error("Unsupported value for 'wrapT' - supported values are ClampToEdgeWrapping, MirroredRepeatWrapping and RepeatWrapping. Defaulting to RepeatWrapping.");
            value = RepeatWrapping;
        }
        return value;
    }

    _checkFlipY(value) {
        return !!value;
    }

    _checkEncoding(value) {
        value = value || LinearEncoding;
        if (value !== LinearEncoding && value !== sRGBEncoding) {
            this.error("Unsupported value for 'encoding' - supported values are LinearEncoding and sRGBEncoding. Defaulting to LinearEncoding.");
            value = LinearEncoding;
        }
        return value;
    }

    _webglContextRestored() {
        this._state.texture = new Texture2D({gl: this.scene.canvas.gl});
        if (this._image) {
            this.image = this._image;
        } else if (this._src) {
            this.src = this._src;
        }
    }

    _update() {
        const state = this._state;
        if (this._matrixDirty) {
            let matrix;
            let t;
            if (this._translate[0] !== 0 || this._translate[1] !== 0) {
                matrix = math.translationMat4v([this._translate[0], this._translate[1], 0], this._state.matrix);
            }
            if (this._scale[0] !== 1 || this._scale[1] !== 1) {
                t = math.scalingMat4v([this._scale[0], this._scale[1], 1]);
                matrix = matrix ? math.mulMat4(matrix, t) : t;
            }
            if (this._rotate !== 0) {
                t = math.rotationMat4v(this._rotate * 0.0174532925, [0, 0, 1]);
                matrix = matrix ? math.mulMat4(matrix, t) : t;
            }
            if (matrix) {
                state.matrix = matrix;
            }
            this._matrixDirty = false;
        }
        this.glRedraw();
    }


    /**
     * Sets an HTML DOM Image object to source this Texture from.
     *
     * Sets {@link Texture#src} null.
     *
     * @type {HTMLImageElement}
     */
    set image(value) {
        this._image = ensureImageSizePowerOfTwo$1(value);
        this._image.crossOrigin = "Anonymous";
        this._state.texture.setImage(this._image, this._state);
        this._src = null;
        this.glRedraw();
    }

    /**
     * Gets HTML DOM Image object this Texture is sourced from, if any.
     *
     * Returns null if not set.
     *
     * @type {HTMLImageElement}
     */
    get image() {
        return this._image;
    }

    /**
     * Sets path to an image file to source this Texture from.
     *
     * Sets {@link Texture#image} null.
     *
     * @type {String}
     */
    set src(src) {
        this.scene.loading++;
        this.scene.canvas.spinner.processes++;
        const self = this;
        let image = new Image();
        image.onload = function () {
            image = ensureImageSizePowerOfTwo$1(image);
            self._state.texture.setImage(image, self._state);
            self.scene.loading--;
            self.glRedraw();
            self.scene.canvas.spinner.processes--;
        };
        image.src = src;
        this._src = src;
        this._image = null;
    }

    /**
     * Gets path to the image file this Texture from, if any.
     *
     * Returns null if not set.
     *
     * @type {String}
     */
    get src() {
        return this._src;
    }

    /**
     * Sets the 2D translation vector added to this Texture's *S* and *T* UV coordinates.
     *
     * Default value is ````[0, 0]````.
     *
     * @type {Number[]}
     */
    set translate(value) {
        this._translate.set(value || [0, 0]);
        this._matrixDirty = true;
        this._needUpdate();
    }

    /**
     * Gets the 2D translation vector added to this Texture's *S* and *T* UV coordinates.
     *
     * Default value is ````[0, 0]````.
     *
     * @type {Number[]}
     */
    get translate() {
        return this._translate;
    }

    /**
     * Sets the 2D scaling vector that will be applied to this Texture's *S* and *T* UV coordinates.
     *
     * Default value is ````[1, 1]````.
     *
     * @type {Number[]}
     */
    set scale(value) {
        this._scale.set(value || [1, 1]);
        this._matrixDirty = true;
        this._needUpdate();
    }

    /**
     * Gets the 2D scaling vector that will be applied to this Texture's *S* and *T* UV coordinates.
     *
     * Default value is ````[1, 1]````.
     *
     * @type {Number[]}
     */
    get scale() {
        return this._scale;
    }

    /**
     * Sets the rotation angles, in degrees, that will be applied to this Texture's *S* and *T* UV coordinates.
     *
     * Default value is ````0````.
     *
     * @type {Number}
     */
    set rotate(value) {
        value = value || 0;
        if (this._rotate === value) {
            return;
        }
        this._rotate = value;
        this._matrixDirty = true;
        this._needUpdate();
    }

    /**
     * Gets the rotation angles, in degrees, that will be applied to this Texture's *S* and *T* UV coordinates.
     *
     * Default value is ````0````.
     *
     * @type {Number}
     */
    get rotate() {
        return this._rotate;
    }

    /**
     * Gets how this Texture is sampled when a texel covers less than one pixel.
     *
     * Options are:
     *
     * * NearestFilter - Uses the value of the texture element that is nearest
     * (in Manhattan distance) to the center of the pixel being textured.
     *
     * * LinearFilter - Uses the weighted average of the four texture elements that are
     * closest to the center of the pixel being textured.
     *
     * * NearestMipMapNearestFilter - Chooses the mipmap that most closely matches the
     * size of the pixel being textured and uses the "nearest" criterion (the texture
     * element nearest to the center of the pixel) to produce a texture value.
     *
     * * LinearMipMapNearestFilter - Chooses the mipmap that most closely matches the size of
     * the pixel being textured and uses the "linear" criterion (a weighted average of the
     * four texture elements that are closest to the center of the pixel) to produce a
     * texture value.
     *
     * * NearestMipMapLinearFilter - Chooses the two mipmaps that most closely
     * match the size of the pixel being textured and uses the "nearest" criterion
     * (the texture element nearest to the center of the pixel) to produce a texture
     * value from each mipmap. The final texture value is a weighted average of those two
     * values.
     *
     * * LinearMipMapLinearFilter - (default) - Chooses the two mipmaps that most closely match the size
     * of the pixel being textured and uses the "linear" criterion (a weighted average
     * of the four texture elements that are closest to the center of the pixel) to
     * produce a texture value from each mipmap. The final texture value is a weighted
     * average of those two values.
     *
     * Default value is LinearMipMapLinearFilter.
     *
     *  @type {Number}
     */
    get minFilter() {
        return this._state.minFilter;
    }

    /**
     * Gets how this Texture is sampled when a texel covers more than one pixel.
     *
     * * NearestFilter - Uses the value of the texture element that is nearest
     * (in Manhattan distance) to the center of the pixel being textured.
     * * LinearFilter - (default) - Uses the weighted average of the four texture elements that are
     * closest to the center of the pixel being textured.
     *
     * Default value is LinearMipMapLinearFilter.
     *
     * @type {Number}
     */
    get magFilter() {
        return this._state.magFilter;
    }

    /**
     * Gets the wrap parameter for this Texture's *S* coordinate.
     *
     * Values can be:
     *
     * * ClampToEdgeWrapping -  causes *S* coordinates to be clamped to the size of the texture.
     * * MirroredRepeatWrapping - causes the *S* coordinate to be set to the fractional part of the texture coordinate
     * if the integer part of *S* is even; if the integer part of *S* is odd, then the *S* texture coordinate is
     * set to *1 - frac ⁡ S* , where *frac ⁡ S* represents the fractional part of *S*.
     * * RepeatWrapping - (default) - causes the integer part of the *S* coordinate to be ignored; xeokit uses only the
     * fractional part, thereby creating a repeating pattern.
     *
     * Default value is RepeatWrapping.
     *
     * @type {Number}
     */
    get wrapS() {
        return this._state.wrapS;
    }

    /**
     * Gets the wrap parameter for this Texture's *T* coordinate.
     *
     * Values can be:
     *
     * * ClampToEdgeWrapping -  causes *S* coordinates to be clamped to the size of the texture.
     *  * MirroredRepeatWrapping - causes the *S* coordinate to be set to the fractional part of the texture coordinate
     * if the integer part of *S* is even; if the integer part of *S* is odd, then the *S* texture coordinate is
     * set to *1 - frac ⁡ S* , where *frac ⁡ S* represents the fractional part of *S*.
     * * RepeatWrapping - (default) - causes the integer part of the *S* coordinate to be ignored; xeokit uses only the
     * fractional part, thereby creating a repeating pattern.
     *
     * Default value is RepeatWrapping.
     *
     * @type {Number}
     */
    get wrapT() {
        return this._state.wrapT;
    }

    /**
     * Gets if this Texture's source data is flipped along its vertical axis.
     *
     * @type {Number}
     */
    get flipY() {
        return this._state.flipY;
    }

    /**
     * Gets the Texture's encoding format.
     *
     * @type {Number}
     */
    get encoding() {
        return this._state.encoding;
    }

    /**
     * Destroys this Texture
     */
    destroy() {
        super.destroy();
        if (this._state.texture) {
            this._state.texture.destroy();
        }
        this._state.destroy();
        stats.memory.textures--;
    }
}

math.vec3();
math.vec3();
math.vec3();
math.vec3([0, -1, 0]);
math.vec4([0, 0, 0, 1]);

const PRESETS$3 = {
    "default": {
        fill: true,
        fillColor: [0.4, 0.4, 0.4],
        fillAlpha: 0.2,
        edges: true,
        edgeColor: [0.2, 0.2, 0.2],
        edgeAlpha: 0.5,
        edgeWidth: 1
    },
    "defaultWhiteBG": {
        fill: true,
        fillColor: [1, 1, 1],
        fillAlpha: 0.6,
        edgeColor: [0.2, 0.2, 0.2],
        edgeAlpha: 1.0,
        edgeWidth: 1
    },
    "defaultLightBG": {
        fill: true,
        fillColor: [0.4, 0.4, 0.4],
        fillAlpha: 0.2,
        edges: true,
        edgeColor: [0.2, 0.2, 0.2],
        edgeAlpha: 0.5,
        edgeWidth: 1
    },
    "defaultDarkBG": {
        fill: true,
        fillColor: [0.4, 0.4, 0.4],
        fillAlpha: 0.2,
        edges: true,
        edgeColor: [0.5, 0.5, 0.5],
        edgeAlpha: 0.5,
        edgeWidth: 1
    },
    "phosphorous": {
        fill: true,
        fillColor: [0.0, 0.0, 0.0],
        fillAlpha: 0.4,
        edges: true,
        edgeColor: [0.9, 0.9, 0.9],
        edgeAlpha: 0.5,
        edgeWidth: 2
    },
    "sunset": {
        fill: true,
        fillColor: [0.9, 0.9, 0.6],
        fillAlpha: 0.2,
        edges: true,
        edgeColor: [0.9, 0.9, 0.9],
        edgeAlpha: 0.5,
        edgeWidth: 1
    },
    "vectorscope": {
        fill: true,
        fillColor: [0.0, 0.0, 0.0],
        fillAlpha: 0.7,
        edges: true,
        edgeColor: [0.2, 1.0, 0.2],
        edgeAlpha: 1,
        edgeWidth: 2
    },
    "battlezone": {
        fill: true,
        fillColor: [0.0, 0.0, 0.0],
        fillAlpha: 1.0,
        edges: true,
        edgeColor: [0.2, 1.0, 0.2],
        edgeAlpha: 1,
        edgeWidth: 3
    },
    "sepia": {
        fill: true,
        fillColor: [0.970588207244873, 0.7965892553329468, 0.6660899519920349],
        fillAlpha: 0.4,
        edges: true,
        edgeColor: [0.529411792755127, 0.4577854573726654, 0.4100345969200134],
        edgeAlpha: 1.0,
        edgeWidth: 1
    },
    "yellowHighlight": {
        fill: true,
        fillColor: [1.0, 1.0, 0.0],
        fillAlpha: 0.5,
        edges: true,
        edgeColor: [1.0, 1.0, 1.0],
        edgeAlpha: 1.0,
        edgeWidth: 1
    },
    "greenSelected": {
        fill: true,
        fillColor: [0.0, 1.0, 0.0],
        fillAlpha: 0.5,
        edges: true,
        edgeColor: [1.0, 1.0, 1.0],
        edgeAlpha: 1.0,
        edgeWidth: 1
    },
    "gamegrid": {
        fill: true,
        fillColor: [0.2, 0.2, 0.7],
        fillAlpha: 0.9,
        edges: true,
        edgeColor: [0.4, 0.4, 1.6],
        edgeAlpha: 0.8,
        edgeWidth: 3
    }
};

/**
 * Configures the appearance of {@link Entity}s when they are xrayed, highlighted or selected.
 *
 * * XRay an {@link Entity} by setting {@link Entity#xrayed} ````true````.
 * * Highlight an {@link Entity} by setting {@link Entity#highlighted} ````true````.
 * * Select an {@link Entity} by setting {@link Entity#selected} ````true````.
 * * When {@link Entity}s are within the subtree of a root {@link Entity}, then setting {@link Entity#xrayed}, {@link Entity#highlighted} or {@link Entity#selected}
 * on the root will collectively set those properties on all sub-{@link Entity}s.
 * * EmphasisMaterial provides several presets. Select a preset by setting {@link EmphasisMaterial#preset} to the ID of a preset in {@link EmphasisMaterial#presets}.
 * * By default, a {@link Mesh} uses the default EmphasisMaterials in {@link Scene#xrayMaterial}, {@link Scene#highlightMaterial} and {@link Scene#selectedMaterial}
 * but you can assign each {@link Mesh#xrayMaterial}, {@link Mesh#highlightMaterial} or {@link Mesh#selectedMaterial} to a custom EmphasisMaterial, if required.
 *
 * ## Usage
 *
 * In the example below, we'll create a {@link Mesh} with its own XRayMaterial and set {@link Mesh#xrayed} ````true```` to xray it.
 *
 * Recall that {@link Mesh} is a concrete subtype of the abstract {@link Entity} base class.
 *
 * ````javascript
 * new Mesh(viewer.scene, {
 *     geometry: new BoxGeometry(viewer.scene, {
 *         edgeThreshold: 1
 *     }),
 *     material: new PhongMaterial(viewer.scene, {
 *         diffuse: [0.2, 0.2, 1.0]
 *     }),
 *     xrayMaterial: new EmphasisMaterial(viewer.scene, {
 *         fill: true,
 *         fillColor: [0, 0, 0],
 *         fillAlpha: 0.7,
 *         edges: true,
 *         edgeColor: [0.2, 1.0, 0.2],
 *         edgeAlpha: 1.0,
 *         edgeWidth: 2
 *     }),
 *     xrayed: true
 * });
 * ````
 *
 * Note the ````edgeThreshold```` configuration for the {@link ReadableGeometry} on our {@link Mesh}.  EmphasisMaterial configures
 * a wireframe representation of the {@link ReadableGeometry} for the selected emphasis mode, which will have inner edges (those edges between
 * adjacent co-planar triangles) removed for visual clarity. The ````edgeThreshold```` indicates that, for
 * this particular {@link ReadableGeometry}, an inner edge is one where the angle between the surface normals of adjacent triangles
 * is not greater than ````5```` degrees. That's set to ````2```` by default, but we can override it to tweak the effect
 * as needed for particular Geometries.
 *
 * Here's the example again, this time implicitly defaulting to the {@link Scene#edgeMaterial}. We'll also modify that EdgeMaterial
 * to customize the effect.
 *
 * ````javascript
 * new Mesh({
 *     geometry: new TeapotGeometry(viewer.scene, {
 *         edgeThreshold: 5
 *     }),
 *     material: new PhongMaterial(viewer.scene, {
 *         diffuse: [0.2, 0.2, 1.0]
 *     }),
 *     xrayed: true
 * });
 *
 * var xrayMaterial = viewer.scene.xrayMaterial;
 *
 * xrayMaterial.fillColor = [0.2, 1.0, 0.2];
 * xrayMaterial.fillAlpha = 1.0;
 * ````
 *
 * ## Presets
 *
 * Let's switch the {@link Scene#xrayMaterial} to one of the presets in {@link EmphasisMaterial#presets}:
 *
 * ````javascript
 * viewer.xrayMaterial.preset = EmphasisMaterial.presets["sepia"];
 * ````
 *
 * We can also create an EmphasisMaterial from a preset, while overriding properties of the preset as required:
 *
 * ````javascript
 * var myEmphasisMaterial = new EMphasisMaterial(viewer.scene, {
 *      preset: "sepia",
 *      fillColor = [1.0, 0.5, 0.5]
 * });
 * ````
 */
class EmphasisMaterial extends Material {

    /**
     @private
     */
    get type() {
        return "EmphasisMaterial";
    }

    /**
     * Gets available EmphasisMaterial presets.
     *
     * @type {Object}
     */
    get presets() {
        return PRESETS$3;
    };

    /**
     * @constructor
     * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well.
     * @param {*} [cfg] The EmphasisMaterial configuration
     * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted.
     * @param {Boolean} [cfg.fill=true] Indicates if xray surfaces are filled with color.
     * @param {Number[]} [cfg.fillColor=[0.4,0.4,0.4]] EmphasisMaterial fill color.
     * @param  {Number} [cfg.fillAlpha=0.2] Transparency of filled xray faces. A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque.
     * @param {Boolean} [cfg.edges=true] Indicates if xray edges are visible.
     * @param {Number[]} [cfg.edgeColor=[0.2,0.2,0.2]]  RGB color of xray edges.
     * @param {Number} [cfg.edgeAlpha=0.5] Transparency of xray edges. A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque.
     * @param {Number} [cfg.edgeWidth=1] Width of xray edges, in pixels.
     * @param {String} [cfg.preset] Deprecated - will be removed in future releases - Selects a preset EmphasisMaterial configuration - see {@link EmphasisMaterial#presets}.
     * @param {Boolean} [cfg.backfaces=false] Whether to render geometry backfaces when emphasising.
     * @param {Boolean} [cfg.glowThrough=true] Whether to make the emphasized object appear to float on top of other objects, as if it were "glowing through" them.
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._state = new RenderState({
            type: "EmphasisMaterial",
            fill: null,
            fillColor: null,
            fillAlpha: null,
            edges: null,
            edgeColor: null,
            edgeAlpha: null,
            edgeWidth: null,
            backfaces: true,
            glowThrough: true
        });

        this._preset = "default";

        if (cfg.preset) { // Apply preset then override with configs where provided
            this.preset = cfg.preset;
            if (cfg.fill !== undefined) {
                this.fill = cfg.fill;
            }
            if (cfg.fillColor) {
                this.fillColor = cfg.fillColor;
            }
            if (cfg.fillAlpha !== undefined) {
                this.fillAlpha = cfg.fillAlpha;
            }
            if (cfg.edges !== undefined) {
                this.edges = cfg.edges;
            }
            if (cfg.edgeColor) {
                this.edgeColor = cfg.edgeColor;
            }
            if (cfg.edgeAlpha !== undefined) {
                this.edgeAlpha = cfg.edgeAlpha;
            }
            if (cfg.edgeWidth !== undefined) {
                this.edgeWidth = cfg.edgeWidth;
            }
            if (cfg.backfaces !== undefined) {
                this.backfaces = cfg.backfaces;
            }
            if (cfg.glowThrough !== undefined) {
                this.glowThrough = cfg.glowThrough;
            }
        } else {
            this.fill = cfg.fill;
            this.fillColor = cfg.fillColor;
            this.fillAlpha = cfg.fillAlpha;
            this.edges = cfg.edges;
            this.edgeColor = cfg.edgeColor;
            this.edgeAlpha = cfg.edgeAlpha;
            this.edgeWidth = cfg.edgeWidth;
            this.backfaces = cfg.backfaces;
            this.glowThrough = cfg.glowThrough;
        }
    }

    /**
     * Sets if surfaces are filled with color.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    set fill(value) {
        value = value !== false;
        if (this._state.fill === value) {
            return;
        }
        this._state.fill = value;
        this.glRedraw();
    }

    /**
     * Gets if surfaces are filled with color.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    get fill() {
        return this._state.fill;
    }

    /**
     * Sets the RGB color of filled faces.
     *
     * Default is ````[0.4, 0.4, 0.4]````.
     *
     * @type {Number[]}
     */
    set fillColor(value) {
        let fillColor = this._state.fillColor;
        if (!fillColor) {
            fillColor = this._state.fillColor = new Float32Array(3);
        } else if (value && fillColor[0] === value[0] && fillColor[1] === value[1] && fillColor[2] === value[2]) {
            return;
        }
        if (value) {
            fillColor[0] = value[0];
            fillColor[1] = value[1];
            fillColor[2] = value[2];
        } else {
            fillColor[0] = 0.4;
            fillColor[1] = 0.4;
            fillColor[2] = 0.4;
        }
        this.glRedraw();
    }

    /**
     * Gets the RGB color of filled faces.
     *
     * Default is ````[0.4, 0.4, 0.4]````.
     *
     * @type {Number[]}
     */
    get fillColor() {
        return this._state.fillColor;
    }

    /**
     * Sets the transparency of filled faces.
     *
     * A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque.
     *
     * Default is ````0.2````.
     *
     * @type {Number}
     */
    set fillAlpha(value) {
        value = (value !== undefined && value !== null) ? value : 0.2;
        if (this._state.fillAlpha === value) {
            return;
        }
        this._state.fillAlpha = value;
        this.glRedraw();
    }

    /**
     * Gets the transparency of filled faces.
     *
     * A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque.
     *
     * Default is ````0.2````.
     *
     * @type {Number}
     */
    get fillAlpha() {
        return this._state.fillAlpha;
    }

    /**
     * Sets if edges are visible.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    set edges(value) {
        value = value !== false;
        if (this._state.edges === value) {
            return;
        }
        this._state.edges = value;
        this.glRedraw();
    }

    /**
     * Gets if edges are visible.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    get edges() {
        return this._state.edges;
    }

    /**
     * Sets the RGB color of edges.
     *
     * Default is ```` [0.2, 0.2, 0.2]````.
     *
     * @type {Number[]}
     */
    set edgeColor(value) {
        let edgeColor = this._state.edgeColor;
        if (!edgeColor) {
            edgeColor = this._state.edgeColor = new Float32Array(3);
        } else if (value && edgeColor[0] === value[0] && edgeColor[1] === value[1] && edgeColor[2] === value[2]) {
            return;
        }
        if (value) {
            edgeColor[0] = value[0];
            edgeColor[1] = value[1];
            edgeColor[2] = value[2];
        } else {
            edgeColor[0] = 0.2;
            edgeColor[1] = 0.2;
            edgeColor[2] = 0.2;
        }
        this.glRedraw();
    }

    /**
     * Gets the RGB color of edges.
     *
     * Default is ```` [0.2, 0.2, 0.2]````.
     *
     * @type {Number[]}
     */
    get edgeColor() {
        return this._state.edgeColor;
    }

    /**
     * Sets the transparency of edges.
     *
     * A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque.
     *
     * Default is ````0.2````.
     *
     * @type {Number}
     */
    set edgeAlpha(value) {
        value = (value !== undefined && value !== null) ? value : 0.5;
        if (this._state.edgeAlpha === value) {
            return;
        }
        this._state.edgeAlpha = value;
        this.glRedraw();
    }

    /**
     * Gets the transparency of edges.
     *
     * A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque.
     *
     * Default is ````0.2````.
     *
     * @type {Number}
     */
    get edgeAlpha() {
        return this._state.edgeAlpha;
    }

    /**
     * Sets edge width.
     *
     * This is not supported by WebGL implementations based on DirectX [2019].
     *
     * Default value is ````1.0```` pixels.
     *
     * @type {Number}
     */
    set edgeWidth(value) {
        this._state.edgeWidth = value || 1.0;
        this.glRedraw();
    }

    /**
     * Gets edge width.
     *
     * This is not supported by WebGL implementations based on DirectX [2019].
     *
     * Default value is ````1.0```` pixels.
     *
     * @type {Number}
     */
    get edgeWidth() {
        return this._state.edgeWidth;
    }

    /**
     * Sets whether to render backfaces when {@link EmphasisMaterial#fill} is ````true````.
     *
     * Default is ````false````.
     *
     * @type {Boolean}
     */
    set backfaces(value) {
        value = !!value;
        if (this._state.backfaces === value) {
            return;
        }
        this._state.backfaces = value;
        this.glRedraw();
    }

    /**
     * Gets whether to render backfaces when {@link EmphasisMaterial#fill} is ````true````.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    get backfaces() {
        return this._state.backfaces;
    }

    /**
     * Sets whether to render emphasized objects over the top of other objects, as if they were "glowing through".
     *
     * Default is ````true````.
     *
     * Note: updating this property will not affect the appearance of objects that are already emphasized.
     *
     * @type {Boolean}
     */
    set glowThrough(value) {
        value = (value !== false);
        if (this._state.glowThrough === value) {
            return;
        }
        this._state.glowThrough = value;
        this.glRedraw();
    }

    /**
     * Sets whether to render emphasized objects over the top of other objects, as if they were "glowing through".
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    get glowThrough() {
        return this._state.glowThrough;
    }

    /**
     * @deprecated
     * Selects a preset EmphasisMaterial configuration.
     *
     * Default value is "default".
     *
     * @type {String}
     */
    set preset(value) {
        console.warn("EmphasisMaterial.preset is deprecated and will be removed in future versions.");
        value = value || "default";
        if (this._preset === value) {
            return;
        }
        const preset = PRESETS$3[value];
        if (!preset) {
            this.error("unsupported preset: '" + value + "' - supported values are " + Object.keys(PRESETS$3).join(", "));
            return;
        }
        this.fill = preset.fill;
        this.fillColor = preset.fillColor;
        this.fillAlpha = preset.fillAlpha;
        this.edges = preset.edges;
        this.edgeColor = preset.edgeColor;
        this.edgeAlpha = preset.edgeAlpha;
        this.edgeWidth = preset.edgeWidth;
        this.glowThrough = preset.glowThrough;
        this._preset = value;
    }

    /**
     * @deprecated
     * Gets the current preset EmphasisMaterial configuration.
     *
     * Default value is "default".
     *
     * @type {String}
     */
    get preset() {
        console.warn("EmphasisMaterial.preset is deprecated and will be removed in future versions.");
        return this._preset;
    }

    /**
     * Destroys this EmphasisMaterial.
     */
    destroy() {
        super.destroy();
        this._state.destroy();
    }
}

const PRESETS$2 = {
    "default": {
        edgeColor: [0.0, 0.0, 0.0],
        edgeAlpha: 1.0,
        edgeWidth: 1
    },
    "defaultWhiteBG": {
        edgeColor: [0.2, 0.2, 0.2],
        edgeAlpha: 1.0,
        edgeWidth: 1
    },
    "defaultLightBG": {
        edgeColor: [0.2, 0.2, 0.2],
        edgeAlpha: 1.0,
        edgeWidth: 1
    },
    "defaultDarkBG": {
        edgeColor: [0.5, 0.5, 0.5],
        edgeAlpha: 1.0,
        edgeWidth: 1
    }
};

/**
 * @desc Configures the appearance of {@link Entity}s when their edges are emphasised.
 *
 * * Emphasise edges of an {@link Entity} by setting {@link Entity#edges} ````true````.
 * * When {@link Entity}s are within the subtree of a root {@link Entity}, then setting {@link Entity#edges} on the root
 * will collectively set that property on all sub-{@link Entity}s.
 * * EdgeMaterial provides several presets. Select a preset by setting {@link EdgeMaterial#preset} to the ID of a preset in {@link EdgeMaterial#presets}.
 * * By default, a {@link Mesh} uses the default EdgeMaterial in {@link Scene#edgeMaterial}, but you can assign each {@link Mesh#edgeMaterial} to a custom EdgeMaterial if required.
 *
 * ## Usage
 *
 * In the example below, we'll create a {@link Mesh} with its own EdgeMaterial and set {@link Mesh#edges} ````true```` to emphasise its edges.
 *
 * Recall that {@link Mesh} is a concrete subtype of the abstract {@link Entity} base class.
 *
 * [[Run this example](/examples/index.html#materials_EdgeMaterial)]
 *
 * ````javascript
 * import {Viewer, Mesh, buildSphereGeometry,
 *      ReadableGeometry, PhongMaterial, EdgeMaterial} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *      canvasId: "myCanvas",
 *      transparent: true
 * });
 *
 * viewer.scene.camera.eye = [0, 0, 5];
 * viewer.scene.camera.look = [0, 0, 0];
 * viewer.scene.camera.up = [0, 1, 0];
 *
 * new Mesh(viewer.scene, {
 *
 *      geometry: new ReadableGeometry(viewer.scene, buildSphereGeometry({
 *          radius: 1.5,
 *          heightSegments: 24,
 *          widthSegments: 16,
 *          edgeThreshold: 2 // Default is 10
 *      })),
 *
 *      material: new PhongMaterial(viewer.scene, {
 *          diffuse: [0.4, 0.4, 1.0],
 *          ambient: [0.9, 0.3, 0.9],
 *          shininess: 30,
 *          alpha: 0.5,
 *          alphaMode: "blend"
 *      }),
 *
 *      edgeMaterial: new EdgeMaterial(viewer.scene, {
 *          edgeColor: [0.0, 0.0, 1.0]
 *          edgeAlpha: 1.0,
 *          edgeWidth: 2
 *      }),
 *
 *      edges: true
 * });
 * ````
 *
 * Note the ````edgeThreshold```` configuration for the {@link ReadableGeometry} on our {@link Mesh}.  EdgeMaterial configures
 * a wireframe representation of the {@link ReadableGeometry}, which will have inner edges (those edges between
 * adjacent co-planar triangles) removed for visual clarity. The ````edgeThreshold```` indicates that, for
 * this particular {@link ReadableGeometry}, an inner edge is one where the angle between the surface normals of adjacent triangles
 * is not greater than ````5```` degrees. That's set to ````2```` by default, but we can override it to tweak the effect
 * as needed for particular Geometries.
 *
 * Here's the example again, this time implicitly defaulting to the {@link Scene#edgeMaterial}. We'll also modify that EdgeMaterial
 * to customize the effect.
 *
 * ````javascript
 * new Mesh({
 *     geometry: new ReadableGeometry(viewer.scene, buildSphereGeometry({
 *          radius: 1.5,
 *          heightSegments: 24,
 *          widthSegments: 16,
 *          edgeThreshold: 2 // Default is 10
 *      })),
 *     material: new PhongMaterial(viewer.scene, {
 *         diffuse: [0.2, 0.2, 1.0]
 *     }),
 *     edges: true
 * });
 *
 * var edgeMaterial = viewer.scene.edgeMaterial;
 *
 * edgeMaterial.edgeColor = [0.2, 1.0, 0.2];
 * edgeMaterial.edgeAlpha = 1.0;
 * edgeMaterial.edgeWidth = 2;
 * ````
 *
 *  ## Presets
 *
 * Let's switch the {@link Scene#edgeMaterial} to one of the presets in {@link EdgeMaterial#presets}:
 *
 * ````javascript
 * viewer.edgeMaterial.preset = EdgeMaterial.presets["sepia"];
 * ````
 *
 * We can also create an EdgeMaterial from a preset, while overriding properties of the preset as required:
 *
 * ````javascript
 * var myEdgeMaterial = new EdgeMaterial(viewer.scene, {
 *      preset: "sepia",
 *      edgeColor = [1.0, 0.5, 0.5]
 * });
 * ````
 */
class EdgeMaterial extends Material {

    /**
     @private
     */
    get type() {
        return "EdgeMaterial";
    }

    /**
     * Gets available EdgeMaterial presets.
     *
     * @type {Object}
     */
    get presets() {
        return PRESETS$2;
    };

    /**
     * @constructor
     * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well.
     * @param {*} [cfg] The EdgeMaterial configuration
     * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted.
     * @param {Number[]} [cfg.edgeColor=[0.2,0.2,0.2]] RGB edge color.
     * @param {Number} [cfg.edgeAlpha=1.0] Edge transparency. A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque.
     * @param {Number} [cfg.edgeWidth=1] Edge width in pixels.
     * @param {String} [cfg.preset] Selects a preset EdgeMaterial configuration - see {@link EdgeMaterial#presets}.
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._state = new RenderState({
            type: "EdgeMaterial",
            edges: null,
            edgeColor: null,
            edgeAlpha: null,
            edgeWidth: null
        });

        this._preset = "default";

        if (cfg.preset) { // Apply preset then override with configs where provided
            this.preset = cfg.preset;
            if (cfg.edgeColor) {
                this.edgeColor = cfg.edgeColor;
            }
            if (cfg.edgeAlpha !== undefined) {
                this.edgeAlpha = cfg.edgeAlpha;
            }
            if (cfg.edgeWidth !== undefined) {
                this.edgeWidth = cfg.edgeWidth;
            }
        } else {
            this.edgeColor = cfg.edgeColor;
            this.edgeAlpha = cfg.edgeAlpha;
            this.edgeWidth = cfg.edgeWidth;
        }
        this.edges = (cfg.edges !== false);
    }


    /**
     * Sets if edges are visible.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    set edges(value) {
        value = value !== false;
        if (this._state.edges === value) {
            return;
        }
        this._state.edges = value;
        this.glRedraw();
    }

    /**
     * Gets if edges are visible.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    get edges() {
        return this._state.edges;
    }

    /**
     * Sets RGB edge color.
     *
     * Default value is ````[0.2, 0.2, 0.2]````.
     *
     * @type {Number[]}
     */
    set edgeColor(value) {
        let edgeColor = this._state.edgeColor;
        if (!edgeColor) {
            edgeColor = this._state.edgeColor = new Float32Array(3);
        } else if (value && edgeColor[0] === value[0] && edgeColor[1] === value[1] && edgeColor[2] === value[2]) {
            return;
        }
        if (value) {
            edgeColor[0] = value[0];
            edgeColor[1] = value[1];
            edgeColor[2] = value[2];
        } else {
            edgeColor[0] = 0.2;
            edgeColor[1] = 0.2;
            edgeColor[2] = 0.2;
        }
        this.glRedraw();
    }

    /**
     * Gets RGB edge color.
     *
     * Default value is ````[0.2, 0.2, 0.2]````.
     *
     * @type {Number[]}
     */
    get edgeColor() {
        return this._state.edgeColor;
    }

    /**
     * Sets edge transparency.
     *
     * A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque.
     *
     * Default value is ````1.0````.
     *
     * @type {Number}
     */
    set edgeAlpha(value) {
        value = (value !== undefined && value !== null) ? value : 1.0;
        if (this._state.edgeAlpha === value) {
            return;
        }
        this._state.edgeAlpha = value;
        this.glRedraw();
    }

    /**
     * Gets edge transparency.
     *
     * A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque.
     *
     * Default value is ````1.0````.
     *
     * @type {Number}
     */
    get edgeAlpha() {
        return this._state.edgeAlpha;
    }

    /**
     * Sets edge width.
     *
     * This is not supported by WebGL implementations based on DirectX [2019].
     *
     * Default value is ````1.0```` pixels.
     *
     * @type {Number}
     */
    set edgeWidth(value) {
        this._state.edgeWidth = value || 1.0;
        this.glRedraw();
    }

    /**
     * Gets edge width.
     *
     * This is not supported by WebGL implementations based on DirectX [2019].
     *
     * Default value is ````1.0```` pixels.
     *
     * @type {Number}
     */
    get edgeWidth() {
        return this._state.edgeWidth;
    }

    /**
     * Selects a preset EdgeMaterial configuration.
     *
     * Default value is ````"default"````.
     *
     * @type {String}
     */
    set preset(value) {
        value = value || "default";
        if (this._preset === value) {
            return;
        }
        const preset = PRESETS$2[value];
        if (!preset) {
            this.error("unsupported preset: '" + value + "' - supported values are " + Object.keys(PRESETS$2).join(", "));
            return;
        }
        this.edgeColor = preset.edgeColor;
        this.edgeAlpha = preset.edgeAlpha;
        this.edgeWidth = preset.edgeWidth;
        this._preset = value;
    }

    /**
     * The current preset EdgeMaterial configuration.
     *
     * Default value is ````"default"````.
     *
     * @type {String}
     */
    get preset() {
        return this._preset;
    }

    /**
     * Destroys this EdgeMaterial.
     */
    destroy() {
        super.destroy();
        this._state.destroy();
    }
}

/**
 *  A plane-shaped 3D object containing a bitmap image.
 *
 * * Creates a 3D quad containing our bitmap, located and oriented using ````pos````, ````normal```` and ````up```` vectors.
 * * Registered by {@link Bitmap#id} in {@link Scene#bitmaps}.
 * * {@link BCFViewpointsPlugin} will save and load Bitmaps in BCF viewpoints.
 *
 * ## Usage
 *
 * In the example below, we'll load the Schependomlaan model, then use
 * an ````Bitmap```` to show a storey plan next to the model.
 *
 * [<img src="http://xeokit.github.io/xeokit-sdk/assets/images/Bitmap_storeyPlan.png">](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#ImagePlane_imageInSectionPlane)
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#ImagePlane_imageInSectionPlane)]
 *
 * ````javascript
 * import {Viewer, Bitmap, XKTLoaderPlugin} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *      canvasId: "myCanvas",
 *      transparent: true
 *  });
 *
 * viewer.camera.eye = [-24.65, 21.69, 8.16];
 * viewer.camera.look = [-14.62, 2.16, -1.38];
 * viewer.camera.up = [0.59, 0.57, -0.56];
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * xktLoader.load({
 *      id: "myModel",
 *      src: "./models/xkt/Schependomlaan.xkt",
 *      edges: true
 *  });
 *
 * new Bitmap(viewer.scene, {
 *      src: "./images/schependomlaanPlanView.png",
 *      visible: true,                             // Show the Bitmap
 *      height: 24.0,                              // Height of Bitmap
 *      pos: [-15, 0, -10],                        // World-space position of Bitmap's center
 *      normal: [0, -1, 0],                        // Vector perpendicular to Bitmap
 *      up: [0, 0, 1],                             // Direction of Bitmap "up"
 *      collidable: false,                         // Bitmap does not contribute to Scene boundary
 *      clippable: true,                           // Bitmap can be clipped by SectionPlanes
 *      pickable: true                             // Allow the ground plane to be picked
 * });
 * ````
 */
class Bitmap extends Component {

    /**
     * Creates a new Bitmap.
     *
     * Registers the Bitmap in {@link Scene#bitmaps}; causes Scene to fire a "bitmapCreated" event.
     *
     * @constructor
     * @param {Component} [owner]  Owner component. When destroyed, the owner will destroy this ````Bitmap```` as well.
     * @param {*} [cfg]  ````Bitmap```` configuration
     * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted.
     * @param {Boolean} [cfg.visible=true] Indicates whether or not this ````Bitmap```` is visible.
     * @param {Number[]} [cfg.pos=[0,0,0]] World-space position of the ````Bitmap````.
     * @param {Number[]} [cfg.normal=[0,0,1]] Normal vector indicating the direction the ````Bitmap```` faces.
     * @param {Number[]} [cfg.up=[0,1,0]] Direction of "up" for the ````Bitmap````.
     * @param {Number[]} [cfg.height=1] World-space height of the ````Bitmap````.
     * @param {Number[]} [cfg.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]] Modelling transform matrix for the ````Bitmap````. Overrides the ````position````, ````height```, ````rotation```` and ````normal```` parameters.
     * @param {Boolean} [cfg.collidable=true] Indicates if the ````Bitmap```` is initially included in boundary calculations.
     * @param {Boolean} [cfg.clippable=true] Indicates if the ````Bitmap```` is initially clippable.
     * @param {Boolean} [cfg.pickable=true] Indicates if the ````Bitmap```` is initially pickable.
     * @param {Number} [cfg.opacity=1.0] ````Bitmap````'s initial opacity factor, multiplies by the rendered fragment alpha.
     * @param {String} [cfg.src] URL of image. Accepted file types are PNG and JPEG.
     * @param {HTMLImageElement} [cfg.image] An ````HTMLImageElement```` to source the image from. Overrides ````src````.
     * @param {String} [cfg.imageData]    Image data as a base64 encoded string.
     * @param {String} [cfg.type="jpg"] Image MIME type. Accepted values are "jpg" and "png". Default is "jpg". Normally only needed with ````image```` or ````imageData````. Automatically inferred from file extension of ````src````, if the file has a recognized extension.
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._type = cfg.type || (cfg.src ? cfg.src.split('.').pop() : null) || "jpg";
        this._pos = math.vec3(cfg.pos || [0, 0, 0]);
        this._up = math.vec3(cfg.up || [0, 1, 0]);
        this._normal = math.vec3(cfg.normal || [0, 0, 1]);
        this._height = cfg.height || 1.0;

        this._origin = math.vec3();
        this._rtcPos = math.vec3();
        this._imageSize = math.vec2();

        this._texture = new Texture(this, {
            flipY: true
        });

        this._image = new Image();

        if (this._type !== "jpg" && this._type !== "png") {
            this.error(`Unsupported type - defaulting to "jpg"`);
            this._type = "jpg";
        }

        this._node = new Node$1(this, {
            matrix: math.inverseMat4(
                math.lookAtMat4v(this._pos, math.subVec3(this._pos, this._normal, math.mat4()),
                    this._up,
                    math.mat4())),
            children: [

                this._bitmapMesh = new Mesh(this, {
                    scale: [1, 1, 1],
                    rotation: [-90, 0, 0],
                    collidable: cfg.collidable,
                    pickable: cfg.pickable,
                    opacity: cfg.opacity,
                    clippable: cfg.clippable,

                    geometry: new ReadableGeometry(this, buildPlaneGeometry({
                        center: [0, 0, 0],
                        xSize: 1,
                        zSize: 1,
                        xSegments: 2,
                        zSegments: 2
                    })),

                    material: new PhongMaterial(this, {
                        diffuse: [0, 0, 0],
                        ambient: [0, 0, 0],
                        specular: [0, 0, 0],
                        diffuseMap: this._texture,
                        emissiveMap: this._texture,
                        backfaces: true
                    })
                })
            ]
        });

        if (cfg.image) {
            this.image = cfg.image;
        } else if (cfg.src) {
            this.src = cfg.src;
        } else if (cfg.imageData) {
            this.imageData = cfg.imageData;
        }

        this.scene._bitmapCreated(this);
    }

    /**
     * Sets if this ````Bitmap```` is visible or not.
     *
     * Default value is ````true````.
     *
     * @param {Boolean} visible Set ````true```` to make this ````Bitmap```` visible.
     */
    set visible(visible) {
        this._bitmapMesh.visible = visible;
    }

    /**
     * Gets if this ````Bitmap```` is visible or not.
     *
     * Default value is ````true````.
     *
     * @returns {Boolean} Returns ````true```` if visible.
     */
    get visible() {
        return this._bitmapMesh.visible;
    }

    /**
     * Sets an ````HTMLImageElement```` to source the image from.
     *
     * Sets {@link Texture#src} null.
     *
     * You may also need to set {@link Bitmap#type}, if you want to read the image data with {@link Bitmap#imageData}.
     *
     * @type {HTMLImageElement}
     */
    set image(image) {
        this._image = image;
        if (this._image) {
            this._texture.image = this._image;
            this._imageSize[0] = this._image.width;
            this._imageSize[1] = this._image.height;
            this._updateBitmapMeshScale();
        }
    }

    /**
     * Gets the ````HTMLImageElement```` the ````Bitmap````'s image is sourced from, if set.
     *
     * Returns null if not set.
     *
     * @type {HTMLImageElement}
     */
    get image() {
        return this._image;
    }

    /**
     * Sets an image file path that the ````Bitmap````'s image is sourced from.
     *
     * If the file extension is a recognized MIME type, also sets {@link Bitmap#type} to that MIME type.
     *
     * Accepted file types are PNG and JPEG.
     *
     * @type {String}
     */
    set src(src) {
        if (src) {
            this._image.onload = () => {
                this._texture.image = this._image;
                this._imageSize[0] = this._image.width;
                this._imageSize[1] = this._image.height;
                this._updateBitmapMeshScale();
            };
            this._image.src = src;
            const ext = src.split('.').pop();
            switch (ext) {
                case "jpeg":
                case "jpg":
                    this._type = "jpg";
                    break;
                case "png":
                    this._type = "png";
            }
        }
    }

    /**
     * Gets the image file path that the ````Bitmap````'s image is sourced from, if set.
     *
     * Returns null if not set.
     *
     * @type {String}
     */
    get src() {
        return this._image.src;
    }

    /**
     * Sets an image file path that the ````Bitmap````'s image is sourced from.
     *
     * Accepted file types are PNG and JPEG.
     *
     * Sets {@link Texture#image} null.
     *
     * You may also need to set {@link Bitmap#type}, if you want to read the image data with {@link Bitmap#imageData}.
     *
     * @type {String}
     */
    set imageData(imageData) {
        this._image.onload = () => {
            this._texture.image = image;
            this._imageSize[0] = image.width;
            this._imageSize[1] = image.height;
            this._updateBitmapMeshScale();
        };
        this._image.src = imageData;
    }

    /**
     * Gets the image file path that the ````Bitmap````'s image is sourced from, if set.
     *
     * Returns null if not set.
     *
     * @type {String}
     */
    get imageData() {
        const canvas = document.createElement('canvas');
        const context = canvas.getContext('2d');
        canvas.width = this._image.width;
        canvas.height = this._image.height;
        context.drawImage(this._image, 0, 0);
        return canvas.toDataURL(this._type === "jpg" ? 'image/jpeg' : 'image/png');
    }

    /**
     * Sets the MIME type of this Bitmap.
     *
     * This is used by ````Bitmap```` when getting image data with {@link Bitmap#imageData}.
     *
     * Supported values are "jpg" and "png",
     *
     * Default is "jpg".
     *
     * @type {String}
     */
    set type(type) {
        type = type || "jpg";
        if (type !== "png" || type !== "jpg") {
            this.error("Unsupported value for `type` - supported types are `jpg` and `png` - defaulting to `jpg`");
            type = "jpg";
        }
        this._type = type;
    }

    /**
     * Gets the MIME type of this Bitmap.
     *
     * @type {String}
     */
    get type() {
        return this._type;
    }

    /**
     * Gets the World-space position of this ````Bitmap````.
     *
     * Default value is ````[0, 0, 0]````.
     *
     * @returns {Number[]} Current position.
     */
    get pos() {
        return this._pos;
    }

    /**
     * Gets the direction of the normal vector that is perpendicular to this ````Bitmap````.
     *
     * @returns {Number[]} value Current normal direction.
     */
    get normal() {
        return this._normal;
    }

    /**
     * Gets the "up" direction of this ````Bitmap````.
     *
     * @returns {Number[]} value Current "up" direction.
     */
    get up() {
        return this._up;
    }

    /**
     * Sets the World-space height of the ````Bitmap````.
     *
     * Default value is ````1.0````.
     *
     * @param {Number} height New World-space height of the ````Bitmap````.
     */
    set height(height) {
        this._height = (height === undefined || height === null) ? 1.0 : height;
        if (this._image) {
            this._updateBitmapMeshScale();
        }
    }

    /**
     * Gets the World-space height of the ````Bitmap````.
     *
     * Returns {Number} World-space height of the ````Bitmap````.
     */
    get height() {
        return this._height;
    }

    /**
     * Sets if this ````Bitmap```` is included in boundary calculations.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    set collidable(value) {
        this._bitmapMesh.collidable = (value !== false);
    }

    /**
     * Gets if this ````Bitmap```` is included in boundary calculations.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    get collidable() {
        return this._bitmapMesh.collidable;
    }

    /**
     * Sets if this ````Bitmap```` is clippable.
     *
     * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    set clippable(value) {
        this._bitmapMesh.clippable = (value !== false);
    }

    /**
     * Gets if this ````Bitmap````  is clippable.
     *
     * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    get clippable() {
        return this._bitmapMesh.clippable;
    }

    /**
     * Sets if this ````Bitmap```` is pickable.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    set pickable(value) {
        this._bitmapMesh.pickable = (value !== false);
    }

    /**
     * Gets if this ````Bitmap````  is pickable.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    get pickable() {
        return this._bitmapMesh.pickable;
    }

    /**
     * Sets the opacity factor for this ````Bitmap````.
     *
     * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas.
     *
     * @type {Number}
     */
    set opacity(opacity) {
        this._bitmapMesh.opacity = opacity;
    }

    /**
     * Gets this ````Bitmap````'s opacity factor.
     *
     * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas.
     *
     * @type {Number}
     */
    get opacity() {
        return this._bitmapMesh.opacity;
    }

    /**
     * Destroys this ````Bitmap````.
     *
     * Removes the ```Bitmap```` from {@link Scene#bitmaps}; causes Scene to fire a "bitmapDestroyed" event.
     */
    destroy() {
        super.destroy();
        this.scene._bitmapDestroyed(this);
    }

    _updateBitmapMeshScale() {
        const aspect = this._imageSize[1] / this._imageSize[0];
        this._bitmapMesh.scale = [this._height / aspect, 1.0, this._height];
    }
}

/**
 * Uses edge adjacency counts to identify if the given triangle mesh can be rendered with backface culling enabled.
 *
 * If all edges are connected to exactly two triangles, then the mesh will likely be a closed solid, and we can safely
 * render it with backface culling enabled.
 *
 * Otherwise, the mesh is a surface, and we must render it with backface culling disabled.
 *
 * @private
 */
const isTriangleMeshSolid = (indices, positions) => {

    const vertexIndexMapping = [];
    let edges = [];

    function compareIndexPositions(a, b) {
        let posA, posB;

        for (let i = 0; i < 3; i++) {
            posA = positions [a * 3 + i];
            posB = positions [b * 3 + i];

            if (posA !== posB) {
                return posB - posA;
            }
        }

        return 0;
    }
    // Group together indices corresponding to same position coordinates
    let newIndices = indices.slice().sort(compareIndexPositions);

    // Calculate the mapping:
    // - from original index in indices array
    // - to indices-for-unique-positions
    let uniqueVertexIndex = null;

    for (let i = 0, len = newIndices.length; i < len; i++) {
        if (i === 0 || 0 !== compareIndexPositions(
            newIndices[i],
            newIndices[i - 1],
        )) {
            // different position
            uniqueVertexIndex = newIndices [i];
        }

        vertexIndexMapping [
            newIndices[i]
            ] = uniqueVertexIndex;
    }

    // Generate the list of edges
    for (let i = 0, len = indices.length; i < len; i += 3) {

        const a = vertexIndexMapping[indices[i]];
        const b = vertexIndexMapping[indices[i + 1]];
        const c = vertexIndexMapping[indices[i + 2]];

        let a2 = a;
        let b2 = b;
        let c2 = c;

        if (a > b && a > c) {
            if (b > c) {
                a2 = a;
                b2 = b;
                c2 = c;
            } else {
                a2 = a;
                b2 = c;
                c2 = b;
            }
        } else if (b > a && b > c) {
            if (a > c) {
                a2 = b;
                b2 = a;
                c2 = c;
            } else {
                a2 = b;
                b2 = c;
                c2 = a;
            }
        } else if (c > a && c > b) {
            if (a > b) {
                a2 = c;
                b2 = a;
                c2 = b;
            } else {
                a2 = c;
                b2 = b;
                c2 = a;
            }
        }

        edges[i + 0] = [
            a2, b2
        ];
        edges[i + 1] = [
            b2, c2
        ];

        if (a2 > c2) {
            const temp = c2;
            c2 = a2;
            a2 = temp;
        }

        edges[i + 2] = [
            c2, a2
        ];
    }

    // Group semantically equivalent edgdes together
    function compareEdges(e1, e2) {
        let a, b;

        for (let i = 0; i < 2; i++) {
            a = e1[i];
            b = e2[i];

            if (b !== a) {
                return b - a;
            }
        }

        return 0;
    }

    edges = edges.slice(0, indices.length);

    edges.sort(compareEdges);

    // Make sure each edge is used exactly twice
    let sameEdgeCount = 0;

    for (let i = 0; i < edges.length; i++) {
        if (i === 0 || 0 !== compareEdges(
            edges[i], edges[i - 1]
        )) {
            // different edge
            if (0 !== i && sameEdgeCount !== 2) {
                return false;
            }

            sameEdgeCount = 1;
        } else {
            // same edge
            sameEdgeCount++;
        }
    }

    if (edges.length > 0 && sameEdgeCount !== 2) {
        return false;
    }

    // Each edge is used exactly twice, this is a
    // watertight surface and hence a solid geometry.
    return true;
};

const v1$1 = math.vec3();
const v2$1 = math.vec3();
const v3$1 = math.vec3();

/**
 * Calculates the volume of triangle meshes.
 */
class MeshVolume {
    constructor() {
        this.vertices = [];
        this.indices = [];
        this.reset();
    }

    /**
     * Resets, ready to add vertices and indices for a new mesh.
     */
    reset() {
        this.lenVertices = 0;
        this.lenIndices = 0;
        this.primitive = null;
    }

    setPrimitive(primitive) {
        this.primitive = primitive;
    }

    /**
     * Adds a vertex.
     * @param vertex
     */
    addVertex(vertex) {
        this.vertices[this.lenVertices++] = vertex[0];
        this.vertices[this.lenVertices++] = vertex[1];
        this.vertices[this.lenVertices++] = vertex[2];
    }

    /**
     * Adds an index.
     * @param index
     */
    addIndex(index) {
        this.indices[this.lenIndices++] = index;
    }

    /**
     * Gets the volume of the mesh.
     *
     * The mesh must be a closed solid.
     *
     * @returns {number} The volume of the mesh, or -1 if the mesh is not solid, in which case volume cannot be determined.
     */
    get volume() {

        const vertices = this.vertices;
        const indices = this.indices;

        if (this.primitive !== "solid" && this.primitive !== "surface" && this.primitive !== "triangles") {
            return -1;
        }

        if (this.primitive !== "solid" && !isTriangleMeshSolid(indices, vertices)) {
            return -1;
        }

        let volume = 0;

        for (let i = 0; i < this.lenIndices; i += 3) {

            const index1 = indices[i] * 3;
            const index2 = indices[i + 1] * 3;
            const index3 = indices[i + 2] * 3;

            v1$1[0] = vertices[index1];
            v1$1[1] = vertices[index1 + 1];
            v1$1[2] = vertices[index1 + 2];

            v2$1[0] = vertices[index2];
            v2$1[1] = vertices[index2 + 1];
            v2$1[2] = vertices[index2 + 2];

            v3$1[0] = vertices[index3];
            v3$1[1] = vertices[index3 + 1];
            v3$1[2] = vertices[index3 + 2];

            math.cross3Vec3(v1$1, v2$1, v1$1);

            volume += math.dotVec3(v1$1, v3$1);
        }

        return volume / 6;
    }
}

/**
 * Singleton instance of {@link MeshVolume}.
 * @type {MeshVolume}
 */
const meshVolume = new MeshVolume();

const v1 = math.vec3();
const v2 = math.vec3();
const v3 = math.vec3();
const v4 = math.vec3();
const v5 = math.vec3();
const v6 = math.vec3();

/**
 * Calculates the surface area of triangle meshes.
 */
class MeshSurfaceArea {
    constructor() {
        this.vertices = [];
        this.indices = [];
        this.reset();
    }

    /**
     * Resets, ready to add vertices and indices for a new mesh.
     */
    reset() {
        this.lenVertices = 0;
        this.lenIndices = 0;
    }

    /**
     * Adds a vertex.
     * @param vertex
     */
    addVertex(vertex) {
        this.vertices[this.lenVertices++] = vertex[0];
        this.vertices[this.lenVertices++] = vertex[1];
        this.vertices[this.lenVertices++] = vertex[2];
    }

    /**
     * Adds an index.
     * @param index
     */
    addIndex(index) {
        this.indices[this.lenIndices++] = index;
    }

    /**
     * Gets the surface area of the mesh.
     *
     * @returns {number}
     */
    get surfaceArea() {

        const vertices = this.vertices;
        const indices = this.indices;

        let surfaceArea = 0;

        for (let i = 0; i < this.lenIndices; i += 3) {

            const index1 = indices[i] * 3;
            const index2 = indices[i + 1] * 3;
            const index3 = indices[i + 2] * 3;

            v1[0] = vertices[index1];
            v1[1] = vertices[index1 + 1];
            v1[2] = vertices[index1 + 2];

            v2[0] = vertices[index2];
            v2[1] = vertices[index2 + 1];
            v2[2] = vertices[index2 + 2];

            v3[0] = vertices[index3];
            v3[1] = vertices[index3 + 1];
            v3[2] = vertices[index3 + 2];

            math.subVec3(v1, v2, v4);
            math.subVec3(v3, v2, v5);

            surfaceArea += math.lenVec3(math.cross3Vec3(v4, v5, v6)) * 0.5;
        }

        return surfaceArea;
    }
}

/**
 * Singleton instance of {@link MeshSurfaceArea}.
 * @type {MeshSurfaceArea}
 */
const meshSurfaceArea = new MeshSurfaceArea();

const tempOBB3$1 = math.OBB3();
const tempOBB3b = math.OBB3();
const tempOBB3c = math.OBB3();


/**
 * A mesh within a {@link SceneModel}.
 *
 * * Created with {@link SceneModel#createMesh}
 * * Belongs to exactly one {@link SceneModelEntity}
 * * Stored by ID in {@link SceneModel#meshes}
 * * Referenced by {@link SceneModelEntity#meshes}
 * * Can have a {@link SceneModelTransform} to dynamically scale, rotate and translate it.
 */
class SceneModelMesh {

    /**
     * @private
     */
    constructor(model, id, color, opacity, transform, textureSet, layer = null, portionId = 0) {

        /**
         * The {@link SceneModel} that owns this SceneModelMesh.
         *
         * @type {SceneModel}
         */
        this.model = model;

        /**
         * The {@link SceneModelEntity} that owns this SceneModelMesh.
         *
         * @type {SceneModelEntity}
         */
        this.object = null;

        /**
         * @private
         */
        this.parent = null;

        /**
         * The {@link SceneModelTransform} that transforms this SceneModelMesh.
         *
         * * This only exists when the SceneModelMesh is instancing its geometry.
         * * These are created with {@link SceneModel#createTransform}
         * * Each of these is also registered in {@link SceneModel#transforms}.
         *
         * @type {SceneModelTransform}
         */
        this.transform = transform;

        /**
         * The {@link SceneModelTextureSet} that optionally textures this SceneModelMesh.
         *
         * * This only exists when the SceneModelMesh has texture.
         * * These are created with {@link SceneModel#createTextureSet}
         * * Each of these is also registered in {@link SceneModel#textureSets}.
         *
         * @type {SceneModelTextureSet}
         */
        this.textureSet = textureSet;

        this._matrixDirty = false;
        this._matrixUpdateScheduled = false;

        /**
         * Unique ID of this SceneModelMesh.
         *
         * The SceneModelMesh is registered against this ID in {@link SceneModel#meshes}.
         */
        this.id = id;

        /**
         * @private
         */
        this.obb = null;

        this._aabbLocal = null;
        this._aabbWorld = math.AABB3();
        this._aabbWorldDirty = false;

        /**
         * @private
         */
        this.layer = layer;

        /**
         * @private
         */
        this.portionId = portionId;

        this._color = new Uint8Array([color[0], color[1], color[2], opacity]); // [0..255]
        this._colorize = new Uint8Array([color[0], color[1], color[2], opacity]); // [0..255]
        this._colorizing = false;
        this._transparent = (opacity < 255);

        /**
         * @private
         */
        this.numTriangles = 0;

        /**
         * @private
         * @type {null}
         */
        this.origin = null; // Set By SceneModel

        /**
         * The {@link SceneModelEntity} that owns this SceneModelMesh.
         *
         * @type {SceneModelEntity}
         */
        this.entity = null;

        if (transform) {
            transform._addMesh(this);
        }

        this._volume = null;
        this._surfaceArea = null;
    }

    _sceneModelDirty() {
        this._aabbWorldDirty = true;
        this.layer.aabbDirty = true;
    }

    _transformDirty() {
        if (!this._matrixDirty && !this._matrixUpdateScheduled) {
            this.model._meshMatrixDirty(this);
            this._matrixDirty = true;
            this._matrixUpdateScheduled = true;
        }
        this._aabbWorldDirty = true;
        this.layer.aabbDirty = true;
        if (this.entity) {
            this.entity._transformDirty();
        }
    }

    _updateMatrix() {
        if (this.transform && this._matrixDirty) {
            this.layer.setMatrix(this.portionId, this.transform.worldMatrix);
        }
        this._matrixDirty = false;
        this._matrixUpdateScheduled = false;
    }

    _finalize(entityFlags) {
        this.layer.initFlags(this.portionId, entityFlags, this._transparent);
    }

    _finalize2() {
        if (this.layer.flushInitFlags) {
            this.layer.flushInitFlags();
        }
    }

    _setVisible(entityFlags) {
        this.layer.setVisible(this.portionId, entityFlags, this._transparent);
    }

    _setColor(color) {
        this._color[0] = color[0];
        this._color[1] = color[1];
        this._color[2] = color[2];
        if (!this._colorizing) {
            this.layer.setColor(this.portionId, this._color, false);
        }
    }

    _setColorize(colorize) {
        const setOpacity = false;
        if (colorize) {
            this._colorize[0] = colorize[0];
            this._colorize[1] = colorize[1];
            this._colorize[2] = colorize[2];
            this.layer.setColor(this.portionId, this._colorize, setOpacity);
            this._colorizing = true;
        } else {
            this.layer.setColor(this.portionId, this._color, setOpacity);
            this._colorizing = false;
        }
    }

    _setOpacity(opacity, entityFlags) {
        const newTransparent = (opacity < 255);
        const lastTransparent = this._transparent;
        const changingTransparency = (lastTransparent !== newTransparent);
        this._color[3] = opacity;
        this._colorize[3] = opacity;
        this._transparent = newTransparent;
        if (this._colorizing) {
            this.layer.setColor(this.portionId, this._colorize);
        } else {
            this.layer.setColor(this.portionId, this._color);
        }
        if (changingTransparency) {
            this.layer.setTransparent(this.portionId, entityFlags, newTransparent);
        }
    }

    _setOffset(offset) {
        this.layer.setOffset(this.portionId, offset);
    }

    _setHighlighted(entityFlags) {
        this.layer.setHighlighted(this.portionId, entityFlags, this._transparent);
    }

    _setXRayed(entityFlags) {
        this.layer.setXRayed(this.portionId, entityFlags, this._transparent);
    }

    _setSelected(entityFlags) {
        this.layer.setSelected(this.portionId, entityFlags, this._transparent);
    }

    _setEdges(entityFlags) {
        this.layer.setEdges(this.portionId, entityFlags, this._transparent);
    }

    _setClippable(entityFlags) {
        this.layer.setClippable(this.portionId, entityFlags, this._transparent);
    }

    _setCollidable(entityFlags) {
        this.layer.setCollidable(this.portionId, entityFlags);
    }

    _setPickable(flags) {
        this.layer.setPickable(this.portionId, flags, this._transparent);
    }

    _setCulled(flags) {
        this.layer.setCulled(this.portionId, flags, this._transparent);
    }

    /**
     * @private
     */
    canPickTriangle() {
        return false;
    }

    /**
     * @private
     */
    drawPickTriangles(renderFlags, frameCtx) {
        // NOP
    }

    /**
     * @private
     */
    pickTriangleSurface(pickResult) {
        // NOP
    }

    /**
     * @private
     */
    precisionRayPickSurface(worldRayOrigin, worldRayDir, worldSurfacePos, worldSurfaceNormal) {
        return this.layer.precisionRayPickSurface ? this.layer.precisionRayPickSurface(this.portionId, worldRayOrigin, worldRayDir, worldSurfacePos, worldSurfaceNormal) : false;
    }

    /**
     * @private
     */
    canPickWorldPos() {
        return true;
    }

    /**
     * @private
     */
    drawPickDepths(frameCtx) {
        this.model.drawPickDepths(frameCtx);
    }

    /**
     * @private
     */
    drawPickNormals(frameCtx) {
        this.model.drawPickNormals(frameCtx);
    }

    /**
     * @private
     */
    delegatePickedEntity() {
        return this.parent;
    }

    /**
     * @private
     */
    getEachVertex(callback) {
        if (this.layer.getEachVertex) {
            this.layer.getEachVertex(this.portionId, callback);
        }
    }

    /**
     * @private
     */
    getEachIndex(callback) {
        if (this.layer.getEachIndex ) {
            this.layer.getEachIndex(this.portionId, callback);
        }
    }

    isSolid() {
        return this.layer.solid;
    }

    /**
     * Returns the volume of this SceneModelMesh.
     * @returns {number}
     */
    get volume() {
        if (this._volume !== null) {
            return this._volume;
        }
        switch (this.layer.primitive) {
            case "solid":
            case "surface":
            case "triangles":
                meshVolume.reset();
                meshVolume.setPrimitive(this.layer.primitive);
                this.getEachVertex((vertex) =>{
                    meshVolume.addVertex(vertex);
                });
                this.getEachIndex((index)=>{
                   meshVolume.addIndex(index);
                });
                this._volume = meshVolume.volume;
                break;
            default:
                this._volume = 0;
                break;
        }
        return this._volume;
    }

    /**
     * Returns the surface area of this SceneModelMesh.
     * @returns {number}
     */
    get surfaceArea() {
        if (this._surfaceArea !== null) {
            return this._surfaceArea;
        }
        switch (this.layer.primitive) {
            case "solid":
            case "surface":
            case "triangles":
                meshSurfaceArea.reset();
                this.getEachVertex((vertex) =>{
                    meshSurfaceArea.addVertex(vertex);
                });
                this.getEachIndex((index)=>{
                    meshSurfaceArea.addIndex(index);
                });
                this._surfaceArea = meshSurfaceArea.surfaceArea;
                break;
            default:
                this._surfaceArea = 0;
                break;
        }
        return this._surfaceArea;
    }

    /**
     * @private
     */
    set aabb(aabb) { // Called by SceneModel
        this._aabbLocal = aabb;
        if (this.origin) {
            this._aabbLocal = aabb.slice(0);
            const origin = this.origin;
            this._aabbLocal[0] += origin[0];
            this._aabbLocal[1] += origin[1];
            this._aabbLocal[2] += origin[2];
            this._aabbLocal[3] += origin[0];
            this._aabbLocal[4] += origin[1];
            this._aabbLocal[5] += origin[2];
        }
    }

    /**
     * @private
     */
    get aabb() { // called by SceneModelEntity
        if (this._aabbWorldDirty) {
            math.AABB3ToOBB3(this._aabbLocal, tempOBB3$1);
            if (this.transform) {
                math.transformOBB3(this.transform.worldMatrix, tempOBB3$1, tempOBB3b);
                math.transformOBB3(this.model.worldMatrix, tempOBB3b, tempOBB3c);
                math.OBB3ToAABB3(tempOBB3c, this._aabbWorld);
            } else {
                math.transformOBB3(this.model.worldMatrix, tempOBB3$1, tempOBB3b);
                math.OBB3ToAABB3(tempOBB3b, this._aabbWorld);
            }
            this._aabbWorldDirty = false;
        }
        return this._aabbWorld;
    }

    /**
     * @private
     */
    _destroy() {
        this.model.scene._renderer.putPickID(this.pickId);
    }
}

/**
 * Provides scratch memory for methods like TrianglesBatchingLayer setFlags() and setColors(),
 * so they don't need to allocate temporary arrays that need garbage collection.
 *
 * @private
 */
class ScratchMemory {

    constructor() {
        this._uint8Arrays = {};
        this._float32Arrays = {};
    }

    _clear() {
        this._uint8Arrays = {};
        this._float32Arrays = {};
    }

    getUInt8Array(len) {
        let uint8Array = this._uint8Arrays[len];
        if (!uint8Array) {
            uint8Array = new Uint8Array(len);
            this._uint8Arrays[len] = uint8Array;
        }
        return uint8Array;
    }

    getFloat32Array(len) {
        let float32Array = this._float32Arrays[len];
        if (!float32Array) {
            float32Array = new Float32Array(len);
            this._float32Arrays[len] = float32Array;
        }
        return float32Array;
    }
}

const batchingLayerScratchMemory = new ScratchMemory();

let countUsers = 0;

/**
 * @private
 */
function getScratchMemory() {
    countUsers++;
    return batchingLayerScratchMemory;
}

/**
 * @private
 */
function putScratchMemory() {
    if (countUsers === 0) {
        return;
    }
    countUsers--;
    if (countUsers === 0) {
        batchingLayerScratchMemory._clear();
    }
}

/**
 * @private
 * @type {{PICKABLE: number, CLIPPABLE: number, BACKFACES: number, VISIBLE: number, SELECTED: number, OUTLINED: number, CULLED: number, RECEIVE_SHADOW: number, COLLIDABLE: number, XRAYED: number, CAST_SHADOW: number, EDGES: number, HIGHLIGHTED: number}}
 */
const ENTITY_FLAGS = {
    VISIBLE: 1,
    CULLED: 1 << 2,
    PICKABLE: 1 << 3,
    CLIPPABLE: 1 << 4,
    COLLIDABLE: 1 << 5,
    XRAYED: 1 << 8,
    HIGHLIGHTED: 1 << 9,
    SELECTED: 1 << 10,
    EDGES: 1 << 11};

/**
 * @private
 */
const RENDER_PASSES = {

    // Skipped - suppress rendering

    NOT_RENDERED: 0,

    // Normal rendering - mutually exclusive modes

    COLOR_OPAQUE: 1,
    COLOR_TRANSPARENT: 2,

    // Emphasis silhouette rendering - mutually exclusive modes

    SILHOUETTE_HIGHLIGHTED: 3,
    SILHOUETTE_SELECTED: 4,
    SILHOUETTE_XRAYED: 5,

    // Edges rendering - mutually exclusive modes

    EDGES_COLOR_OPAQUE: 6,
    EDGES_COLOR_TRANSPARENT: 7,
    EDGES_HIGHLIGHTED: 8,
    EDGES_SELECTED: 9,
    EDGES_XRAYED: 10,

    // Picking

    PICK: 11
};

const defaultColor$2 = new Float32Array([1, 1, 1, 1]);
const edgesDefaultColor = new Float32Array([0, 0, 0, 1]);

const tempVec4 = math.vec4();
const tempVec3a$F = math.vec3();
const tempVec3c$w = math.vec3();
const tempMat4a$r = math.mat4();

/**
 * @private
 */
class VBORenderer {
    constructor(scene, withSAO = false, {instancing = false, edges = false, useAlphaCutoff = false} = {}) {
        this._scene = scene;
        this._withSAO = withSAO;
        this._instancing = instancing;
        this._edges = edges;
        this._useAlphaCutoff = useAlphaCutoff;
        this._hash = this._getHash();

        /**
         * Matrices Uniform Block Buffer
         *
         * In shaders, matrices in the Matrices Uniform Block MUST be set in this order:
         *  - worldMatrix
         *  - viewMatrix
         *  - projMatrix
         *  - positionsDecodeMatrix
         *  - worldNormalMatrix
         *  - viewNormalMatrix
         */
        this._matricesUniformBlockBufferBindingPoint = 0;

        this._matricesUniformBlockBuffer = this._scene.canvas.gl.createBuffer();
        this._matricesUniformBlockBufferData = new Float32Array(4 * 4 * 6); // there is 6 mat4

        /**
         * A Vertex Array Object by Layer
         */
        this._vaoCache = new WeakMap();

        this._allocate();
    }

    /**
     * Should be overrided by subclasses if it does not only "depend" on section planes state.
     * @returns { string }
     */
    _getHash() {
        return this._scene._sectionPlanesState.getHash();
    }

    _buildShader() {
        return {
            vertex: this._buildVertexShader(),
            fragment: this._buildFragmentShader()
        };
    }

    _buildVertexShader() {
        return [""];
    }

    _buildFragmentShader() {
        return [""];
    }

    _addMatricesUniformBlockLines(src, normals = false) {
        src.push("uniform Matrices {");
        src.push("    mat4 worldMatrix;");
        src.push("    mat4 viewMatrix;");
        src.push("    mat4 projMatrix;");
        src.push("    mat4 positionsDecodeMatrix;");
        if (normals) {
            src.push("    mat4 worldNormalMatrix;");
            src.push("    mat4 viewNormalMatrix;");
        }
        src.push("};");
        return src;
    }

    _addRemapClipPosLines(src, viewportSize = 1) {
        src.push("uniform vec2 drawingBufferSize;");
        src.push("uniform vec2 pickClipPos;");

        src.push("vec4 remapClipPos(vec4 clipPos) {");
        src.push("    clipPos.xy /= clipPos.w;");
        if (viewportSize === 1) {
            src.push("    clipPos.xy = (clipPos.xy - pickClipPos) * drawingBufferSize;");
        } else {
            src.push(`    clipPos.xy = (clipPos.xy - pickClipPos) * (drawingBufferSize / float(${viewportSize}));`);
        }
        src.push("    clipPos.xy *= clipPos.w;");
        src.push("    return clipPos;");
        src.push("}");
        return src;
    }

    getValid() {
        return this._hash === this._getHash();
    }

    setSectionPlanesStateUniforms(layer) {
        const scene = this._scene;
        const {gl} = scene.canvas;
        const {model, layerIndex} = layer;

        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const baseIndex = layerIndex * numSectionPlanes;
            const renderFlags = model.renderFlags;
            if (scene.crossSections) {
                gl.uniform4fv(this._uSliceColor, scene.crossSections.sliceColor);
                gl.uniform1f(this._uSliceThickness, scene.crossSections.sliceThickness);
            }
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            const origin = layer._state.origin;
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$F, model.matrix);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }
    }


    _allocate() {
        const scene = this._scene;
        const gl = scene.canvas.gl;
        const lightsState = scene._lightsState;

        this._program = new Program(gl, this._buildShader());

        if (this._program.errors) {
            this.errors = this._program.errors;
            return;
        }

        const program = this._program;

        this._uRenderPass = program.getLocation("renderPass");

        this._uColor = program.getLocation("color");
        if (!this._uColor) {
            // some shader may have color as attribute, in this case the uniform must be renamed silhouetteColor
            this._uColor = program.getLocation("silhouetteColor");
        }
        this._uUVDecodeMatrix = program.getLocation("uvDecodeMatrix");
        this._uPickInvisible = program.getLocation("pickInvisible");
        this._uGammaFactor = program.getLocation("gammaFactor");

        gl.uniformBlockBinding(
            program.handle,
            gl.getUniformBlockIndex(program.handle, "Matrices"),
            this._matricesUniformBlockBufferBindingPoint
        );

        this._uShadowViewMatrix = program.getLocation("shadowViewMatrix");
        this._uShadowProjMatrix = program.getLocation("shadowProjMatrix");
        if (scene.logarithmicDepthBufferEnabled) {
            this._uZFar = program.getLocation("zFar");
        }

        this._uLightAmbient = program.getLocation("lightAmbient");
        this._uLightColor = [];
        this._uLightDir = [];
        this._uLightPos = [];
        this._uLightAttenuation = [];

        // TODO add a gard to prevent light params if not affected by light ?
        const lights = lightsState.lights;
        let light;

        for (let i = 0, len = lights.length; i < len; i++) {
            light = lights[i];
            switch (light.type) {
                case "dir":
                    this._uLightColor[i] = program.getLocation("lightColor" + i);
                    this._uLightPos[i] = null;
                    this._uLightDir[i] = program.getLocation("lightDir" + i);
                    break;
                case "point":
                    this._uLightColor[i] = program.getLocation("lightColor" + i);
                    this._uLightPos[i] = program.getLocation("lightPos" + i);
                    this._uLightDir[i] = null;
                    this._uLightAttenuation[i] = program.getLocation("lightAttenuation" + i);
                    break;
                case "spot":
                    this._uLightColor[i] = program.getLocation("lightColor" + i);
                    this._uLightPos[i] = program.getLocation("lightPos" + i);
                    this._uLightDir[i] = program.getLocation("lightDir" + i);
                    this._uLightAttenuation[i] = program.getLocation("lightAttenuation" + i);
                    break;
            }
        }

        if (lightsState.reflectionMaps.length > 0) {
            this._uReflectionMap = "reflectionMap";
        }

        if (lightsState.lightMaps.length > 0) {
            this._uLightMap = "lightMap";
        }

        this._uSectionPlanes = [];

        for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            this._uSectionPlanes.push({
                active: program.getLocation("sectionPlaneActive" + i),
                pos: program.getLocation("sectionPlanePos" + i),
                dir: program.getLocation("sectionPlaneDir" + i)
            });
        }

        this._aPosition = program.getAttribute("position");
        this._aOffset = program.getAttribute("offset");
        this._aNormal = program.getAttribute("normal");
        this._aUV = program.getAttribute("uv");
        this._aColor = program.getAttribute("color");
        this._aMetallicRoughness = program.getAttribute("metallicRoughness");
        this._aFlags = program.getAttribute("flags");
        this._aPickColor = program.getAttribute("pickColor");
        this._uPickZNear = program.getLocation("pickZNear");
        this._uPickZFar = program.getLocation("pickZFar");
        this._uPickClipPos = program.getLocation("pickClipPos");
        this._uDrawingBufferSize = program.getLocation("drawingBufferSize");

        this._uColorMap = "uColorMap";
        this._uMetallicRoughMap = "uMetallicRoughMap";
        this._uEmissiveMap = "uEmissiveMap";
        this._uNormalMap = "uNormalMap";
        this._uAOMap = "uAOMap";

        if (this._instancing) {

            this._aModelMatrix = program.getAttribute("modelMatrix");

            this._aModelMatrixCol0 = program.getAttribute("modelMatrixCol0");
            this._aModelMatrixCol1 = program.getAttribute("modelMatrixCol1");
            this._aModelMatrixCol2 = program.getAttribute("modelMatrixCol2");

            this._aModelNormalMatrixCol0 = program.getAttribute("modelNormalMatrixCol0");
            this._aModelNormalMatrixCol1 = program.getAttribute("modelNormalMatrixCol1");
            this._aModelNormalMatrixCol2 = program.getAttribute("modelNormalMatrixCol2");
        }

        if (this._withSAO) {
            this._uOcclusionTexture = "uOcclusionTexture";
            this._uSAOParams = program.getLocation("uSAOParams");
        }

        if (this._useAlphaCutoff) {
            this._alphaCutoffLocation = program.getLocation("materialAlphaCutoff");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }

        if (scene.pointsMaterial._state.filterIntensity) {
            this._uIntensityRange = program.getLocation("intensityRange");
        }

        this._uPointSize = program.getLocation("pointSize");
        this._uNearPlaneHeight = program.getLocation("nearPlaneHeight");

        if (scene.crossSections) {
            this._uSliceColor = program.getLocation("sliceColor");
            this._uSliceThickness = program.getLocation("sliceThickness");
        }
    }

    _bindProgram(frameCtx) {
        const scene = this._scene;
        const gl = scene.canvas.gl;
        const program = this._program;
        const lightsState = scene._lightsState;
        const lights = lightsState.lights;

        program.bind();

        frameCtx.textureUnit = 0;

        if (this._uLightAmbient) {
            gl.uniform4fv(this._uLightAmbient, lightsState.getAmbientColorAndIntensity());
        }

        if (this._uGammaFactor) {
            gl.uniform1f(this._uGammaFactor, scene.gammaFactor);
        }

        for (let i = 0, len = lights.length; i < len; i++) {

            const light = lights[i];

            if (this._uLightColor[i]) {
                gl.uniform4f(this._uLightColor[i], light.color[0], light.color[1], light.color[2], light.intensity);
            }
            if (this._uLightPos[i]) {
                gl.uniform3fv(this._uLightPos[i], light.pos);
                if (this._uLightAttenuation[i]) {
                    gl.uniform1f(this._uLightAttenuation[i], light.attenuation);
                }
            }
            if (this._uLightDir[i]) {
                gl.uniform3fv(this._uLightDir[i], light.dir);
            }
        }
    }

    _makeVAO(state) {
        const gl = this._scene.canvas.gl;
        const vao = gl.createVertexArray();
        gl.bindVertexArray(vao);

        if (this._instancing) {
            this._aModelMatrixCol0.bindArrayBuffer(state.modelMatrixCol0Buf);
            this._aModelMatrixCol1.bindArrayBuffer(state.modelMatrixCol1Buf);
            this._aModelMatrixCol2.bindArrayBuffer(state.modelMatrixCol2Buf);

            gl.vertexAttribDivisor(this._aModelMatrixCol0.location, 1);
            gl.vertexAttribDivisor(this._aModelMatrixCol1.location, 1);
            gl.vertexAttribDivisor(this._aModelMatrixCol2.location, 1);

            if (this._aModelNormalMatrixCol0) {
                this._aModelNormalMatrixCol0.bindArrayBuffer(state.modelNormalMatrixCol0Buf);
                gl.vertexAttribDivisor(this._aModelNormalMatrixCol0.location, 1);
            }
            if (this._aModelNormalMatrixCol1) {
                this._aModelNormalMatrixCol1.bindArrayBuffer(state.modelNormalMatrixCol1Buf);
                gl.vertexAttribDivisor(this._aModelNormalMatrixCol1.location, 1);
            }
            if (this._aModelNormalMatrixCol2) {
                this._aModelNormalMatrixCol2.bindArrayBuffer(state.modelNormalMatrixCol2Buf);
                gl.vertexAttribDivisor(this._aModelNormalMatrixCol2.location, 1);
            }

        }

        this._aPosition.bindArrayBuffer(state.positionsBuf);

        if (this._aUV) {
            this._aUV.bindArrayBuffer(state.uvBuf);
        }

        if (this._aNormal) {
            this._aNormal.bindArrayBuffer(state.normalsBuf);
        }

        if (this._aMetallicRoughness) {
            this._aMetallicRoughness.bindArrayBuffer(state.metallicRoughnessBuf);
            if (this._instancing) {
                gl.vertexAttribDivisor(this._aMetallicRoughness.location, 1);
            }
        }

        if (this._aColor) {
            this._aColor.bindArrayBuffer(state.colorsBuf);
            if (this._instancing && state.colorsBuf) {
                gl.vertexAttribDivisor(this._aColor.location, 1);
            }
        }

        if (this._aFlags) {
            this._aFlags.bindArrayBuffer(state.flagsBuf);
            if (this._instancing) {
                gl.vertexAttribDivisor(this._aFlags.location, 1);
            }
        }

        if (this._aOffset) {
            this._aOffset.bindArrayBuffer(state.offsetsBuf);
            if (this._instancing) {
                gl.vertexAttribDivisor(this._aOffset.location, 1);
            }
        }

        if (this._aPickColor) {
            this._aPickColor.bindArrayBuffer(state.pickColorsBuf);
            if (this._instancing) {
                gl.vertexAttribDivisor(this._aPickColor.location, 1);
            }
        }

        if (this._instancing) {
            if (this._edges && state.edgeIndicesBuf) {
                state.edgeIndicesBuf.bind();
            } else {
                if (state.indicesBuf) {
                    state.indicesBuf.bind();
                }
            }
        } else {
            if (this._edges && state.edgeIndicesBuf) {
                state.edgeIndicesBuf.bind();
            } else {
                if (state.indicesBuf) {
                    state.indicesBuf.bind();
                }
            }
        }

        return vao;
    }

    drawLayer(frameCtx, layer, renderPass, {colorUniform = false, incrementDrawState = false} = {}) {
        const maxTextureUnits = WEBGL_INFO.MAX_TEXTURE_IMAGE_UNITS;

        const scene = this._scene;
        const gl = scene.canvas.gl;
        const {_state: state, model} = layer;
        const {textureSet, origin, positionsDecodeMatrix} = state;
        const lightsState = scene._lightsState;
        const pointsMaterial = scene.pointsMaterial;
        const {camera} = model.scene;
        const {viewNormalMatrix, project} = camera;
        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;
        const {position, rotationMatrix, worldNormalMatrix} = model;

        if (!this._program) {
            this._allocate();
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram(frameCtx);
        }

        if (this._vaoCache.has(layer)) {
            gl.bindVertexArray(this._vaoCache.get(layer));
        } else {
            this._vaoCache.set(layer, this._makeVAO(state));
        }

        let offset = 0;
        const mat4Size = 4 * 4;

        this._matricesUniformBlockBufferData.set(rotationMatrix, 0);

        const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0);
        const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0);
        if (gotOrigin || gotPosition) {
            const rtcOrigin = tempVec3a$F;
            if (gotOrigin) {
                const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3c$w);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            this._matricesUniformBlockBufferData.set(createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$r), offset += mat4Size);
        } else {
            this._matricesUniformBlockBufferData.set(viewMatrix, offset += mat4Size);
        }

        this._matricesUniformBlockBufferData.set(frameCtx.pickProjMatrix || project.matrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(positionsDecodeMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(worldNormalMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(viewNormalMatrix, offset += mat4Size);

        gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer);
        gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW);

        gl.bindBufferBase(
            gl.UNIFORM_BUFFER,
            this._matricesUniformBlockBufferBindingPoint,
            this._matricesUniformBlockBuffer);


        gl.uniform1i(this._uRenderPass, renderPass);

        this.setSectionPlanesStateUniforms(layer);

        if (scene.logarithmicDepthBufferEnabled) {
            if (this._uLogDepthBufFC) {
                const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix?
                gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
            }
            if (this._uZFar) {
                gl.uniform1f(this._uZFar, scene.camera.project.far);
            }
        }

        if (this._uPickInvisible) {
            gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible);
        }

        if (this._uPickZNear) {
            gl.uniform1f(this._uPickZNear, frameCtx.pickZNear);
        }

        if (this._uPickZFar) {
            gl.uniform1f(this._uPickZFar, frameCtx.pickZFar);
        }

        if (this._uPickClipPos) {
            gl.uniform2fv(this._uPickClipPos, frameCtx.pickClipPos);
        }

        if (this._uDrawingBufferSize) {
            gl.uniform2f(this._uDrawingBufferSize, gl.drawingBufferWidth, gl.drawingBufferHeight);
        }

        if (this._uUVDecodeMatrix) {
            gl.uniformMatrix3fv(this._uUVDecodeMatrix, false, state.uvDecodeMatrix);
        }

        if (this._uIntensityRange && pointsMaterial.filterIntensity) {
            gl.uniform2f(this._uIntensityRange, pointsMaterial.minIntensity, pointsMaterial.maxIntensity);
        }

        if (this._uPointSize) {
            gl.uniform1f(this._uPointSize, pointsMaterial.pointSize);
        }

        if (this._uNearPlaneHeight) {
            const nearPlaneHeight = (scene.camera.projection === "ortho") ?
                1.0
                : (gl.drawingBufferHeight / (2 * Math.tan(0.5 * scene.camera.perspective.fov * Math.PI / 180.0)));
            gl.uniform1f(this._uNearPlaneHeight, nearPlaneHeight);
        }

        if (textureSet) {
            const {
                colorTexture,
                metallicRoughnessTexture,
                emissiveTexture,
                normalsTexture,
                occlusionTexture,
            } = textureSet;

            if (this._uColorMap && colorTexture) {
                this._program.bindTexture(this._uColorMap, colorTexture.texture, frameCtx.textureUnit);
                frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
            }
            if (this._uMetallicRoughMap && metallicRoughnessTexture) {
                this._program.bindTexture(this._uMetallicRoughMap, metallicRoughnessTexture.texture, frameCtx.textureUnit);
                frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
            }
            if (this._uEmissiveMap && emissiveTexture) {
                this._program.bindTexture(this._uEmissiveMap, emissiveTexture.texture, frameCtx.textureUnit);
                frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
            }
            if (this._uNormalMap && normalsTexture) {
                this._program.bindTexture(this._uNormalMap, normalsTexture.texture, frameCtx.textureUnit);
                frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
            }
            if (this._uAOMap && occlusionTexture) {
                this._program.bindTexture(this._uAOMap, occlusionTexture.texture, frameCtx.textureUnit);
                frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
            }

        }

        if (lightsState.reflectionMaps.length > 0 && lightsState.reflectionMaps[0].texture && this._uReflectionMap) {
            this._program.bindTexture(this._uReflectionMap, lightsState.reflectionMaps[0].texture, frameCtx.textureUnit);
            frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
            frameCtx.bindTexture++;
        }

        if (lightsState.lightMaps.length > 0 && lightsState.lightMaps[0].texture && this._uLightMap) {
            this._program.bindTexture(this._uLightMap, lightsState.lightMaps[0].texture, frameCtx.textureUnit);
            frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
            frameCtx.bindTexture++;
        }

        if (this._withSAO) {
            const sao = scene.sao;
            const saoEnabled = sao.possible;
            if (saoEnabled) {
                const viewportWidth = gl.drawingBufferWidth;
                const viewportHeight = gl.drawingBufferHeight;
                tempVec4[0] = viewportWidth;
                tempVec4[1] = viewportHeight;
                tempVec4[2] = sao.blendCutoff;
                tempVec4[3] = sao.blendFactor;
                gl.uniform4fv(this._uSAOParams, tempVec4);
                this._program.bindTexture(this._uOcclusionTexture, frameCtx.occlusionTexture, frameCtx.textureUnit);
                frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits;
                frameCtx.bindTexture++;
            }
        }

        if (this._useAlphaCutoff) {
            gl.uniform1f(this._alphaCutoffLocation, textureSet.alphaCutoff);
        }

        if (colorUniform) {
            const colorKey = this._edges ? "edgeColor" : "fillColor";
            const alphaKey = this._edges ? "edgeAlpha" : "fillAlpha";

            if (renderPass === RENDER_PASSES[`${this._edges ? "EDGES" : "SILHOUETTE"}_XRAYED`]) {
                const material = scene.xrayMaterial._state;
                const color = material[colorKey];
                const alpha = material[alphaKey];
                gl.uniform4f(this._uColor, color[0], color[1], color[2], alpha);

            } else if (renderPass === RENDER_PASSES[`${this._edges ? "EDGES" : "SILHOUETTE"}_HIGHLIGHTED`]) {
                const material = scene.highlightMaterial._state;
                const color = material[colorKey];
                const alpha = material[alphaKey];
                gl.uniform4f(this._uColor, color[0], color[1], color[2], alpha);

            } else if (renderPass === RENDER_PASSES[`${this._edges ? "EDGES" : "SILHOUETTE"}_SELECTED`]) {
                const material = scene.selectedMaterial._state;
                const color = material[colorKey];
                const alpha = material[alphaKey];
                gl.uniform4f(this._uColor, color[0], color[1], color[2], alpha);

            } else {
                gl.uniform4fv(this._uColor, this._edges ? edgesDefaultColor : defaultColor$2);
            }
        }

        this._draw({state, frameCtx, incrementDrawState});

        gl.bindVertexArray(null);
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
        stats.memory.programs--;
    }
}

/**
 * @private
 */
class TrianglesBatchingRenderer extends VBORenderer {

    constructor(scene, withSAO, {edges = false, useAlphaCutoff = false} = {}) {
        super(scene, withSAO, {instancing: false, edges, useAlphaCutoff});
    }

    _draw(drawCfg) {
        const {gl} = this._scene.canvas;

        const {
            state,
            frameCtx,
            incrementDrawState
        } = drawCfg;

        if (this._edges) {
            if (state.edgeIndicesBuf) {
                gl.drawElements(gl.LINES, state.edgeIndicesBuf.numItems, state.edgeIndicesBuf.itemType, 0);
            }
        } else {
            const count = frameCtx.pickElementsCount || state.indicesBuf.numItems;
            const offset = frameCtx.pickElementsOffset ? frameCtx.pickElementsOffset * state.indicesBuf.itemByteSize : 0;
            gl.drawElements(gl.TRIANGLES, count, state.indicesBuf.itemType, offset);

            if (incrementDrawState) {
                frameCtx.drawElements++;
            }
        }
    }
}

/**
 * @private
 */
class TrianglesColorRenderer$1 extends TrianglesBatchingRenderer {
    _getHash() {
        const scene = this._scene;
        return [scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";");
    }

    drawLayer(frameCtx, layer, renderPass) {
        super.drawLayer(frameCtx, layer, renderPass, { incrementDrawState: true });
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const lightsState = scene._lightsState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        let light;
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles batching draw vertex shader");

        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        src.push("in vec3 normal;");
        src.push("in vec4 color;");
        src.push("in float flags;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        this._addMatricesUniformBlockLines(src, true);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        src.push("uniform vec4 lightAmbient;");

        for (let i = 0, len = lightsState.lights.length; i < len; i++) {
            light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            src.push("uniform vec4 lightColor" + i + ";");
            if (light.type === "dir") {
                src.push("uniform vec3 lightDir" + i + ";");
            }
            if (light.type === "point") {
                src.push("uniform vec3 lightPos" + i + ";");
            }
            if (light.type === "spot") {
                src.push("uniform vec3 lightPos" + i + ";");
                src.push("uniform vec3 lightDir" + i + ";");
            }
        }

        src.push("vec3 octDecode(vec2 oct) {");
        src.push("    vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));");
        src.push("    if (v.z < 0.0) {");
        src.push("        v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);");
        src.push("    }");
        src.push("    return normalize(v);");
        src.push("}");

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec4 vColor;");

        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");

        src.push("vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");

        src.push("vec4 worldNormal =  worldNormalMatrix * vec4(octDecode(normal.xy), 0.0); ");

        src.push("vec3 viewNormal = normalize((viewNormalMatrix * worldNormal).xyz);");

        src.push("vec3 reflectedColor = vec3(0.0, 0.0, 0.0);");
        src.push("vec3 viewLightDir = vec3(0.0, 0.0, -1.0);");

        src.push("float lambertian = 1.0;");
        for (let i = 0, len = lightsState.lights.length; i < len; i++) {
            light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            if (light.type === "dir") {
                if (light.space === "view") {
                    src.push("viewLightDir = normalize(lightDir" + i + ");");
                } else {
                    src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "point") {
                if (light.space === "view") {
                    src.push("viewLightDir = -normalize(lightPos" + i + " - viewPosition.xyz);");
                } else {
                    src.push("viewLightDir = -normalize((viewMatrix * vec4(lightPos" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "spot") {
                if (light.space === "view") {
                    src.push("viewLightDir = normalize(lightDir" + i + ");");
                } else {
                    src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else {
                continue;
            }
            src.push("lambertian = max(dot(-viewNormal, viewLightDir), 0.0);");
            src.push("reflectedColor += lambertian * (lightColor" + i + ".rgb * lightColor" + i + ".a);");
        }

        src.push("vec3 rgb = (vec3(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0));");
        src.push("vColor =  vec4((lightAmbient.rgb * lightAmbient.a * rgb) + (reflectedColor * rgb), float(color.a) / 255.0);");

        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("gl_Position = clipPos;");
        src.push("}");

        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles batching draw fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }

        if (this._withSAO) {
            src.push("uniform sampler2D uOcclusionTexture;");
            src.push("uniform vec4      uSAOParams;");
            src.push("const float       packUpscale = 256. / 255.;");
            src.push("const float       unpackDownScale = 255. / 256.;");
            src.push("const vec3        packFactors = vec3( 256. * 256. * 256., 256. * 256.,  256. );");
            src.push("const vec4        unPackFactors = unpackDownScale / vec4( packFactors, 1. );");

            src.push("float unpackRGBToFloat( const in vec4 v ) {");
            src.push("    return dot( v, unPackFactors );");
            src.push("}");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
            src.push("uniform float sliceThickness;");
            src.push("uniform vec4 sliceColor;");
        }
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        src.push("  vec4 newColor;");
        src.push("  newColor = vColor;");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > sliceThickness) { ");
            src.push("      discard;");
            src.push("  }");
            src.push("  if (dist > 0.0) { ");
            src.push("      newColor = sliceColor;");
            src.push("  }");
            src.push("}");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    float dx = dFdx(vFragDepth);");
            src.push("    float dy = dFdy(vFragDepth);");
            src.push("    float diff = sqrt(dx*dx+dy*dy);");
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth + diff ) * logDepthBufFC * 0.5;");
        } else {
            src.push("    float dx = dFdx(gl_FragCoord.z);");
            src.push("    float dy = dFdy(gl_FragCoord.z);");
            src.push("    float diff = sqrt(dx*dx+dy*dy);");
            src.push("    gl_FragDepth = gl_FragCoord.z + diff;");
        }

        if (this._withSAO) {
            // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top
            // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject
            src.push("   float viewportWidth     = uSAOParams[0];");
            src.push("   float viewportHeight    = uSAOParams[1];");
            src.push("   float blendCutoff       = uSAOParams[2];");
            src.push("   float blendFactor       = uSAOParams[3];");
            src.push("   vec2 uv                 = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);");
            src.push("   float ambient           = smoothstep(blendCutoff, 1.0, unpackRGBToFloat(texture(uOcclusionTexture, uv))) * blendFactor;");
            src.push("   outColor            = vec4(newColor.rgb * ambient, 1.0);");
        } else {
            src.push("   outColor            = newColor;");
        }

        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class TrianglesFlatColorRenderer$1 extends TrianglesBatchingRenderer {
    _getHash() {
        const scene = this._scene;
        return [scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";");
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles batching flat-shading draw vertex shader");

        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        src.push("in vec4 color;");
        src.push("in float flags;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        this._addMatricesUniformBlockLines(src);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec4 vViewPosition;");
        src.push("out vec4 vColor;");

        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");

        src.push("vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("vViewPosition = viewPosition;");
        src.push("vColor = vec4(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0, float(color.a) / 255.0);");

        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("gl_Position = clipPos;");
        src.push("}");

        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const lightsState = scene._lightsState;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles batching flat-shading draw fragment shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }

        if (this._withSAO) {
            src.push("uniform sampler2D uOcclusionTexture;");
            src.push("uniform vec4      uSAOParams;");

            src.push("const float       packUpscale = 256. / 255.;");
            src.push("const float       unpackDownScale = 255. / 256.;");
            src.push("const vec3        packFactors = vec3( 256. * 256. * 256., 256. * 256.,  256. );");
            src.push("const vec4        unPackFactors = unpackDownScale / vec4( packFactors, 1. );");

            src.push("float unpackRGBToFloat( const in vec4 v ) {");
            src.push("    return dot( v, unPackFactors );");
            src.push("}");
        }

        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
            src.push("uniform float sliceThickness;");
            src.push("uniform vec4 sliceColor;");
        }

        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec4 lightAmbient;");
        for (let i = 0, len = lightsState.lights.length; i < len; i++) {
            const light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            src.push("uniform vec4 lightColor" + i + ";");
            if (light.type === "dir") {
                src.push("uniform vec3 lightDir" + i + ";");
            }
            if (light.type === "point") {
                src.push("uniform vec3 lightPos" + i + ";");
            }
            if (light.type === "spot") {
                src.push("uniform vec3 lightPos" + i + ";");
                src.push("uniform vec3 lightDir" + i + ";");
            }
        }

        src.push("in vec4 vViewPosition;");
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");

        src.push("void main(void) {");
        src.push("  vec4 newColor;");
        src.push("  newColor = vColor;");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > sliceThickness) { ");
            src.push("      discard;");
            src.push("  }");
            src.push("  if (dist > 0.0) { ");
            src.push("      newColor = sliceColor;");
            src.push("  }");
            src.push("}");
        }

        src.push("vec3 reflectedColor = vec3(0.0, 0.0, 0.0);");
        src.push("vec3 viewLightDir = vec3(0.0, 0.0, -1.0);");

        src.push("float lambertian = 1.0;");

        src.push("vec3 xTangent = dFdx( vViewPosition.xyz );");
        src.push("vec3 yTangent = dFdy( vViewPosition.xyz );");
        src.push("vec3 viewNormal = normalize( cross( xTangent, yTangent ) );");

        for (let i = 0, len = lightsState.lights.length; i < len; i++) {
            const light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            if (light.type === "dir") {
                if (light.space === "view") {
                    src.push("viewLightDir = normalize(lightDir" + i + ");");
                } else {
                    src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "point") {
                if (light.space === "view") {
                    src.push("viewLightDir = -normalize(lightPos" + i + " - viewPosition.xyz);");
                } else {
                    src.push("viewLightDir = -normalize((viewMatrix * vec4(lightPos" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "spot") {
                if (light.space === "view") {
                    src.push("viewLightDir = normalize(lightDir" + i + ");");
                } else {
                    src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else {
                continue;
            }

            src.push("lambertian = max(dot(-viewNormal, viewLightDir), 0.0);");
            src.push("reflectedColor += lambertian * (lightColor" + i + ".rgb * lightColor" + i + ".a);");
        }

        src.push("vec4 fragColor =  vec4((lightAmbient.rgb * lightAmbient.a * newColor.rgb) + (reflectedColor * newColor.rgb), newColor.a);");

        if (this._withSAO) {
            // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top
            // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject
            src.push("   float viewportWidth     = uSAOParams[0];");
            src.push("   float viewportHeight    = uSAOParams[1];");
            src.push("   float blendCutoff       = uSAOParams[2];");
            src.push("   float blendFactor       = uSAOParams[3];");
            src.push("   vec2 uv                 = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);");
            src.push("   float ambient           = smoothstep(blendCutoff, 1.0, unpackRGBToFloat(texture(uOcclusionTexture, uv))) * blendFactor;");
            src.push("   outColor            = vec4(fragColor.rgb * ambient, 1.0);");
        } else {
            src.push("   outColor            = fragColor;");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        } else {
            src.push("    float dx = dFdx(gl_FragCoord.z);");
            src.push("    float dy = dFdy(gl_FragCoord.z);");
            src.push("    float diff = sqrt(dx*dx+dy*dy);");
            src.push("    gl_FragDepth = gl_FragCoord.z + diff;");
        }

        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class TrianglesSilhouetteRenderer$1 extends TrianglesBatchingRenderer {

    drawLayer(frameCtx, batchingLayer, renderPass) {
        super.drawLayer(frameCtx, batchingLayer, renderPass, { colorUniform: true });
    }

    _buildVertexShader() {

        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;

        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles batching silhouette vertex shader");

        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("in vec4 color;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec4 silhouetteColor;");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }

        src.push("out vec4 vColor;");

        src.push("void main(void) {");

        // silhouetteFlag = NOT_RENDERED | SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | SILHOUETTE_XRAYED
        // renderPass = SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED

        src.push(`int silhouetteFlag = int(flags) >> 4 & 0xF;`);
        src.push(`if (silhouetteFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("} else {");

        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("vColor = vec4(silhouetteColor.r, silhouetteColor.g, silhouetteColor.b, min(silhouetteColor.a, color.a ));");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        let i;
        let len;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles batching silhouette fragment shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
            src.push("uniform float sliceThickness;");
            src.push("uniform vec4 sliceColor;");
        }
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        src.push("  vec4 newColor;");
        src.push("  newColor = vColor;");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > sliceThickness) { ");
            src.push("      discard;");
            src.push("  }");
            src.push("  if (dist > 0.0) { ");
            src.push("      newColor = sliceColor;");
            src.push("  }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outColor = newColor;");
        src.push("}");
        return src;
    }
}

/**
 * @private
 */


class EdgesRenderer$1 extends TrianglesBatchingRenderer {
    constructor(scene) {
        super(scene, false, {instancing: false, edges: true});
    }
}

/**
 * @private
 */
class EdgesEmphasisRenderer$1 extends EdgesRenderer$1 {

    drawLayer(frameCtx, batchingLayer, renderPass) {
        super.drawLayer(frameCtx, batchingLayer, renderPass, { colorUniform: true });
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];

        src.push("#version 300 es");
        src.push("// EdgesEmphasisRenderer vertex shader");

        src.push("uniform int renderPass;");
        src.push("uniform vec4 color;");

        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");

        this._addMatricesUniformBlockLines(src);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }

        src.push("out vec4 vColor;");

        src.push("void main(void) {");

        // edgeFlag = NOT_RENDERED | EDGES_COLOR_OPAQUE | EDGES_COLOR_TRANSPARENT | EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED
        // renderPass = EDGES_COLOR_OPAQUE | EDGES_COLOR_TRANSPARENT | EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED

        src.push(`int edgeFlag = int(flags) >> 8 & 0xF;`);
        src.push(`if (edgeFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");

        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");

        if (clipping) {
            src.push("  vWorldPosition = worldPosition;");
            src.push("  vFlags = flags;");
        }

        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("vColor = vec4(color.r, color.g, color.b, color.a);");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// EdgesEmphasisRenderer fragment shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > 0.0) { discard; }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outColor = vColor;");
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class EdgesColorRenderer$1 extends EdgesRenderer$1 {

    drawLayer(frameCtx, batchingLayer, renderPass) {
        super.drawLayer(frameCtx, batchingLayer, renderPass, { colorUniform: false });
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Batched geometry edges drawing vertex shader");

        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        src.push("in vec4 color;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");

        this._addMatricesUniformBlockLines(src);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }

        src.push("out vec4 vColor;");
        src.push("void main(void) {");

        // edgeFlag = NOT_RENDERED | EDGES_COLOR_OPAQUE | EDGES_COLOR_TRANSPARENT | EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED
        // renderPass = EDGES_COLOR_OPAQUE | EDGES_COLOR_TRANSPARENT

        src.push(`int edgeFlag = int(flags) >> 8 & 0xF;`);
        src.push(`if (edgeFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");

        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");

        if (clipping) {
            src.push("  vWorldPosition = worldPosition;");
            src.push("  vFlags = flags;");
        }

        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        //src.push("vColor = vec4(float(color.r-100.0) / 255.0, float(color.g-100.0) / 255.0, float(color.b-100.0) / 255.0, float(color.a) / 255.0);");
        src.push("vColor = vec4(float(color.r*0.5) / 255.0, float(color.g*0.5) / 255.0, float(color.b*0.5) / 255.0, float(color.a) / 255.0);");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Batched geometry edges drawing fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > 0.0) { discard; }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outColor = vColor;");
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class TrianglesPickMeshRenderer$1 extends TrianglesBatchingRenderer {

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Batched geometry picking vertex shader");
        

        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");

        src.push("in vec4 pickColor;");

        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }

        this._addRemapClipPosLines(src);

        src.push("out vec4 vPickColor;");

        src.push("void main(void) {");

        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK

        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("  } else {");
        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("      vPickColor = vec4(float(pickColor.r) / 255.0, float(pickColor.g) / 255.0, float(pickColor.b) / 255.0, float(pickColor.a) / 255.0);");
        if (clipping) {
            src.push("      vWorldPosition = worldPosition;");
            src.push("      vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
           src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = remapClipPos(clipPos);");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Batched geometry picking fragment shader");
        
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vPickColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("   outColor = vPickColor; ");
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class TrianglesPickDepthRenderer$1 extends TrianglesBatchingRenderer {

    _buildVertexShader() {

        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles batching pick depth vertex shader");

        

        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");

        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }

        this._addRemapClipPosLines(src);

        src.push("out vec4 vViewPosition;");
        src.push("void main(void) {");

        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK

        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("      gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("  } else {");
        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("      vWorldPosition = worldPosition;");
            src.push("      vFlags = flags;");
        }
        src.push("vViewPosition = viewPosition;");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
           src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = remapClipPos(clipPos);");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {

        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles batching pick depth fragment shader");

        

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }

        src.push("uniform float pickZNear;");
        src.push("uniform float pickZFar;");

        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vViewPosition;");
        src.push("vec4 packDepth(const in float depth) {");
        src.push("  const vec4 bitShift = vec4(256.0*256.0*256.0, 256.0*256.0, 256.0, 1.0);");
        src.push("  const vec4 bitMask  = vec4(0.0, 1.0/256.0, 1.0/256.0, 1.0/256.0);");
        src.push("  vec4 res = fract(depth * bitShift);");
        src.push("  res -= res.xxyz * bitMask;");
        src.push("  return res;");
        src.push("}");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("    float zNormalizedDepth = abs((pickZNear + vViewPosition.z) / (pickZFar - pickZNear));");
        src.push("    outColor = packDepth(zNormalizedDepth); ");  // Must be linear depth
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class TrianglesPickNormalsRenderer$1 extends TrianglesBatchingRenderer {

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles batching pick normals vertex shader");
        
        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in vec3 normal;");
        src.push("in float flags;");
        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        this._addRemapClipPosLines(src, 3);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }
        src.push("vec3 octDecode(vec2 oct) {");
        src.push("    vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));");
        src.push("    if (v.z < 0.0) {");
        src.push("        v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);");
        src.push("    }");
        src.push("    return normalize(v);");
        src.push("}");
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec3 vWorldNormal;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK
        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("      gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("  } else {");
        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("      vec3 worldNormal =  octDecode(normal.xy); ");
        src.push("      vWorldNormal = worldNormal;");
        if (clipping) {
            src.push("      vWorldPosition = worldPosition;");
            src.push("      vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
           src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = remapClipPos(clipPos);");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles batching pick normals fragment shader");
        
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec3 vWorldNormal;");
        src.push("out highp ivec4 outNormal;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push(`    outNormal = ivec4(vWorldNormal * float(${math.MAX_INT}), 1.0);`);
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class TrianglesOcclusionRenderer$1 extends TrianglesBatchingRenderer {

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles batching occlusion vertex shader");
        
        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in vec4 color;");
        src.push("in float flags;");

        this._addMatricesUniformBlockLines(src);
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE
        // Only opaque objects can be occluders

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("      gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("  } else {");
        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }

        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("      vWorldPosition = worldPosition;");
            src.push("      vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        src.push("gl_Position = clipPos;");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles batching occlusion fragment shader");
        
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        src.push("   outColor = vec4(0.0, 0.0, 1.0, 1.0); "); // Occluders are blue
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class TrianglesDepthRenderer$1 extends TrianglesBatchingRenderer {

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles batching depth vertex shader");

        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");

        this._addMatricesUniformBlockLines(src);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec2 vHighPrecisionZW;");
        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("      gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("  } else {");
        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("      vWorldPosition = worldPosition;");
            src.push("      vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("vHighPrecisionZW = gl_Position.zw;");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = (sectionPlanesState.getNumAllocatedSectionPlanes() > 0);
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles batching depth fragment shader");

        src.push("precision highp float;");
        src.push("precision highp int;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec2 vHighPrecisionZW;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("float fragCoordZ = 0.5 * vHighPrecisionZW[0] / vHighPrecisionZW[1] + 0.5;");
        src.push("    outColor = vec4(vec3(1.0 - fragCoordZ), 1.0); ");
        src.push("}");
        return src;
    }
}

/**
 * @private
 */


class TrianglesNormalsRenderer$1 extends TrianglesBatchingRenderer {

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Batched geometry normals vertex shader");
        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in vec3 normal;");
        src.push("in vec4 color;");
        src.push("in float flags;");

        this._addMatricesUniformBlockLines(src, true);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }
        src.push("vec3 octDecode(vec2 oct) {");
        src.push("    vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));");
        src.push("    if (v.z < 0.0) {");
        src.push("        v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);");
        src.push("    }");
        src.push("    return normalize(v);");
        src.push("}");
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec3 vViewNormal;");
        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("      gl_Position = vec4(0.0, 0.0, 0.0, 0.0);");

        src.push("  } else {");
        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("      vec4 viewPosition   = viewMatrix * worldPosition; ");
        src.push("      vec4 worldNormal    = worldNormalMatrix * vec4(octDecode(normal.xy), 0.0); ");
        src.push("      vec3 viewNormal     = normalize((viewNormalMatrix * worldNormal).xyz);");
        if (clipping) {
            src.push("      vWorldPosition  = worldPosition;");
            src.push("      vFlags         = flags;");
        }
        src.push("      vViewNormal = viewNormal;");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = (sectionPlanesState.getNumAllocatedSectionPlanes() > 0);
        const src = [];
        src.push("#version 300 es");
        src.push("// Batched geometry normals fragment shader");


        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }

        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec3 vViewNormal;");
        src.push("vec3 packNormalToRGB( const in vec3 normal ) {");
        src.push("    return normalize( normal ) * 0.5 + 0.5;");
        src.push("}");
        src.push("out vec4 outColor;");

        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("    outColor = vec4(packNormalToRGB(vViewNormal), 1.0); ");
        src.push("}");
        return src;
    }
}

/**
 * Renders BatchingLayer fragment depths to a shadow map.
 *
 * @private
 */
class TrianglesShadowRenderer$1 extends TrianglesBatchingRenderer {

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Batched geometry shadow vertex shader");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in vec4 color;");
        src.push("in float flags;");
        src.push("uniform mat4 shadowViewMatrix;");
        src.push("uniform mat4 shadowProjMatrix;");

        this._addMatricesUniformBlockLines(src);

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec4 vViewPosition;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        src.push(`  int colorFlag = int(flags) & 0xF;`);
        src.push("  bool visible        = (colorFlag > 0);");
        src.push("  bool transparent    = ((float(color.a) / 255.0) < 1.0);");
        src.push("  if (!visible || transparent) {");
        src.push("      gl_Position = vec4(0.0, 0.0, 0.0, 0.0);");
        src.push("  } else {");
        src.push("      vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("      vec4 viewPosition  = shadowViewMatrix * worldPosition; ");
        if (clipping) {
            src.push("      vWorldPosition = worldPosition;");
            src.push("      vFlags = flags;");
        }
        src.push("      vViewPosition = viewPosition;");
        src.push("      gl_Position = shadowProjMatrix * viewPosition;");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = (sectionPlanesState.getNumAllocatedSectionPlanes() > 0);
        const src = [];
        src.push("#version 300 es");
        src.push("// Batched geometry shadow fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vViewPosition;");

        src.push("vec4 encodeFloat( const in float v ) {");
        src.push("  const vec4 bitShift = vec4(256 * 256 * 256, 256 * 256, 256, 1.0);");
        src.push("  const vec4 bitMask = vec4(0, 1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0);");
        src.push("  vec4 comp = fract(v * bitShift);");
        src.push("  comp -= comp.xxyz * bitMask;");
        src.push("  return comp;");
        src.push("}");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        src.push("    outColor = encodeFloat( gl_FragCoord.z); ");
        src.push("}");
        return src;
    }
}

// const TEXTURE_DECODE_FUNCS = {};
// TEXTURE_DECODE_FUNCS[LinearEncoding] = "linearToLinear";
// TEXTURE_DECODE_FUNCS[sRGBEncoding] = "sRGBToLinear";

/**
 * @private
 */
class TrianglesPBRRenderer$1 extends TrianglesBatchingRenderer {
    _getHash() {
        const scene = this._scene;
        return [scene.gammaOutput, scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";");
    }

    drawLayer(frameCtx, layer, renderPass) {
        super.drawLayer(frameCtx, layer, renderPass, { incrementDrawState: true });
    }

    _buildVertexShader() {

        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const lightsState = scene._lightsState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const clippingCaps = sectionPlanesState.clippingCaps;

        const src = [];

        src.push("#version 300 es");
        src.push("// Triangles batching quality draw vertex shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");

        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        src.push("in vec3 normal;");
        src.push("in vec4 color;");
        src.push("in vec2 uv;");
        src.push("in vec2 metallicRoughness;");
        src.push("in float flags;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        this._addMatricesUniformBlockLines(src, true);

        src.push("uniform mat3 uvDecodeMatrix;");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        src.push("vec3 octDecode(vec2 oct) {");
        src.push("    vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));");
        src.push("    if (v.z < 0.0) {");
        src.push("        v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);");
        src.push("    }");
        src.push("    return normalize(v);");
        src.push("}");

        src.push("out vec4 vViewPosition;");
        src.push("out vec3 vViewNormal;");
        src.push("out vec4 vColor;");
        src.push("out vec2 vUV;");
        src.push("out vec2 vMetallicRoughness;");

        if (lightsState.lightMaps.length > 0) {
            src.push("out vec3 vWorldNormal;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
            if (clippingCaps) {
                src.push("out vec4 vClipPosition;");
            }
        }

        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");

        src.push("vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("vec4 worldNormal =  worldNormalMatrix * vec4(octDecode(normal.xy), 0.0); ");
        src.push("vec3 viewNormal = normalize((viewNormalMatrix * worldNormal).xyz);");

        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
            src.push("vFragDepth = 1.0 + clipPos.w;");
        }

        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
            if (clippingCaps) {
                src.push("vClipPosition = clipPos;");
            }
        }

        src.push("vViewPosition = viewPosition;");
        src.push("vViewNormal = viewNormal;");
        src.push("vColor = color;");
        src.push("vUV = (uvDecodeMatrix * vec3(uv, 1.0)).xy;");
        src.push("vMetallicRoughness = metallicRoughness;");

        if (lightsState.lightMaps.length > 0) {
            src.push("vWorldNormal = worldNormal.xyz;");
        }

        src.push("gl_Position = clipPos;");
        src.push("}");

        src.push("}");
        return src;
    }

    _buildFragmentShader() {

        const scene = this._scene;
        const gammaOutput = scene.gammaOutput; // If set, then it expects that all textures and colors need to be outputted in premultiplied gamma. Default is false.
        const sectionPlanesState = scene._sectionPlanesState;
        const lightsState = scene._lightsState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const clippingCaps = sectionPlanesState.clippingCaps;
        const src = [];

        src.push('#version 300 es');
        src.push("// Triangles batching quality draw fragment shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }

        src.push("uniform sampler2D uColorMap;");
        src.push("uniform sampler2D uMetallicRoughMap;");
        src.push("uniform sampler2D uEmissiveMap;");
        src.push("uniform sampler2D uNormalMap;");
        src.push("uniform sampler2D uAOMap;");

        src.push("in vec4 vViewPosition;");
        src.push("in vec3 vViewNormal;");
        src.push("in vec4 vColor;");
        src.push("in vec2 vUV;");
        src.push("in vec2 vMetallicRoughness;");

        if (lightsState.lightMaps.length > 0) {
            src.push("in vec3 vWorldNormal;");
        }

        this._addMatricesUniformBlockLines(src, true);

        if (lightsState.reflectionMaps.length > 0) {
            src.push("uniform samplerCube reflectionMap;");
        }

        if (lightsState.lightMaps.length > 0) {
            src.push("uniform samplerCube lightMap;");
        }

        src.push("uniform vec4 lightAmbient;");

        for (let i = 0, len = lightsState.lights.length; i < len; i++) {
            const light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            src.push("uniform vec4 lightColor" + i + ";");
            if (light.type === "dir") {
                src.push("uniform vec3 lightDir" + i + ";");
            }
            if (light.type === "point") {
                src.push("uniform vec3 lightPos" + i + ";");
            }
            if (light.type === "spot") {
                src.push("uniform vec3 lightPos" + i + ";");
                src.push("uniform vec3 lightDir" + i + ";");
            }
        }

        if (this._withSAO) {
            src.push("uniform sampler2D uOcclusionTexture;");
            src.push("uniform vec4      uSAOParams;");

            src.push("const float       packUpscale = 256. / 255.;");
            src.push("const float       unpackDownScale = 255. / 256.;");
            src.push("const vec3        packFactors = vec3( 256. * 256. * 256., 256. * 256.,  256. );");
            src.push("const vec4        unPackFactors = unpackDownScale / vec4( packFactors, 1. );");

            src.push("float unpackRGBToFloat( const in vec4 v ) {");
            src.push("    return dot( v, unPackFactors );");
            src.push("}");
        }

        src.push("uniform float gammaFactor;");
        src.push("vec4 linearToLinear( in vec4 value ) {");
        src.push("  return value;");
        src.push("}");
        src.push("vec4 sRGBToLinear( in vec4 value ) {");
        src.push("  return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.w );");
        src.push("}");
        src.push("vec4 gammaToLinear( in vec4 value) {");
        src.push("  return vec4( pow( value.xyz, vec3( gammaFactor ) ), value.w );");
        src.push("}");

        if (gammaOutput) {
            src.push("vec4 linearToGamma( in vec4 value, in float gammaFactor ) {");
            src.push("  return vec4( pow( value.xyz, vec3( 1.0 / gammaFactor ) ), value.w );");
            src.push("}");
        }

        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            if (clippingCaps) {
                src.push("in vec4 vClipPosition;");
            }
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }

        // CONSTANT DEFINITIONS

        src.push("#define PI 3.14159265359");
        src.push("#define RECIPROCAL_PI 0.31830988618");
        src.push("#define RECIPROCAL_PI2 0.15915494");
        src.push("#define EPSILON 1e-6");

        src.push("#define saturate(a) clamp( a, 0.0, 1.0 )");

        // UTILITY DEFINITIONS

        src.push("vec3 perturbNormal2Arb( vec3 eye_pos, vec3 surf_norm, vec2 uv ) {");
        src.push("       vec3 texel = texture( uNormalMap, uv ).xyz;");
        src.push("       if (texel.x == 0.0 && texel.y == 0.0 && texel.z == 0.0) {");
        src.push("              return surf_norm;");
        src.push("       }");
        src.push("      vec3 q0 = vec3( dFdx( eye_pos.x ), dFdx( eye_pos.y ), dFdx( eye_pos.z ) );");
        src.push("      vec3 q1 = vec3( dFdy( eye_pos.x ), dFdy( eye_pos.y ), dFdy( eye_pos.z ) );");
        src.push("      vec2 st0 = dFdx( uv.st );");
        src.push("      vec2 st1 = dFdy( uv.st );");
        src.push("      vec3 S = normalize( q0 * st1.t - q1 * st0.t );");
        src.push("      vec3 T = normalize( -q0 * st1.s + q1 * st0.s );");
        src.push("      vec3 N = normalize( surf_norm );");
        src.push("      vec3 mapN = texel.xyz * 2.0 - 1.0;");
        src.push("      mat3 tsn = mat3( S, T, N );");
            //src.push("      mapN *= 3.0;");
        src.push("      return normalize( tsn * mapN );");
        src.push("}");

        src.push("vec3 inverseTransformDirection(in vec3 dir, in mat4 matrix) {");
        src.push("   return normalize( ( vec4( dir, 0.0 ) * matrix ).xyz );");
        src.push("}");

        // STRUCTURES

        src.push("struct IncidentLight {");
        src.push("   vec3 color;");
        src.push("   vec3 direction;");
        src.push("};");

        src.push("struct ReflectedLight {");
        src.push("   vec3 diffuse;");
        src.push("   vec3 specular;");
        src.push("};");

        src.push("struct Geometry {");
        src.push("   vec3 position;");
        src.push("   vec3 viewNormal;");
        src.push("   vec3 worldNormal;");
        src.push("   vec3 viewEyeDir;");
        src.push("};");

        src.push("struct Material {");
        src.push("   vec3  diffuseColor;");
        src.push("   float specularRoughness;");
        src.push("   vec3  specularColor;");
        src.push("   float shine;"); // Only used for Phong
        src.push("};");

        // IRRADIANCE EVALUATION

        src.push("float GGXRoughnessToBlinnExponent(const in float ggxRoughness) {");
        src.push("   float r = ggxRoughness + 0.0001;");
        src.push("   return (2.0 / (r * r) - 2.0);");
        src.push("}");

        src.push("float getSpecularMIPLevel(const in float blinnShininessExponent, const in int maxMIPLevel) {");
        src.push("   float maxMIPLevelScalar = float( maxMIPLevel );");
        src.push("   float desiredMIPLevel = maxMIPLevelScalar - 0.79248 - 0.5 * log2( ( blinnShininessExponent * blinnShininessExponent ) + 1.0 );");
        src.push("   return clamp( desiredMIPLevel, 0.0, maxMIPLevelScalar );");
        src.push("}");

        if (lightsState.reflectionMaps.length > 0) {
            src.push("vec3 getLightProbeIndirectRadiance(const in vec3 reflectVec, const in float blinnShininessExponent, const in int maxMIPLevel) {");
            src.push("   float mipLevel = 0.5 * getSpecularMIPLevel(blinnShininessExponent, maxMIPLevel);"); //TODO: a random factor - fix this
            src.push("   vec3 envMapColor = sRGBToLinear(texture(reflectionMap, reflectVec, mipLevel)).rgb;");
            src.push("  return envMapColor;");
            src.push("}");
        }

        // SPECULAR BRDF EVALUATION

        src.push("vec3 F_Schlick(const in vec3 specularColor, const in float dotLH) {");
        src.push("   float fresnel = exp2( ( -5.55473 * dotLH - 6.98316 ) * dotLH );");
        src.push("   return ( 1.0 - specularColor ) * fresnel + specularColor;");
        src.push("}");

        src.push("float G_GGX_Smith(const in float alpha, const in float dotNL, const in float dotNV) {");
        src.push("   float a2 = ( alpha * alpha );");
        src.push("   float gl = dotNL + sqrt( a2 + ( 1.0 - a2 ) * ( dotNL * dotNL ) );");
        src.push("   float gv = dotNV + sqrt( a2 + ( 1.0 - a2 ) * ( dotNV * dotNV ) );");
        src.push("   return 1.0 / ( gl * gv );");
        src.push("}");

        src.push("float G_GGX_SmithCorrelated(const in float alpha, const in float dotNL, const in float dotNV) {");
        src.push("   float a2 = ( alpha * alpha );");
        src.push("   float gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * ( dotNV * dotNV ) );");
        src.push("   float gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * ( dotNL * dotNL ) );");
        src.push("   return 0.5 / max( gv + gl, EPSILON );");
        src.push("}");

        src.push("float D_GGX(const in float alpha, const in float dotNH) {");
        src.push("   float a2 = ( alpha * alpha );");
        src.push("   float denom = ( dotNH * dotNH) * ( a2 - 1.0 ) + 1.0;");
        src.push("   return RECIPROCAL_PI * a2 / ( denom * denom);");
        src.push("}");

        src.push("vec3 BRDF_Specular_GGX(const in IncidentLight incidentLight, const in Geometry geometry, const in vec3 specularColor, const in float roughness) {");
        src.push("   float alpha = ( roughness * roughness );");
        src.push("   vec3 halfDir = normalize( incidentLight.direction + geometry.viewEyeDir );");
        src.push("   float dotNL = saturate( dot( geometry.viewNormal, incidentLight.direction ) );");
        src.push("   float dotNV = saturate( dot( geometry.viewNormal, geometry.viewEyeDir ) );");
        src.push("   float dotNH = saturate( dot( geometry.viewNormal, halfDir ) );");
        src.push("   float dotLH = saturate( dot( incidentLight.direction, halfDir ) );");
        src.push("   vec3  F = F_Schlick( specularColor, dotLH );");
        src.push("   float G = G_GGX_SmithCorrelated( alpha, dotNL, dotNV );");
        src.push("   float D = D_GGX( alpha, dotNH );");
        src.push("   return F * (G * D);");
        src.push("}");

        src.push("vec3 BRDF_Specular_GGX_Environment(const in Geometry geometry, const in vec3 specularColor, const in float roughness) {");
        src.push("   float dotNV = saturate(dot(geometry.viewNormal, geometry.viewEyeDir));");
        src.push("   const vec4 c0 = vec4( -1, -0.0275, -0.572,  0.022);");
        src.push("   const vec4 c1 = vec4(  1,  0.0425,   1.04, -0.04);");
        src.push("   vec4 r = roughness * c0 + c1;");
        src.push("   float a004 = min(r.x * r.x, exp2(-9.28 * dotNV)) * r.x + r.y;");
        src.push("   vec2 AB    = vec2(-1.04, 1.04) * a004 + r.zw;");
        src.push("   return specularColor * AB.x + AB.y;");
        src.push("}");

        if (lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0) {
            src.push("void computePBRLightMapping(const in Geometry geometry, const in Material material, inout ReflectedLight reflectedLight) {");
            if (lightsState.lightMaps.length > 0) {
                src.push("   vec3 irradiance = sRGBToLinear(texture(lightMap, geometry.worldNormal)).rgb;");
                src.push("   irradiance *= PI;");
                src.push("   vec3 diffuseBRDFContrib = (RECIPROCAL_PI * material.diffuseColor);");
                src.push("   reflectedLight.diffuse +=  irradiance * diffuseBRDFContrib;");
            }
            if (lightsState.reflectionMaps.length > 0) {
                src.push("   vec3 reflectVec             = reflect(geometry.viewEyeDir, geometry.viewNormal);");
                src.push("   reflectVec                  = inverseTransformDirection(reflectVec, viewMatrix);");
                src.push("   float blinnExpFromRoughness = GGXRoughnessToBlinnExponent(material.specularRoughness);");
                src.push("   vec3 radiance               = getLightProbeIndirectRadiance(reflectVec, blinnExpFromRoughness, 8);");
                src.push("   vec3 specularBRDFContrib    = BRDF_Specular_GGX_Environment(geometry, material.specularColor, material.specularRoughness);");
                src.push("   reflectedLight.specular     += radiance * specularBRDFContrib;");
            }
            src.push("}");
        }

        // MAIN LIGHTING COMPUTATION FUNCTION

        src.push("void computePBRLighting(const in IncidentLight incidentLight, const in Geometry geometry, const in Material material, inout ReflectedLight reflectedLight) {");
        src.push("   float dotNL     = saturate(dot(geometry.viewNormal, incidentLight.direction));");
        src.push("   vec3 irradiance = dotNL * incidentLight.color * PI;");
        src.push("   reflectedLight.diffuse  += irradiance * (RECIPROCAL_PI * material.diffuseColor);");
        src.push("   reflectedLight.specular += irradiance * BRDF_Specular_GGX(incidentLight, geometry, material.specularColor, material.specularRoughness);");
        src.push("}");

        src.push("out vec4 outColor;");

        src.push("void main(void) {");

        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            if (clippingCaps) {
                src.push("  if (dist > (0.002 * vClipPosition.w)) {");
                src.push("      discard;");
                src.push("  }");
                src.push("  if (dist > 0.0) { ");
                src.push("      outColor=vec4(1.0, 0.0, 0.0, 1.0);");
                if (scene.logarithmicDepthBufferEnabled) {
                    src.push("  gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
                }
                src.push("  return;");
                src.push("}");
            } else {
                src.push("  if (dist > 0.0) { ");
                src.push("      discard;");
                src.push("  }");
            }
            src.push("}");
        }

        src.push("IncidentLight  light;");
        src.push("Material       material;");
        src.push("Geometry       geometry;");
        src.push("ReflectedLight reflectedLight = ReflectedLight(vec3(0.0,0.0,0.0), vec3(0.0,0.0,0.0));");

        src.push("vec3 rgb = (vec3(float(vColor.r) / 255.0, float(vColor.g) / 255.0, float(vColor.b) / 255.0));");
        src.push("float opacity = float(vColor.a) / 255.0;");

        src.push("vec3  baseColor = rgb;");
        src.push("float specularF0 = 1.0;");
        src.push("float metallic = float(vMetallicRoughness.r) / 255.0;");
        src.push("float roughness = float(vMetallicRoughness.g) / 255.0;");
        src.push("float dielectricSpecular = 0.16 * specularF0 * specularF0;");

        src.push("vec4 colorTexel = sRGBToLinear(texture(uColorMap, vUV));");
        src.push("baseColor *= colorTexel.rgb;");
     //    src.push("opacity *= colorTexel.a;");

        src.push("vec3 metalRoughTexel = texture(uMetallicRoughMap, vUV).rgb;");
        src.push("metallic *= metalRoughTexel.b;");
        src.push("roughness *= metalRoughTexel.g;");

        src.push("vec3 viewNormal = perturbNormal2Arb(vViewPosition.xyz, normalize(vViewNormal), vUV );");

        src.push("material.diffuseColor      = baseColor * (1.0 - dielectricSpecular) * (1.0 - metallic);");
        src.push("material.specularRoughness = clamp(roughness, 0.04, 1.0);");
        src.push("material.specularColor     = mix(vec3(dielectricSpecular), baseColor, metallic);");

        src.push("geometry.position      = vViewPosition.xyz;");
        src.push("geometry.viewNormal    = -normalize(viewNormal);");
        src.push("geometry.viewEyeDir    = normalize(vViewPosition.xyz);");

        if (lightsState.lightMaps.length > 0) {
            src.push("geometry.worldNormal   = normalize(vWorldNormal);");
        }

        if (lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0) {
            src.push("computePBRLightMapping(geometry, material, reflectedLight);");
        }

        for (let i = 0, len = lightsState.lights.length; i < len; i++) {
            const light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            if (light.type === "dir") {
                if (light.space === "view") {
                    src.push("light.direction =  normalize(lightDir" + i + ");");
                } else {
                    src.push("light.direction =  normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "point") {
                if (light.space === "view") {
                    src.push("light.direction =  normalize(lightPos" + i + " - vViewPosition.xyz);");
                } else {
                    src.push("light.direction =  normalize((viewMatrix * vec4(lightPos" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "spot") {
                if (light.space === "view") {
                    src.push("light.direction =  normalize(lightDir" + i + ");");
                } else {
                    src.push("light.direction =  normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else {
                continue;
            }

            src.push("light.color =  lightColor" + i + ".rgb * lightColor" + i + ".a;"); // a is intensity

            src.push("computePBRLighting(light, geometry, material, reflectedLight);");
        }

        src.push("vec3 emissiveColor = sRGBToLinear(texture(uEmissiveMap, vUV)).rgb;"); // TODO: correct gamma function
        src.push("float aoFactor = texture(uAOMap, vUV).r;");

        src.push("vec3 outgoingLight = (lightAmbient.rgb * lightAmbient.a * baseColor * opacity * rgb) + (reflectedLight.diffuse) + (reflectedLight.specular) + emissiveColor;");
        src.push("vec4 fragColor;");

        if (this._withSAO) {
            // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top
            // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject
            src.push("   float viewportWidth     = uSAOParams[0];");
            src.push("   float viewportHeight    = uSAOParams[1];");
            src.push("   float blendCutoff       = uSAOParams[2];");
            src.push("   float blendFactor       = uSAOParams[3];");
            src.push("   vec2 uv                 = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);");
            src.push("   float ambient           = smoothstep(blendCutoff, 1.0, unpackRGBToFloat(texture(uOcclusionTexture, uv))) * blendFactor;");
            src.push("   fragColor               = vec4(outgoingLight.rgb * ambient * aoFactor, opacity);");
        } else {
            src.push("   fragColor            = vec4(outgoingLight.rgb * aoFactor, opacity);");
        }

        if (gammaOutput) {
            src.push("fragColor = linearToGamma(fragColor, gammaFactor);");
        }

        src.push("outColor = fragColor;");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }

        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class TrianglesPickNormalsFlatRenderer$1 extends TrianglesBatchingRenderer {

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles batching pick flat normals vertex shader");
        
        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        this._addRemapClipPosLines(src, 3);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }
        src.push("out vec4 vWorldPosition;");
        if (clipping) {
            src.push("out float vFlags;");
        }
        src.push("void main(void) {");
        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK
        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("      gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("  } else {");
        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("      vWorldPosition = worldPosition;");
        if (clipping) {
            src.push("      vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
           src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = remapClipPos(clipPos);");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles batching pick flat normals fragment shader");
        
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("in vec4 vWorldPosition;");
        if (clipping) {
            src.push("in float vFlags;");
            for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("out highp ivec4 outNormal;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("  vec3 xTangent = dFdx( vWorldPosition.xyz );");
        src.push("  vec3 yTangent = dFdy( vWorldPosition.xyz );");
        src.push("  vec3 worldNormal = normalize( cross( xTangent, yTangent ) );");
        src.push(`  outNormal = ivec4(worldNormal * float(${math.MAX_INT}), 1.0);`);
        src.push("}");
        return src;
    }
}

/**
 * @private
 */

class TrianglesColorTextureRenderer$1 extends TrianglesBatchingRenderer {
    _getHash() {
        const scene = this._scene;
        return [scene.gammaOutput, scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";");
    }

    drawLayer(frameCtx, layer, renderPass) {
        super.drawLayer(frameCtx, layer, renderPass, { incrementDrawState: true });
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles batching color texture vertex shader");

        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        src.push("in vec4 color;");
        src.push("in vec2 uv;");
        src.push("in float flags;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        this._addMatricesUniformBlockLines(src);

        src.push("uniform mat3 uvDecodeMatrix;");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec4 vViewPosition;");
        src.push("out vec4 vColor;");
        src.push("out vec2 vUV;");

        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");

        src.push("vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("worldPosition.xyz = worldPosition.xyz + offset;");
        }

        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("vViewPosition = viewPosition;");
        src.push("vColor = vec4(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0, float(color.a) / 255.0);");
        src.push("vUV = (uvDecodeMatrix * vec3(uv, 1.0)).xy;");

        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }

        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }

        src.push("gl_Position = clipPos;");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const gammaOutput = scene.gammaOutput; // If set, then it expects that all textures and colors need to be outputted in premultiplied gamma. Default is false.
        const lightsState = scene._lightsState;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const useAlphaCutoff = this._useAlphaCutoff;
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles batching color texture fragment shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("uniform sampler2D uColorMap;");
        if (this._withSAO) {
            src.push("uniform sampler2D uOcclusionTexture;");
            src.push("uniform vec4      uSAOParams;");

            src.push("const float       packUpscale = 256. / 255.;");
            src.push("const float       unpackDownScale = 255. / 256.;");
            src.push("const vec3        packFactors = vec3( 256. * 256. * 256., 256. * 256.,  256. );");
            src.push("const vec4        unPackFactors = unpackDownScale / vec4( packFactors, 1. );");

            src.push("float unpackRGBToFloat( const in vec4 v ) {");
            src.push("    return dot( v, unPackFactors );");
            src.push("}");
        }
        src.push("uniform float gammaFactor;");
        src.push("vec4 linearToLinear( in vec4 value ) {");
        src.push("  return value;");
        src.push("}");
        src.push("vec4 sRGBToLinear( in vec4 value ) {");
        src.push("  return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.w );");
        src.push("}");
        src.push("vec4 gammaToLinear( in vec4 value) {");
        src.push("  return vec4( pow( value.xyz, vec3( gammaFactor ) ), value.w );");
        src.push("}");
        if (gammaOutput) {
            src.push("vec4 linearToGamma( in vec4 value, in float gammaFactor ) {");
            src.push("  return vec4( pow( value.xyz, vec3( 1.0 / gammaFactor ) ), value.w );");
            src.push("}");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
            src.push("uniform float sliceThickness;");
            src.push("uniform vec4 sliceColor;");
        }
        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec4 lightAmbient;");
        for (let i = 0, len = lightsState.lights.length; i < len; i++) {
            const light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            src.push("uniform vec4 lightColor" + i + ";");
            if (light.type === "dir") {
                src.push("uniform vec3 lightDir" + i + ";");
            }
            if (light.type === "point") {
                src.push("uniform vec3 lightPos" + i + ";");
            }
            if (light.type === "spot") {
                src.push("uniform vec3 lightPos" + i + ";");
                src.push("uniform vec3 lightDir" + i + ";");
            }
        }

        if (useAlphaCutoff) {
            src.push("uniform float materialAlphaCutoff;");
        }

        src.push("in vec4 vViewPosition;");
        src.push("in vec4 vColor;");
        src.push("in vec2 vUV;");
        src.push("out vec4 outColor;");

        src.push("void main(void) {");
        src.push("  vec4 newColor;");
        src.push("  newColor = vColor;");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > sliceThickness) { ");
            src.push("      discard;");
            src.push("  }");
            src.push("  if (dist > 0.0) { ");
            src.push("      newColor = sliceColor;");
            src.push("  }");
            src.push("}");
        }

        src.push("vec3 reflectedColor = vec3(0.0, 0.0, 0.0);");
        src.push("vec3 viewLightDir = vec3(0.0, 0.0, -1.0);");

        src.push("float lambertian = 1.0;");

        src.push("vec3 xTangent = dFdx( vViewPosition.xyz );");
        src.push("vec3 yTangent = dFdy( vViewPosition.xyz );");
        src.push("vec3 viewNormal = normalize( cross( xTangent, yTangent ) );");

        for (let i = 0, len = lightsState.lights.length; i < len; i++) {
            const light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            if (light.type === "dir") {
                if (light.space === "view") {
                    src.push("viewLightDir = normalize(lightDir" + i + ");");
                } else {
                    src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "point") {
                if (light.space === "view") {
                    src.push("viewLightDir = -normalize(lightPos" + i + " - viewPosition.xyz);");
                } else {
                    src.push("viewLightDir = -normalize((viewMatrix * vec4(lightPos" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "spot") {
                if (light.space === "view") {
                    src.push("viewLightDir = normalize(lightDir" + i + ");");
                } else {
                    src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else {
                continue;
            }

            src.push("lambertian = max(dot(-viewNormal, viewLightDir), 0.0);");
            src.push("reflectedColor += lambertian * (lightColor" + i + ".rgb * lightColor" + i + ".a);");
        }

        src.push("vec4 color =  vec4((lightAmbient.rgb * lightAmbient.a * newColor.rgb) + (reflectedColor * newColor.rgb), newColor.a);");

        src.push("vec4 sampleColor = texture(uColorMap, vUV);");

        if (useAlphaCutoff) {
            src.push("if (sampleColor.a < materialAlphaCutoff) {");
            src.push("   discard;");
            src.push("}");
        }

        if (gammaOutput) {
            src.push("sampleColor = sRGBToLinear(sampleColor);");
        }

        src.push("vec4 colorTexel = color * sampleColor;");

        src.push("float opacity = color.a;");

        if (this._withSAO) {
            // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top
            // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject
            src.push("   float viewportWidth     = uSAOParams[0];");
            src.push("   float viewportHeight    = uSAOParams[1];");
            src.push("   float blendCutoff       = uSAOParams[2];");
            src.push("   float blendFactor       = uSAOParams[3];");
            src.push("   vec2 uv                 = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);");
            src.push("   float ambient           = smoothstep(blendCutoff, 1.0, unpackRGBToFloat(texture(uOcclusionTexture, uv))) * blendFactor;");

            src.push("   outColor                = vec4(colorTexel.rgb * ambient, opacity);");
        } else {
            src.push("   outColor                = vec4(colorTexel.rgb, opacity);");
        }

        if (gammaOutput) {
            src.push("outColor = linearToGamma(outColor, gammaFactor);");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }

        src.push("}");
        return src;
    }
}

const tempVec3a$E = math.vec3();
const tempVec3b$A = math.vec3();
const tempVec3c$v = math.vec3();
const tempVec3d$e = math.vec3();
const tempMat4a$q = math.mat4();

/**
 * @private
 */
class TrianglesSnapInitRenderer$1 extends VBORenderer {

    drawLayer(frameCtx, batchingLayer, renderPass) {

        if (!this._program) {
            this._allocate();
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram();
        }

        const model = batchingLayer.model;
        const scene = model.scene;
        const camera = scene.camera;
        const gl = scene.canvas.gl;
        const state = batchingLayer._state;
        const origin = batchingLayer._state.origin;
        const {position, rotationMatrix} = model;
        const aabb = batchingLayer.aabb; // Per-layer AABB for best RTC accuracy
        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;

        if (this._vaoCache.has(batchingLayer)) {
            gl.bindVertexArray(this._vaoCache.get(batchingLayer));
        } else {
            this._vaoCache.set(batchingLayer, this._makeVAO(state));
        }

        const coordinateScaler = tempVec3a$E;
        coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT;
        coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT;
        coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT;

        frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]);
        frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]);
        frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]);

        let rtcViewMatrix;
        let rtcCameraEye;

        if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) {
            const rtcOrigin = tempVec3b$A;
            if (origin) {
                const rotatedOrigin = tempVec3c$v;
                math.transformPoint3(rotationMatrix, origin, rotatedOrigin);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$q);
            rtcCameraEye = tempVec3d$e;
            rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0];
            rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1];
            rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2];
            frameCtx.snapPickOrigin[0] = rtcOrigin[0];
            frameCtx.snapPickOrigin[1] = rtcOrigin[1];
            frameCtx.snapPickOrigin[2] = rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            rtcCameraEye = camera.eye;
            frameCtx.snapPickOrigin[0] = 0;
            frameCtx.snapPickOrigin[1] = 0;
            frameCtx.snapPickOrigin[2] = 0;
        }

        gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye);
        gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA);
        gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB);
        gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber);
        gl.uniform3fv(this._uCoordinateScaler, coordinateScaler);
        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible);

        let offset = 0;
        const mat4Size = 4 * 4;

        this._matricesUniformBlockBufferData.set(rotationMatrix, 0);
        this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size);

        gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer);
        gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW);

        gl.bindBufferBase(
            gl.UNIFORM_BUFFER,
            this._matricesUniformBlockBufferBindingPoint,
            this._matricesUniformBlockBuffer);

        {
            const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix?
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }

        this.setSectionPlanesStateUniforms(batchingLayer);
        //=============================================================
        // TODO: Use drawElements count and offset to draw only one entity
        //=============================================================

        state.indicesBuf.bind();
        gl.drawElements(gl.TRIANGLES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0);
        state.indicesBuf.unbind();

        gl.bindVertexArray(null);
    }

    _allocate() {
        super._allocate();

        const program = this._program;

        {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
        this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc");
        this.uVectorA = program.getLocation("snapVectorA");
        this.uInverseVectorAB = program.getLocation("snapInvVectorAB");
        this._uLayerNumber = program.getLocation("layerNumber");
        this._uCoordinateScaler = program.getLocation("coordinateScaler");
    }

    _bindProgram() {
        this._program.bind();
    }

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// VBO SnapBatchingDepthBufInitRenderer vertex shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");
        src.push("uniform int renderPass;");
        src.push("in vec4 pickColor;");
        src.push("in vec3 position;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec3 uCameraEyeRtc;");
        src.push("uniform vec2 snapVectorA;");
        src.push("uniform vec2 snapInvVectorAB;");
        {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("out float isPerspective;");
        }
        src.push("bool isPerspectiveMatrix(mat4 m) {");
        src.push("    return (m[2][3] == - 1.0);");
        src.push("}");
        src.push("vec2 remapClipPos(vec2 clipPos) {");
        src.push("    float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;");
        src.push("    float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;");
        src.push("    return vec2(x, y);");
        src.push("}");
        src.push("flat out vec4 vPickColor;");
        src.push("out vec4 vWorldPosition;");
        if (clipping) {
            src.push("out float vFlags;");
        }
        src.push("out highp vec3 relativeToOriginPosition;");
        src.push("void main(void) {");
        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK
        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("      gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("  } else {");
        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("      relativeToOriginPosition = worldPosition.xyz;");
        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("      vWorldPosition = worldPosition;");
        if (clipping) {
            src.push("      vFlags = flags;");
        }
        src.push("vPickColor = pickColor;");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        src.push("float tmp = clipPos.w;");
        src.push("clipPos.xyzw /= tmp;");
        src.push("clipPos.xy = remapClipPos(clipPos.xy);");
        src.push("clipPos.xyzw *= tmp;");
        {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// VBO SnapBatchingDepthBufInitRenderer fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("uniform int layerNumber;");
        src.push("uniform vec3 coordinateScaler;");
        src.push("in vec4 vWorldPosition;");
        src.push("flat in vec4 vPickColor;");
        if (clipping) {
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in highp vec3 relativeToOriginPosition;");
        src.push("layout(location = 0) out highp ivec4 outCoords;");
        src.push("layout(location = 1) out highp ivec4 outNormal;");
        src.push("layout(location = 2) out lowp uvec4 outPickColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        {
            src.push("    float dx = dFdx(vFragDepth);");
            src.push("    float dy = dFdy(vFragDepth);");
            src.push("    float diff = sqrt(dx*dx+dy*dy);");
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth + diff ) * logDepthBufFC * 0.5;");
        }
        src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, -layerNumber);");

        src.push("vec3 xTangent = dFdx( vWorldPosition.xyz );");
        src.push("vec3 yTangent = dFdy( vWorldPosition.xyz );");
        src.push("vec3 worldNormal = normalize( cross( xTangent, yTangent ) );");
        src.push(`outNormal = ivec4(worldNormal * float(${math.MAX_INT}), 1.0);`);
        src.push("outPickColor = uvec4(vPickColor);");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

const tempVec3a$D = math.vec3();
const tempVec3b$z = math.vec3();
const tempVec3c$u = math.vec3();
const tempVec3d$d = math.vec3();
const tempMat4a$p = math.mat4();

/**
 * @private
 */
class TrianglesSnapRenderer$1 extends VBORenderer{
    _getHash() {
        return this._scene._sectionPlanesState.getHash() + (this._scene.pointsMaterial.hash);
    }

    drawLayer(frameCtx, batchingLayer, renderPass) {

        if (!this._program) {
            this._allocate();
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram();
        }

        const model = batchingLayer.model;
        const scene = model.scene;
        const camera = scene.camera;
        const gl = scene.canvas.gl;
        const state = batchingLayer._state;
        const origin = batchingLayer._state.origin;
        const {position, rotationMatrix} = model;
        const aabb = batchingLayer.aabb; // Per-layer AABB for best RTC accuracy
        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;

        if (this._vaoCache.has(batchingLayer)) {
            gl.bindVertexArray(this._vaoCache.get(batchingLayer));
        } else {
            this._vaoCache.set(batchingLayer, this._makeVAO(state));
        }

        const coordinateScaler = tempVec3a$D;
        coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT;
        coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT;
        coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT;

        frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]);
        frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]);
        frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]);

        let rtcViewMatrix;
        let rtcCameraEye;

        if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) {
            const rtcOrigin = tempVec3b$z;
            if (origin) {
                const rotatedOrigin = tempVec3c$u;
                math.transformPoint3(rotationMatrix, origin, rotatedOrigin);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$p);
            rtcCameraEye = tempVec3d$d;
            rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0];
            rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1];
            rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2];
            frameCtx.snapPickOrigin[0] = rtcOrigin[0];
            frameCtx.snapPickOrigin[1] = rtcOrigin[1];
            frameCtx.snapPickOrigin[2] = rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            rtcCameraEye = camera.eye;
            frameCtx.snapPickOrigin[0] = 0;
            frameCtx.snapPickOrigin[1] = 0;
            frameCtx.snapPickOrigin[2] = 0;
        }

        gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye);
        gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA);
        gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB);
        gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber);
        gl.uniform3fv(this._uCoordinateScaler, coordinateScaler);
        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible);

        let offset = 0;
        const mat4Size = 4 * 4;

        this._matricesUniformBlockBufferData.set(rotationMatrix, 0);
        this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size);

        gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer);
        gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW);

        gl.bindBufferBase(
            gl.UNIFORM_BUFFER,
            this._matricesUniformBlockBufferBindingPoint,
            this._matricesUniformBlockBuffer);

        {
            const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix?
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }

        this.setSectionPlanesStateUniforms(batchingLayer);

        //=============================================================
        // TODO: Use drawElements count and offset to draw only one entity
        //=============================================================

        if (frameCtx.snapMode === "edge") {
            if (state.edgeIndicesBuf) {
                state.edgeIndicesBuf.bind();
                gl.drawElements(gl.LINES, state.edgeIndicesBuf.numItems, state.edgeIndicesBuf.itemType, 0);
                state.edgeIndicesBuf.unbind(); // needed?
            }
        } else {
            gl.drawArrays(gl.POINTS, 0, state.positionsBuf.numItems);
        }

        gl.bindVertexArray(null);
    }

    _allocate() {
        super._allocate();

        const program = this._program;

        {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }

        this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); 
        this.uVectorA = program.getLocation("snapVectorA"); 
        this.uInverseVectorAB = program.getLocation("snapInvVectorAB"); 
        this._uLayerNumber = program.getLocation("layerNumber"); 
        this._uCoordinateScaler = program.getLocation("coordinateScaler"); 
    }

    _bindProgram() {
        this._program.bind();
    }

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        scene.pointsMaterial._state;
        const src = [];
        src.push ('#version 300 es');
        src.push("// SnapBatchingDepthRenderer vertex shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");
        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec3 uCameraEyeRtc;"); 
        src.push("uniform vec2 snapVectorA;"); 
        src.push("uniform vec2 snapInvVectorAB;"); 
        {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }
        src.push("vec2 remapClipPos(vec2 clipPos) {");
        src.push("    float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;");
        src.push("    float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;");
        src.push("    return vec2(x, y);");
        src.push("}");
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out highp vec3 relativeToOriginPosition;");
        src.push("void main(void) {");

        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK

        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("      gl_Position = vec4(2.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("  } else {");
        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("relativeToOriginPosition = worldPosition.xyz;");
        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("      vWorldPosition = worldPosition;");
            src.push("      vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        src.push("float tmp = clipPos.w;");
        src.push("clipPos.xyzw /= tmp;");
        src.push("clipPos.xy = remapClipPos(clipPos.xy);");
        src.push("clipPos.xyzw *= tmp;");
        {
           src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("gl_PointSize = 1.0;"); // Windows needs this?
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// SnapBatchingDepthRenderer fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("uniform int layerNumber;"); 
        src.push("uniform vec3 coordinateScaler;"); 
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in highp vec3 relativeToOriginPosition;");
        src.push("out highp ivec4 outCoords;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, layerNumber);");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

/**
 * @private
 */
class Renderers$1 {

    constructor(scene) {
        this._scene = scene;
    }

    _compile() {
        if (this._colorRenderer && (!this._colorRenderer.getValid())) {
            this._colorRenderer.destroy();
            this._colorRenderer = null;
        }
        if (this._colorRendererWithSAO && (!this._colorRendererWithSAO.getValid())) {
            this._colorRendererWithSAO.destroy();
            this._colorRendererWithSAO = null;
        }
        if (this._flatColorRenderer && (!this._flatColorRenderer.getValid())) {
            this._flatColorRenderer.destroy();
            this._flatColorRenderer = null;
        }
        if (this._flatColorRendererWithSAO && (!this._flatColorRendererWithSAO.getValid())) {
            this._flatColorRendererWithSAO.destroy();
            this._flatColorRendererWithSAO = null;
        }
        if (this._colorTextureRenderer && (!this._colorTextureRenderer.getValid())) {
            this._colorTextureRenderer.destroy();
            this._colorTextureRenderer = null;
        }
        if (this._colorTextureRendererWithSAO && (!this._colorTextureRendererWithSAO.getValid())) {
            this._colorTextureRendererWithSAO.destroy();
            this._colorTextureRendererWithSAO = null;
        }
        if (this._colorTextureRendererAlphaCutoff && (!this._colorTextureRendererAlphaCutoff.getValid())) {
            this._colorTextureRendererAlphaCutoff.destroy();
            this._colorTextureRendererAlphaCutoff = null;
        }
        if (this._colorTextureRendererWithSAOAlphaCutoff && (!this._colorTextureRendererWithSAOAlphaCutoff.getValid())) {
            this._colorTextureRendererWithSAOAlphaCutoff.destroy();
            this._colorTextureRendererWithSAOAlphaCutoff = null;
        }
        if (this._pbrRenderer && (!this._pbrRenderer.getValid())) {
            this._pbrRenderer.destroy();
            this._pbrRenderer = null;
        }
        if (this._pbrRendererWithSAO && (!this._pbrRendererWithSAO.getValid())) {
            this._pbrRendererWithSAO.destroy();
            this._pbrRendererWithSAO = null;
        }
        if (this._depthRenderer && (!this._depthRenderer.getValid())) {
            this._depthRenderer.destroy();
            this._depthRenderer = null;
        }
        if (this._normalsRenderer && (!this._normalsRenderer.getValid())) {
            this._normalsRenderer.destroy();
            this._normalsRenderer = null;
        }
        if (this._silhouetteRenderer && (!this._silhouetteRenderer.getValid())) {
            this._silhouetteRenderer.destroy();
            this._silhouetteRenderer = null;
        }
        if (this._edgesRenderer && (!this._edgesRenderer.getValid())) {
            this._edgesRenderer.destroy();
            this._edgesRenderer = null;
        }
        if (this._edgesColorRenderer && (!this._edgesColorRenderer.getValid())) {
            this._edgesColorRenderer.destroy();
            this._edgesColorRenderer = null;
        }
        if (this._pickMeshRenderer && (!this._pickMeshRenderer.getValid())) {
            this._pickMeshRenderer.destroy();
            this._pickMeshRenderer = null;
        }
        if (this._pickDepthRenderer && (!this._pickDepthRenderer.getValid())) {
            this._pickDepthRenderer.destroy();
            this._pickDepthRenderer = null;
        }
        if (this._pickNormalsRenderer && this._pickNormalsRenderer.getValid() === false) {
            this._pickNormalsRenderer.destroy();
            this._pickNormalsRenderer = null;
        }
        if (this._pickNormalsFlatRenderer && this._pickNormalsFlatRenderer.getValid() === false) {
            this._pickNormalsFlatRenderer.destroy();
            this._pickNormalsFlatRenderer = null;
        }
        if (this._occlusionRenderer && this._occlusionRenderer.getValid() === false) {
            this._occlusionRenderer.destroy();
            this._occlusionRenderer = null;
        }
        if (this._shadowRenderer && (!this._shadowRenderer.getValid())) {
            this._shadowRenderer.destroy();
            this._shadowRenderer = null;
        }
        if (this._snapInitRenderer && (!this._snapInitRenderer.getValid())) {
            this._snapInitRenderer.destroy();
            this._snapInitRenderer = null;
        }
        if (this._snapRenderer && (!this._snapRenderer.getValid())) {
            this._snapRenderer.destroy();
            this._snapRenderer = null;
        }
    }

    eagerCreateRenders() {

        // Pre-initialize certain renderers that would otherwise be lazy-initialised
        // on user interaction, such as picking or emphasis, so that there is no delay
        // when user first begins interacting with the viewer.

        if (!this._silhouetteRenderer) { // Used for highlighting and selection
            this._silhouetteRenderer = new TrianglesSilhouetteRenderer$1(this._scene);
        }
        if (!this._pickMeshRenderer) {
            this._pickMeshRenderer = new TrianglesPickMeshRenderer$1(this._scene);
        }
        if (!this._pickDepthRenderer) {
            this._pickDepthRenderer = new TrianglesPickDepthRenderer$1(this._scene);
        }
        if (!this._snapInitRenderer) {
            this._snapInitRenderer = new TrianglesSnapInitRenderer$1(this._scene, false);
        }
        if (!this._snapRenderer) {
            this._snapRenderer = new TrianglesSnapRenderer$1(this._scene);
        }
    }

    get colorRenderer() {
        if (!this._colorRenderer) {
            this._colorRenderer = new TrianglesColorRenderer$1(this._scene, false);
        }
        return this._colorRenderer;
    }

    get colorRendererWithSAO() {
        if (!this._colorRendererWithSAO) {
            this._colorRendererWithSAO = new TrianglesColorRenderer$1(this._scene, true);
        }
        return this._colorRendererWithSAO;
    }

    get flatColorRenderer() {
        if (!this._flatColorRenderer) {
            this._flatColorRenderer = new TrianglesFlatColorRenderer$1(this._scene, false);
        }
        return this._flatColorRenderer;
    }

    get flatColorRendererWithSAO() {
        if (!this._flatColorRendererWithSAO) {
            this._flatColorRendererWithSAO = new TrianglesFlatColorRenderer$1(this._scene, true);
        }
        return this._flatColorRendererWithSAO;
    }

    get colorTextureRenderer() {
        if (!this._colorTextureRenderer) {
            this._colorTextureRenderer = new TrianglesColorTextureRenderer$1(this._scene, false);
        }
        return this._colorTextureRenderer;
    }

    get colorTextureRendererWithSAO() {
        if (!this._colorTextureRendererWithSAO) {
            this._colorTextureRendererWithSAO = new TrianglesColorTextureRenderer$1(this._scene, true);
        }
        return this._colorTextureRendererWithSAO;
    }

    get colorTextureRendererAlphaCutoff() {
        if (!this._colorTextureRendererAlphaCutoff) {
            this._colorTextureRendererAlphaCutoff = new TrianglesColorTextureRenderer$1(this._scene, false, { useAlphaCutoff: true });
        }
        return this._colorTextureRendererAlphaCutoff;
    }

    get colorTextureRendererWithSAOAlphaCutoff() {
        if (!this._colorTextureRendererWithSAOAlphaCutoff) {
            this._colorTextureRendererWithSAOAlphaCutoff = new TrianglesColorTextureRenderer$1(this._scene, true, { useAlphaCutoff: true });
        }
        return this._colorTextureRendererWithSAOAlphaCutoff;
    }

    get pbrRenderer() {
        if (!this._pbrRenderer) {
            this._pbrRenderer = new TrianglesPBRRenderer$1(this._scene, false);
        }
        return this._pbrRenderer;
    }

    get pbrRendererWithSAO() {
        if (!this._pbrRendererWithSAO) {
            this._pbrRendererWithSAO = new TrianglesPBRRenderer$1(this._scene, true);
        }
        return this._pbrRendererWithSAO;
    }

    get silhouetteRenderer() {
        if (!this._silhouetteRenderer) {
            this._silhouetteRenderer = new TrianglesSilhouetteRenderer$1(this._scene);
        }
        return this._silhouetteRenderer;
    }

    get depthRenderer() {
        if (!this._depthRenderer) {
            this._depthRenderer = new TrianglesDepthRenderer$1(this._scene);
        }
        return this._depthRenderer;
    }

    get normalsRenderer() {
        if (!this._normalsRenderer) {
            this._normalsRenderer = new TrianglesNormalsRenderer$1(this._scene);
        }
        return this._normalsRenderer;
    }

    get edgesRenderer() {
        if (!this._edgesRenderer) {
            this._edgesRenderer = new EdgesEmphasisRenderer$1(this._scene);
        }
        return this._edgesRenderer;
    }

    get edgesColorRenderer() {
        if (!this._edgesColorRenderer) {
            this._edgesColorRenderer = new EdgesColorRenderer$1(this._scene);
        }
        return this._edgesColorRenderer;
    }

    get pickMeshRenderer() {
        if (!this._pickMeshRenderer) {
            this._pickMeshRenderer = new TrianglesPickMeshRenderer$1(this._scene);
        }
        return this._pickMeshRenderer;
    }

    get pickNormalsRenderer() {
        if (!this._pickNormalsRenderer) {
            this._pickNormalsRenderer = new TrianglesPickNormalsRenderer$1(this._scene);
        }
        return this._pickNormalsRenderer;
    }

    get pickNormalsFlatRenderer() {
        if (!this._pickNormalsFlatRenderer) {
            this._pickNormalsFlatRenderer = new TrianglesPickNormalsFlatRenderer$1(this._scene);
        }
        return this._pickNormalsFlatRenderer;
    }

    get pickDepthRenderer() {
        if (!this._pickDepthRenderer) {
            this._pickDepthRenderer = new TrianglesPickDepthRenderer$1(this._scene);
        }
        return this._pickDepthRenderer;
    }

    get occlusionRenderer() {
        if (!this._occlusionRenderer) {
            this._occlusionRenderer = new TrianglesOcclusionRenderer$1(this._scene);
        }
        return this._occlusionRenderer;
    }

    get shadowRenderer() {
        if (!this._shadowRenderer) {
            this._shadowRenderer = new TrianglesShadowRenderer$1(this._scene);
        }
        return this._shadowRenderer;
    }

    get snapRenderer() {
        if (!this._snapRenderer) {
            this._snapRenderer = new TrianglesSnapRenderer$1(this._scene);
        }
        return this._snapRenderer;
    }

    get snapInitRenderer() {
        if (!this._snapInitRenderer) {
            this._snapInitRenderer = new TrianglesSnapInitRenderer$1(this._scene);
        }
        return this._snapInitRenderer;
    }

    _destroy() {
        if (this._colorRenderer) {
            this._colorRenderer.destroy();
        }
        if (this._colorRendererWithSAO) {
            this._colorRendererWithSAO.destroy();
        }
        if (this._flatColorRenderer) {
            this._flatColorRenderer.destroy();
        }
        if (this._flatColorRendererWithSAO) {
            this._flatColorRendererWithSAO.destroy();
        }
        if (this._colorTextureRenderer) {
            this._colorTextureRenderer.destroy();
        }
        if (this._colorTextureRendererWithSAO) {
            this._colorTextureRendererWithSAO.destroy();
        }
        if (this._colorTextureRendererAlphaCutoff) {
            this._colorTextureRendererAlphaCutoff.destroy();
        }
        if (this._colorTextureRendererWithSAOAlphaCutoff) {
            this._colorTextureRendererWithSAOAlphaCutoff.destroy();
        }
        if (this._pbrRenderer) {
            this._pbrRenderer.destroy();
        }
        if (this._pbrRendererWithSAO) {
            this._pbrRendererWithSAO.destroy();
        }
        if (this._depthRenderer) {
            this._depthRenderer.destroy();
        }
        if (this._normalsRenderer) {
            this._normalsRenderer.destroy();
        }
        if (this._silhouetteRenderer) {
            this._silhouetteRenderer.destroy();
        }
        if (this._edgesRenderer) {
            this._edgesRenderer.destroy();
        }
        if (this._edgesColorRenderer) {
            this._edgesColorRenderer.destroy();
        }
        if (this._pickMeshRenderer) {
            this._pickMeshRenderer.destroy();
        }
        if (this._pickDepthRenderer) {
            this._pickDepthRenderer.destroy();
        }
        if (this._pickNormalsRenderer) {
            this._pickNormalsRenderer.destroy();
        }
        if (this._pickNormalsFlatRenderer) {
            this._pickNormalsFlatRenderer.destroy();
        }
        if (this._occlusionRenderer) {
            this._occlusionRenderer.destroy();
        }
        if (this._shadowRenderer) {
            this._shadowRenderer.destroy();
        }
        if (this._snapInitRenderer) {
            this._snapInitRenderer.destroy();
        }
        if (this._snapRenderer) {
            this._snapRenderer.destroy();
        }
    }
}

const cachedRenderers$6 = {};

/**
 * @private
 */
function getRenderers$7(scene) {
    const sceneId = scene.id;
    let renderers = cachedRenderers$6[sceneId];
    if (!renderers) {
        renderers = new Renderers$1(scene);
        cachedRenderers$6[sceneId] = renderers;
        renderers._compile();
        renderers.eagerCreateRenders();
        scene.on("compile", () => {
            renderers._compile();
            renderers.eagerCreateRenders();
        });
        scene.on("destroyed", () => {
            delete cachedRenderers$6[sceneId];
            renderers._destroy();
        });
    }
    return renderers;
}

let maxDataTextureHeight = 1 << 16;
let maxGeometryBatchSize = 5000000;

/**
 * Manages global configurations for all {@link Viewer}s.
 *
 * ## Example
 *
 * In the example below, we'll disable xeokit's double-precision support, which gives a performance and memory boost
 * on low-power devices, but also means that we can no longer render double-precision models without jittering.
 *
 * That's OK if we know that we're not going to view models that are geographically vast, or offset far from the World coordinate origin.
 *
 * [[Run this example](/examples/index.html#Configs_disableDoublePrecisionAndRAF)]
 *
 * ````javascript
 * import {Configs, Viewer, XKTLoaderPlugin} from "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/xeokit-sdk.es.min.js";
 *
 * // Access xeoit-sdk global configs.
 * // We typically set configs only before we create any Viewers.
 * const configs = new Configs();
 *
 * // Disable 64-bit precision for extra speed.
 * // Only set this config once, before you create any Viewers.
 * configs.doublePrecisionEnabled = false;
 *
 * // Create a Viewer, to which our configs apply
 * const viewer = new Viewer({
 *      canvasId: "myCanvas"
 *  });
 *
 * viewer.camera.eye = [-3.933, 2.855, 27.018];
 * viewer.camera.look = [4.400, 3.724, 8.899];
 * viewer.camera.up = [-0.018, 0.999, 0.039];
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const model = xktLoader.load({
 *     src: "../assets/models/xkt/v8/ifc/Duplex.ifc.xkt"
 * });
 * ````
 */
class Configs {

    /**
     * Creates a Configs.
     */
    constructor() {

    }

    /**
     * Sets whether double precision mode is enabled for Viewers.
     *
     * When double precision mode is enabled (default), Viewers will accurately render models that contain
     * double-precision coordinates, without jittering.
     *
     * Internally, double precision incurs extra performance and memory overhead, so if we're certain that
     * we're not going to render models that rely on double-precision coordinates, then it's a good idea to disable
     * it, especially on low-power devices.
     *
     * This should only be set once, before creating any Viewers.
     *
     * @returns {Boolean}
     */
    set doublePrecisionEnabled(doublePrecision) {
        math.setDoublePrecisionEnabled(doublePrecision);
    }

    /**
     * Gets whether double precision mode is enabled for all Viewers.
     *
     * @returns {Boolean}
     */
    get doublePrecisionEnabled() {
        return math.getDoublePrecisionEnabled();
    }

    /**
     * Sets the maximum data texture height.
     *
     * Should be a multiple of 1024. Default is 4096, which is the maximum allowed value.
     */
    set maxDataTextureHeight(value) {
        value = Math.ceil(value / 1024) * 1024;
        if (value > 4096) {
            value = 4096;
        } else if (value < 1024) {
            value = 1024;
        }
        maxDataTextureHeight = value;
    }

    /**
     * Sets maximum data texture height.
     * @returns {*|number}
     */
    get maxDataTextureHeight() {
        return maxDataTextureHeight;
    }

    /**
     * Sets the maximum batched geometry VBO size.
     *
     * Default value is 5000000, which is the maximum size.
     *
     * Minimum size is 100000.
     */
    set maxGeometryBatchSize(value) {
        if (value < 100000) {
            value = 100000;
        } else if (value > 5000000) {
            value = 5000000;
        }
        maxGeometryBatchSize = value;
    }

    /**
     * Gets the maximum batched geometry VBO size.
     */
    get maxGeometryBatchSize() {
        return maxGeometryBatchSize;
    }
}

const configs$2 = new Configs();

/**
 * @private
 */
class VBOBatchingTrianglesBuffer {

    constructor(maxBatchSize = configs$2.maxGeometryBatchSize) {
        this.maxVerts = maxBatchSize;
        this.maxIndices = maxBatchSize * 3; // Rough rule-of-thumb
        this.positions = [];
        this.colors = [];
        this.uv = [];
        this.metallicRoughness = [];
        this.normals = [];
        this.pickColors = [];
        this.offsets = [];
        this.indices = [];
        this.edgeIndices = [];
    }
}

const translate$1 = math.mat4();
const scale$1 = math.mat4();

/**
 * @private
 */
function quantizePositions(positions, aabb, positionsDecodeMatrix) { // http://cg.postech.ac.kr/research/mesh_comp_mobile/mesh_comp_mobile_conference.pdf
    const lenPositions = positions.length;
    const quantizedPositions = new Uint16Array(lenPositions);
    const xmin = aabb[0];
    const ymin = aabb[1];
    const zmin = aabb[2];
    const xwid = aabb[3] - xmin;
    const ywid = aabb[4] - ymin;
    const zwid = aabb[5] - zmin;
    const maxInt = 65525;
    const xMultiplier = maxInt / xwid;
    const yMultiplier = maxInt / ywid;
    const zMultiplier = maxInt / zwid;
    const verify = (num) => num >= 0 ? num : 0;
    for (let i = 0; i < lenPositions; i += 3) {
        quantizedPositions[i + 0] = Math.floor(verify(positions[i + 0] - xmin) * xMultiplier);
        quantizedPositions[i + 1] = Math.floor(verify(positions[i + 1] - ymin) * yMultiplier);
        quantizedPositions[i + 2] = Math.floor(verify(positions[i + 2] - zmin) * zMultiplier);
    }
    math.identityMat4(translate$1);
    math.translationMat4v(aabb, translate$1);
    math.identityMat4(scale$1);
    math.scalingMat4v([xwid / maxInt, ywid / maxInt, zwid / maxInt], scale$1);
    math.mulMat4(translate$1, scale$1, positionsDecodeMatrix);
    return quantizedPositions;
}

/**
 * @private
 * @param aabb
 * @param positionsDecodeMatrix
 * @returns {*}
 */
function createPositionsDecodeMatrix(aabb, positionsDecodeMatrix) { // http://cg.postech.ac.kr/research/mesh_comp_mobile/mesh_comp_mobile_conference.pdf
    const xmin = aabb[0];
    const ymin = aabb[1];
    const zmin = aabb[2];
    const xwid = aabb[3] - xmin;
    const ywid = aabb[4] - ymin;
    const zwid = aabb[5] - zmin;
    const maxInt = 65525;
    math.identityMat4(translate$1);
    math.translationMat4v(aabb, translate$1);
    math.identityMat4(scale$1);
    math.scalingMat4v([xwid / maxInt, ywid / maxInt, zwid / maxInt], scale$1);
    math.mulMat4(translate$1, scale$1, positionsDecodeMatrix);
    return positionsDecodeMatrix;
}

/**
 * @private
 */
function transformAndOctEncodeNormals(worldNormalMatrix, normals, lenNormals, compressedNormals, lenCompressedNormals) {
    // http://jcgt.org/published/0003/02/01/

    function dot(array, vec3) {
        return array[0] * vec3[0] + array[1] * vec3[1] + array[2] * vec3[2];
    }

    let oct, dec, best, currentCos, bestCos;
    let i;
    let localNormal = new Float32Array([0, 0, 0, 0]);
    let worldNormal = new Float32Array([0, 0, 0, 0]);
    for (i = 0; i < lenNormals; i += 3) {
        localNormal[0] = normals[i];
        localNormal[1] = normals[i + 1];
        localNormal[2] = normals[i + 2];

        math.transformVec3(worldNormalMatrix, localNormal, worldNormal);
        math.normalizeVec3(worldNormal, worldNormal);

        // Test various combinations of ceil and floor to minimize rounding errors
        best = oct = octEncodeVec3(worldNormal, "floor", "floor");
        dec = octDecodeVec2(oct);
        currentCos = bestCos = dot(worldNormal, dec);
        oct = octEncodeVec3(worldNormal, "ceil", "floor");
        dec = octDecodeVec2(oct);
        currentCos = dot(worldNormal, dec);
        if (currentCos > bestCos) {
            best = oct;
            bestCos = currentCos;
        }
        oct = octEncodeVec3(worldNormal, "floor", "ceil");
        dec = octDecodeVec2(oct);
        currentCos = dot(worldNormal, dec);
        if (currentCos > bestCos) {
            best = oct;
            bestCos = currentCos;
        }
        oct = octEncodeVec3(worldNormal, "ceil", "ceil");
        dec = octDecodeVec2(oct);
        currentCos = dot(worldNormal, dec);
        if (currentCos > bestCos) {
            best = oct;
            bestCos = currentCos;
        }
        compressedNormals[lenCompressedNormals + i + 0] = best[0];
        compressedNormals[lenCompressedNormals + i + 1] = best[1];
        compressedNormals[lenCompressedNormals + i + 2] = 0.0; // Unused
    }
    lenCompressedNormals += lenNormals;
    return lenCompressedNormals;
}

/**
 * @private
 */
function octEncodeVec3(p, xfunc, yfunc) { // Oct-encode single normal vector in 2 bytes
    let x = p[0] / (Math.abs(p[0]) + Math.abs(p[1]) + Math.abs(p[2]));
    let y = p[1] / (Math.abs(p[0]) + Math.abs(p[1]) + Math.abs(p[2]));
    if (p[2] < 0) {
        let tempx = x;
        let tempy = y;
        tempx = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1);
        tempy = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1);
        x = tempx;
        y = tempy;
    }
    return new Int8Array([
        Math[xfunc](x * 127.5 + (x < 0 ? -1 : 0)),
        Math[yfunc](y * 127.5 + (y < 0 ? -1 : 0))
    ]);
}

/**
 * @private
 */
function octDecodeVec2(oct) { // Decode an oct-encoded normal
    let x = oct[0];
    let y = oct[1];
    x /= x < 0 ? 127 : 128;
    y /= y < 0 ? 127 : 128;
    const z = 1 - Math.abs(x) - Math.abs(y);
    if (z < 0) {
        x = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1);
        y = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1);
    }
    const length = Math.sqrt(x * x + y * y + z * z);
    return [
        x / length,
        y / length,
        z / length
    ];
}

const tempMat4$1 = math.mat4();
const tempMat4b = math.mat4();
const tempVec4a$d = math.vec4([0, 0, 0, 1]);

const tempVec3a$C = math.vec3();
const tempVec3b$y = math.vec3();
const tempVec3c$t = math.vec3();
const tempVec3d$c = math.vec3();
const tempVec3e$2 = math.vec3();
const tempVec3f$2 = math.vec3();
const tempVec3g$1 = math.vec3();

/**
 * @private
 */
class VBOBatchingTrianglesLayer {

    /**
     * @param model
     * @param cfg.model
     * @param cfg.autoNormals
     * @param cfg.layerIndex
     * @param cfg.positionsDecodeMatrix
     * @param cfg.uvDecodeMatrix
     * @param cfg.maxGeometryBatchSize
     * @param cfg.origin
     * @param cfg.scratchMemory
     * @param cfg.textureSet
     * @param cfg.solid
     */
    constructor(cfg) {

     //   console.info("Creating VBOBatchingTrianglesLayer");

        /**
         * Owner model
         * @type {VBOSceneModel}
         */
        this.model = cfg.model;

        /**
         * State sorting key.
         * @type {string}
         */
        this.sortId = "TrianglesBatchingLayer"
            + (cfg.solid ? "-solid" : "-surface")
            + (cfg.autoNormals ? "-autonormals" : "-normals")

            // TODO: These two parts need to be IDs (ie. unique):

            + (cfg.textureSet && cfg.textureSet.colorTexture ? "-colorTexture" : "")
            + (cfg.textureSet && cfg.textureSet.metallicRoughnessTexture ? "-metallicRoughnessTexture" : "");

        /**
         * Index of this TrianglesBatchingLayer in {@link VBOSceneModel#_layerList}.
         * @type {Number}
         */
        this.layerIndex = cfg.layerIndex;

        this._renderers = getRenderers$7(cfg.model.scene);
        this._buffer = new VBOBatchingTrianglesBuffer(cfg.maxGeometryBatchSize);
        this._scratchMemory = cfg.scratchMemory;

        this._state = new RenderState({
            origin: math.vec3(),
            positionsBuf: null,
            offsetsBuf: null,
            normalsBuf: null,
            colorsBuf: null,
            uvBuf: null,
            metallicRoughnessBuf: null,
            flagsBuf: null,
            indicesBuf: null,
            edgeIndicesBuf: null,
            positionsDecodeMatrix: null,
            uvDecodeMatrix: null,
            textureSet: cfg.textureSet,
            pbrSupported: false // Set in #finalize if we have enough to support quality rendering
        });

        // These counts are used to avoid unnecessary render passes
        this._numPortions = 0;
        this._numVisibleLayerPortions = 0;
        this._numTransparentLayerPortions = 0;
        this._numXRayedLayerPortions = 0;
        this._numSelectedLayerPortions = 0;
        this._numHighlightedLayerPortions = 0;
        this._numClippableLayerPortions = 0;
        this._numEdgesLayerPortions = 0;
        this._numPickableLayerPortions = 0;
        this._numCulledLayerPortions = 0;

        this._modelAABB = math.collapseAABB3(); // Model-space AABB
        this._portions = [];
        this._meshes = [];
        this._numVerts = 0;

        this._aabb = math.collapseAABB3();
        this.aabbDirty = true;

        this._finalized = false;

        if (cfg.positionsDecodeMatrix) {
            this._state.positionsDecodeMatrix = math.mat4(cfg.positionsDecodeMatrix);
        }

        if (cfg.uvDecodeMatrix) {
            this._state.uvDecodeMatrix = math.mat3(cfg.uvDecodeMatrix);
            this._preCompressedUVsExpected = true;
        } else {
            this._preCompressedUVsExpected = false;
        }

        if (cfg.origin) {
            this._state.origin.set(cfg.origin);
        }

        /**
         * When true, this layer contains solid triangle meshes, otherwise this layer contains surface triangle meshes
         * @type {boolean}
         */
        this.solid = !!cfg.solid;

        /**
         * The type of primitives in this layer.
         */
        this.primitive = cfg.primitive;
    }

    get aabb() {
        if (this.aabbDirty) {
            math.collapseAABB3(this._aabb);
            for (let i = 0, len = this._meshes.length; i < len; i++) {
                math.expandAABB3(this._aabb, this._meshes[i].aabb);
            }
            this.aabbDirty = false;
        }
        return this._aabb;
    }

    /**
     * Tests if there is room for another portion in this TrianglesBatchingLayer.
     *
     * @param lenPositions Number of positions we'd like to create in the portion.
     * @param lenIndices Number of indices we'd like to create in this portion.
     * @returns {Boolean} True if OK to create another portion.
     */
    canCreatePortion(lenPositions, lenIndices) {
        if (this._finalized) {
            throw "Already finalized";
        }
        return ((this._buffer.positions.length + lenPositions) < (this._buffer.maxVerts * 3) && (this._buffer.indices.length + lenIndices) < (this._buffer.maxIndices));
    }

    /**
     * Creates a new portion within this TrianglesBatchingLayer, returns the new portion ID.
     *
     * Gives the portion the specified geometry, color and matrix.
     *
     * @param mesh The SceneModelMesh that owns the portion
     * @param cfg.positions Flat float Local-space positions array.
     * @param cfg.positionsCompressed Flat quantized positions array - decompressed with TrianglesBatchingLayer positionsDecodeMatrix
     * @param [cfg.normals] Flat float normals array.
     * @param [cfg.uv] Flat UVs array.
     * @param [cfg.uvCompressed]
     * @param [cfg.colors] Flat float colors array.
     * @param [cfg.colorsCompressed]
     * @param cfg.indices  Flat int indices array.
     * @param [cfg.edgeIndices] Flat int edges indices array.
     * @param cfg.color Quantized RGB color [0..255,0..255,0..255,0..255]
     * @param cfg.metallic Metalness factor [0..255]
     * @param cfg.roughness Roughness factor [0..255]
     * @param cfg.opacity Opacity [0..255]
     * @param [cfg.meshMatrix] Flat float 4x4 matrix
     * @param cfg.aabb Flat float AABB World-space AABB
     * @param cfg.pickColor Quantized pick color
     * @returns {number} Portion ID
     */
    createPortion(mesh, cfg) {

        if (this._finalized) {
            throw "Already finalized";
        }

        const positions = cfg.positions;
        const positionsCompressed = cfg.positionsCompressed;
        const normals = cfg.normals;
        const normalsCompressed = cfg.normalsCompressed;
        const uv = cfg.uv;
        const uvCompressed = cfg.uvCompressed;
        const colors = cfg.colors;
        const colorsCompressed = cfg.colorsCompressed;
        const indices = cfg.indices;
        const edgeIndices = cfg.edgeIndices;
        const color = cfg.color;
        const metallic = cfg.metallic;
        const roughness = cfg.roughness;
        const opacity = cfg.opacity;
        const meshMatrix = cfg.meshMatrix;
        const pickColor = cfg.pickColor;

        const scene = this.model.scene;
        const buffer = this._buffer;
        const vertsBaseIndex = buffer.positions.length / 3;

        let numVerts;

        math.expandAABB3(this._modelAABB, cfg.aabb);

        if (this._state.positionsDecodeMatrix) {
            if (!positionsCompressed) {
                throw "positionsCompressed expected";
            }
            numVerts = positionsCompressed.length / 3;
            for (let i = 0, len = positionsCompressed.length; i < len; i++) {
                buffer.positions.push(positionsCompressed[i]);
            }
        } else {
            if (!positions) {
                throw "positions expected";
            }
            numVerts = positions.length / 3;
            for (let i = 0, len = positions.length; i < len; i++) {
                buffer.positions.push(positions[i]);
            }
        }

        if (normalsCompressed && normalsCompressed.length > 0) {
            for (let i = 0, len = normalsCompressed.length; i < len; i++) {
                buffer.normals.push(normalsCompressed[i]);
            }
        } else if (normals && normals.length > 0) {
            const worldNormalMatrix = tempMat4$1;
            if (meshMatrix) {
                math.inverseMat4(math.transposeMat4(meshMatrix, tempMat4b), worldNormalMatrix); // Note: order of inverse and transpose doesn't matter
            } else {
                math.identityMat4(worldNormalMatrix, worldNormalMatrix);
            }
            transformAndOctEncodeNormals(worldNormalMatrix, normals, normals.length, buffer.normals, buffer.normals.length);
        }

        if (colors) {
            for (let i = 0, len = colors.length; i < len; i += 3) {
                buffer.colors.push(colors[i] * 255);
                buffer.colors.push(colors[i + 1] * 255);
                buffer.colors.push(colors[i + 2] * 255);
                buffer.colors.push(255);
            }
        } else if (colorsCompressed) {
            for (let i = 0, len = colors.length; i < len; i += 3) {
                buffer.colors.push(colors[i]);
                buffer.colors.push(colors[i + 1]);
                buffer.colors.push(colors[i + 2]);
                buffer.colors.push(255);
            }
        } else if (color) {
            const r = color[0]; // Color is pre-quantized by VBOSceneModel
            const g = color[1];
            const b = color[2];
            const a = opacity;
            for (let i = 0; i < numVerts; i++) {
                buffer.colors.push(r);
                buffer.colors.push(g);
                buffer.colors.push(b);
                buffer.colors.push(a);
            }
        }
        const metallicValue = (metallic !== null && metallic !== undefined) ? metallic : 0;
        const roughnessValue = (roughness !== null && roughness !== undefined) ? roughness : 255;
        for (let i = 0; i < numVerts; i++) {
            buffer.metallicRoughness.push(metallicValue);
            buffer.metallicRoughness.push(roughnessValue);
        }

        if (uv && uv.length > 0) {
            for (let i = 0, len = uv.length; i < len; i++) {
                buffer.uv.push(uv[i]);
            }
        } else if (uvCompressed && uvCompressed.length > 0) {
            for (let i = 0, len = uvCompressed.length; i < len; i++) {
                buffer.uv.push(uvCompressed[i]);
            }
        }

        for (let i = 0, len = indices.length; i < len; i++) {
            buffer.indices.push(vertsBaseIndex + indices[i]);
        }


        if (edgeIndices) {
            for (let i = 0, len = edgeIndices.length; i < len; i++) {
                buffer.edgeIndices.push(vertsBaseIndex + edgeIndices[i]);
            }
        }

        {
            const pickColorsBase = buffer.pickColors.length;
            const lenPickColors = numVerts * 4;
            for (let i = pickColorsBase, len = pickColorsBase + lenPickColors; i < len; i += 4) {
                buffer.pickColors.push(pickColor[0]);
                buffer.pickColors.push(pickColor[1]);
                buffer.pickColors.push(pickColor[2]);
                buffer.pickColors.push(pickColor[3]);
            }
        }

        if (scene.entityOffsetsEnabled) {
            for (let i = 0; i < numVerts; i++) {
                buffer.offsets.push(0);
                buffer.offsets.push(0);
                buffer.offsets.push(0);
            }
        }

        const portionId = this._portions.length;

        const portion = {
            vertsBaseIndex: vertsBaseIndex,
            numVerts: numVerts,
            indicesBaseIndex: buffer.indices.length - indices.length,
            numIndices: indices.length,
        };

        if (scene.readableGeometryEnabled) {
            // Quantized in-memory positions are initialized in finalize()

            portion.indices = indices;

            if (scene.entityOffsetsEnabled) {
                portion.offset = new Float32Array(3);
            }
        }

        this._portions.push(portion);
        this._numPortions++;
        this.model.numPortions++;
        this._numVerts += portion.numVerts;
        this._meshes.push(mesh);
        return portionId;
    }

    /**
     * Builds batch VBOs from appended geometries.
     * No more portions can then be created.
     */
    finalize() {

        if (this._finalized) {
            return;
        }

        const state = this._state;
        const gl = this.model.scene.canvas.gl;
        const buffer = this._buffer;

        if (buffer.positions.length > 0) {
            const quantizedPositions = (this._state.positionsDecodeMatrix)
                ? new Uint16Array(buffer.positions)
                : quantizePositions(buffer.positions, this._modelAABB, this._state.positionsDecodeMatrix = math.mat4()); // BOTTLENECK
            state.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, quantizedPositions, quantizedPositions.length, 3, gl.STATIC_DRAW);
            if (this.model.scene.readableGeometryEnabled) {
                for (let i = 0, numPortions = this._portions.length; i < numPortions; i++) {
                    const portion = this._portions[i];
                    const start = portion.vertsBaseIndex * 3;
                    const end = start + (portion.numVerts * 3);
                    portion.quantizedPositions = quantizedPositions.slice(start, end);
                }
            }
        }

        if (buffer.normals.length > 0) { // Normals are already oct-encoded
            const normals = new Int8Array(buffer.normals);
            let normalized = true; // For oct encoded UInts
            state.normalsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, normals, buffer.normals.length, 3, gl.STATIC_DRAW, normalized);
        }

        if (buffer.colors.length > 0) { // Colors are already compressed
            const colors = new Uint8Array(buffer.colors);
            let normalized = false;
            state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, colors, buffer.colors.length, 4, gl.DYNAMIC_DRAW, normalized);
        }

        if (buffer.uv.length > 0) {
            if (!state.uvDecodeMatrix) {
                const bounds = geometryCompressionUtils.getUVBounds(buffer.uv);
                const result = geometryCompressionUtils.compressUVs(buffer.uv, bounds.min, bounds.max);
                const uv = result.quantized;
                let notNormalized = false;
                state.uvDecodeMatrix = math.mat3(result.decodeMatrix);
                state.uvBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, uv, uv.length, 2, gl.STATIC_DRAW, notNormalized);
            } else {
                let notNormalized = false;
                state.uvBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, buffer.uv, buffer.uv.length, 2, gl.STATIC_DRAW, notNormalized);
            }
        }

        if (buffer.metallicRoughness.length > 0) {
            const metallicRoughness = new Uint8Array(buffer.metallicRoughness);
            let normalized = false;
            state.metallicRoughnessBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, metallicRoughness, buffer.metallicRoughness.length, 2, gl.STATIC_DRAW, normalized);
        }

        if (buffer.positions.length > 0) { // Because we build flags arrays here, get their length from the positions array
            const flagsLength = (buffer.positions.length / 3);
            const flags = new Float32Array(flagsLength);
            const notNormalized = false;
            state.flagsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, flags, flags.length, 1, gl.DYNAMIC_DRAW, notNormalized);
        }

        if (buffer.pickColors.length > 0) {
            const pickColors = new Uint8Array(buffer.pickColors);
            let normalized = false;
            state.pickColorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, pickColors, buffer.pickColors.length, 4, gl.STATIC_DRAW, normalized);
        }

        if (this.model.scene.entityOffsetsEnabled) {
            if (buffer.offsets.length > 0) {
                const offsets = new Float32Array(buffer.offsets);
                state.offsetsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, offsets, buffer.offsets.length, 3, gl.DYNAMIC_DRAW);
            }
        }

        if (buffer.indices.length > 0) {
            const indices = new Uint32Array(buffer.indices);
            state.indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, indices, buffer.indices.length, 1, gl.STATIC_DRAW);
        }
        if (buffer.edgeIndices.length > 0) {
            const edgeIndices = new Uint32Array(buffer.edgeIndices);
            state.edgeIndicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, edgeIndices, buffer.edgeIndices.length, 1, gl.STATIC_DRAW);
        }

        this._state.pbrSupported
            = !!state.metallicRoughnessBuf
            && !!state.uvBuf
            && !!state.normalsBuf
            && !!state.textureSet
            && !!state.textureSet.colorTexture
            && !!state.textureSet.metallicRoughnessTexture;

        this._state.colorTextureSupported
            = !!state.uvBuf
            && !!state.textureSet
            && !!state.textureSet.colorTexture;

        this._buffer = null;
        this._finalized = true;
    }

    isEmpty() {
        return (!this._state.indicesBuf);
    }

    initFlags(portionId, flags, meshTransparent) {
        if (flags & ENTITY_FLAGS.VISIBLE) {
            this._numVisibleLayerPortions++;
            this.model.numVisibleLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.HIGHLIGHTED) {
            this._numHighlightedLayerPortions++;
            this.model.numHighlightedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.XRAYED) {
            this._numXRayedLayerPortions++;
            this.model.numXRayedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.SELECTED) {
            this._numSelectedLayerPortions++;
            this.model.numSelectedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.CLIPPABLE) {
            this._numClippableLayerPortions++;
            this.model.numClippableLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.EDGES) {
            this._numEdgesLayerPortions++;
            this.model.numEdgesLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.PICKABLE) {
            this._numPickableLayerPortions++;
            this.model.numPickableLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.CULLED) {
            this._numCulledLayerPortions++;
            this.model.numCulledLayerPortions++;
        }
        if (meshTransparent) {
            this._numTransparentLayerPortions++;
            this.model.numTransparentLayerPortions++;
        }
        const deferred = true;
        this._setFlags(portionId, flags, meshTransparent, deferred);
    }

    flushInitFlags() {
        this._setDeferredFlags();
    }

    setVisible(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.VISIBLE) {
            this._numVisibleLayerPortions++;
            this.model.numVisibleLayerPortions++;
        } else {
            this._numVisibleLayerPortions--;
            this.model.numVisibleLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setHighlighted(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.HIGHLIGHTED) {
            this._numHighlightedLayerPortions++;
            this.model.numHighlightedLayerPortions++;
        } else {
            this._numHighlightedLayerPortions--;
            this.model.numHighlightedLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setXRayed(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.XRAYED) {
            this._numXRayedLayerPortions++;
            this.model.numXRayedLayerPortions++;
        } else {
            this._numXRayedLayerPortions--;
            this.model.numXRayedLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setSelected(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.SELECTED) {
            this._numSelectedLayerPortions++;
            this.model.numSelectedLayerPortions++;
        } else {
            this._numSelectedLayerPortions--;
            this.model.numSelectedLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setEdges(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.EDGES) {
            this._numEdgesLayerPortions++;
            this.model.numEdgesLayerPortions++;
        } else {
            this._numEdgesLayerPortions--;
            this.model.numEdgesLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setClippable(portionId, flags) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.CLIPPABLE) {
            this._numClippableLayerPortions++;
            this.model.numClippableLayerPortions++;
        } else {
            this._numClippableLayerPortions--;
            this.model.numClippableLayerPortions--;
        }
        this._setFlags(portionId, flags);
    }

    setCulled(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.CULLED) {
            this._numCulledLayerPortions++;
            this.model.numCulledLayerPortions++;
        } else {
            this._numCulledLayerPortions--;
            this.model.numCulledLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setCollidable(portionId, flags) {
        if (!this._finalized) {
            throw "Not finalized";
        }
    }

    setPickable(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.PICKABLE) {
            this._numPickableLayerPortions++;
            this.model.numPickableLayerPortions++;
        } else {
            this._numPickableLayerPortions--;
            this.model.numPickableLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setColor(portionId, color) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        const portionsIdx = portionId;
        const portion = this._portions[portionsIdx];
        const vertsBaseIndex = portion.vertsBaseIndex;
        const numVerts = portion.numVerts;
        const firstColor = vertsBaseIndex * 4;
        const lenColor = numVerts * 4;
        const tempArray = this._scratchMemory.getUInt8Array(lenColor);
        const r = color[0];
        const g = color[1];
        const b = color[2];
        const a = color[3];
        for (let i = 0; i < lenColor; i += 4) {
            tempArray[i + 0] = r;
            tempArray[i + 1] = g;
            tempArray[i + 2] = b;
            tempArray[i + 3] = a;
        }
        if (this._state.colorsBuf) {
            this._state.colorsBuf.setData(tempArray, firstColor, lenColor);
        }
    }

    setTransparent(portionId, flags, transparent) {
        if (transparent) {
            this._numTransparentLayerPortions++;
            this.model.numTransparentLayerPortions++;
        } else {
            this._numTransparentLayerPortions--;
            this.model.numTransparentLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    /**
     * flags are 4bits values encoded on a 32bit base. color flag on the first 4 bits, silhouette flag on the next 4 bits and so on for edge, pick and clippable.
     */
    _setFlags(portionId, flags, transparent, deferred = false) {

        if (!this._finalized) {
            throw "Not finalized";
        }

        const portionsIdx = portionId;
        const portion = this._portions[portionsIdx];
        const vertsBaseIndex = portion.vertsBaseIndex;
        const numVerts = portion.numVerts;
        const firstFlag = vertsBaseIndex;
        const lenFlags = numVerts;

        const visible = !!(flags & ENTITY_FLAGS.VISIBLE);
        const xrayed = !!(flags & ENTITY_FLAGS.XRAYED);
        const highlighted = !!(flags & ENTITY_FLAGS.HIGHLIGHTED);
        const selected = !!(flags & ENTITY_FLAGS.SELECTED);
        const edges = !!(flags & ENTITY_FLAGS.EDGES);
        const pickable = !!(flags & ENTITY_FLAGS.PICKABLE);
        const culled = !!(flags & ENTITY_FLAGS.CULLED);

        let colorFlag;
        if (!visible || culled || xrayed
            || (highlighted && !this.model.scene.highlightMaterial.glowThrough)
            || (selected && !this.model.scene.selectedMaterial.glowThrough)) {
            colorFlag = RENDER_PASSES.NOT_RENDERED;
        } else {
            if (transparent) {
                colorFlag = RENDER_PASSES.COLOR_TRANSPARENT;
            } else {
                colorFlag = RENDER_PASSES.COLOR_OPAQUE;
            }
        }

        let silhouetteFlag;
        if (!visible || culled) {
            silhouetteFlag = RENDER_PASSES.NOT_RENDERED;
        } else if (selected) {
            silhouetteFlag = RENDER_PASSES.SILHOUETTE_SELECTED;
        } else if (highlighted) {
            silhouetteFlag = RENDER_PASSES.SILHOUETTE_HIGHLIGHTED;
        } else if (xrayed) {
            silhouetteFlag = RENDER_PASSES.SILHOUETTE_XRAYED;
        } else {
            silhouetteFlag = RENDER_PASSES.NOT_RENDERED;
        }

        let edgeFlag = 0;
        if (!visible || culled) {
            edgeFlag = RENDER_PASSES.NOT_RENDERED;
        } else if (selected) {
            edgeFlag = RENDER_PASSES.EDGES_SELECTED;
        } else if (highlighted) {
            edgeFlag = RENDER_PASSES.EDGES_HIGHLIGHTED;
        } else if (xrayed) {
            edgeFlag = RENDER_PASSES.EDGES_XRAYED;
        } else if (edges) {
            if (transparent) {
                edgeFlag = RENDER_PASSES.EDGES_COLOR_TRANSPARENT;
            } else {
                edgeFlag = RENDER_PASSES.EDGES_COLOR_OPAQUE;
            }
        } else {
            edgeFlag = RENDER_PASSES.NOT_RENDERED;
        }

        let pickFlag = (visible && !culled && pickable) ? RENDER_PASSES.PICK : RENDER_PASSES.NOT_RENDERED;

        const clippableFlag = !!(flags & ENTITY_FLAGS.CLIPPABLE) ? 1 : 0;

        if (deferred) {
            // Avoid zillions of individual WebGL bufferSubData calls - buffer them to apply in one shot
            if (!this._deferredFlagValues) {
                this._deferredFlagValues = new Float32Array(this._numVerts);
            }
            for (let i = firstFlag, len = (firstFlag + lenFlags); i < len; i++) {
                let vertFlag = 0;
                vertFlag |= colorFlag;
                vertFlag |= silhouetteFlag << 4;
                vertFlag |= edgeFlag << 8;
                vertFlag |= pickFlag << 12;
                vertFlag |= clippableFlag << 16;

                this._deferredFlagValues[i] = vertFlag;
            }
        } else if (this._state.flagsBuf) {
            const tempArray = this._scratchMemory.getFloat32Array(lenFlags);
            for (let i = 0; i < lenFlags; i++) {
                let vertFlag = 0;
                vertFlag |= colorFlag;
                vertFlag |= silhouetteFlag << 4;
                vertFlag |= edgeFlag << 8;
                vertFlag |= pickFlag << 12;
                vertFlag |= clippableFlag << 16;

                tempArray[i] = vertFlag;
            }
            this._state.flagsBuf.setData(tempArray, firstFlag, lenFlags);
        }
    }

    _setDeferredFlags() {
        if (this._deferredFlagValues) {
            this._state.flagsBuf.setData(this._deferredFlagValues);
            this._deferredFlagValues = null;
        }
    }

    setOffset(portionId, offset) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (!this.model.scene.entityOffsetsEnabled) {
            this.model.error("Entity#offset not enabled for this Viewer"); // See Viewer entityOffsetsEnabled
            return;
        }
        const portionsIdx = portionId;
        const portion = this._portions[portionsIdx];
        const vertsBaseIndex = portion.vertsBaseIndex;
        const numVerts = portion.numVerts;
        const firstOffset = vertsBaseIndex * 3;
        const lenOffsets = numVerts * 3;
        const tempArray = this._scratchMemory.getFloat32Array(lenOffsets);
        const x = offset[0];
        const y = offset[1];
        const z = offset[2];
        for (let i = 0; i < lenOffsets; i += 3) {
            tempArray[i + 0] = x;
            tempArray[i + 1] = y;
            tempArray[i + 2] = z;
        }
        if (this._state.offsetsBuf) {
            this._state.offsetsBuf.setData(tempArray, firstOffset, lenOffsets);
        }
        if (this.model.scene.readableGeometryEnabled) {
            portion.offset[0] = offset[0];
            portion.offset[1] = offset[1];
            portion.offset[2] = offset[2];
        }
    }

    getEachVertex(portionId, callback) {
        if (!this.model.scene.readableGeometryEnabled) {
            return;
        }
        const state = this._state;
        const portion = this._portions[portionId];
        if (!portion) {
            this.model.error("portion not found: " + portionId);
            return;
        }
        const positions = portion.quantizedPositions;
        const sceneModelMatrix = this.model.matrix;
        const origin = math.vec4();
        origin.set(state.origin, 0);
        origin[3] = 1;
        math.mulMat4v4(sceneModelMatrix, origin, origin);
        const offsetX = origin[0];
        const offsetY = origin[1];
        const offsetZ = origin[2];
        const worldPos = tempVec4a$d;
        const positionsDecodeMatrix = state.positionsDecodeMatrix;
        for (let i = 0, len = positions.length; i < len; i += 3) {
            worldPos[0] = positions[i];
            worldPos[1] = positions[i + 1];
            worldPos[2] = positions[i + 2];
            worldPos[3] = 1.0;
            math.decompressPosition(worldPos, positionsDecodeMatrix);
            worldPos[3] = 1;
            math.mulMat4v4(sceneModelMatrix, worldPos, worldPos);
            worldPos[0] += offsetX;
            worldPos[1] += offsetY;
            worldPos[2] += offsetZ;
            callback(worldPos);
        }
    }

    getEachIndex(portionId, callback) {
        if (!this.model.scene.readableGeometryEnabled) {
            return;
        }
        const portion = this._portions[portionId];
        if (!portion) {
            this.model.error("portion not found: " + portionId);
            return;
        }
        const indices = portion.indices;
        for (let i = 0, len = indices.length; i < len; i++) {
            callback(indices[i]);
        }
    }

    getElementsCountAndOffset(portionId) {
        let count = null;
        let offset = null;
        const portion = this._portions[portionId];

        if (portion) {
            count = portion.numIndices;
            offset = portion.indicesBaseIndex;
        }

        return {count, offset}
    }

    readGeometryData(portionId) {
        if (!this._finalized) {
            throw "Not finalized";
        }

        const portion = this._portions[portionId];
        const state = this._state;
        const sceneModelMatrix = this.model.matrix;
        const positionsDecodeMatrix = state.positionsDecodeMatrix;

        const origin = math.vec4();
        origin.set(state.origin, 0); origin[3] = 1;
        math.mulMat4v4(sceneModelMatrix, origin, origin);

        const indices = state.indicesBuf.getData(
            portion.indicesBaseIndex, portion.numIndices
        ).map(i => i - portion.vertsBaseIndex);

        const matrix = math.mulMat4(
            sceneModelMatrix,
            positionsDecodeMatrix,
            new Array(16)
        );

        matrix[12] += origin[0];
        matrix[13] += origin[1];
        matrix[14] += origin[2];

        const positionsQuantized = state.positionsBuf.getData(
            portion.vertsBaseIndex, portion.numVerts
        );

        const positions = math.transformPositions3(
            matrix,
            positionsQuantized,
            new Array(positionsQuantized.length)
        );

        // const aabb = math.positions3ToAABB3(positions);
        // console.log({aabbToniBatching: aabb});

        return { indices, positions };
    }

    // ---------------------- COLOR RENDERING -----------------------------------

    drawColorOpaque(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        const useAlphaCutoff = this._state.textureSet && (typeof(this._state.textureSet.alphaCutoff) === "number");
        if (frameCtx.withSAO && this.model.saoEnabled) {
            if (frameCtx.pbrEnabled && this.model.pbrEnabled && this._state.pbrSupported) {
                if (this._renderers.pbrRendererWithSAO) {
                    this._renderers.pbrRendererWithSAO.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
                }
            } else if (frameCtx.colorTextureEnabled && this.model.colorTextureEnabled && this._state.colorTextureSupported) {
                if (useAlphaCutoff) {
                    if (this._renderers.colorTextureRendererWithSAOAlphaCutoff) {
                        this._renderers.colorTextureRendererWithSAOAlphaCutoff.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
                    }
                } else {
                    if (this._renderers.colorTextureRendererWithSAO) {
                        this._renderers.colorTextureRendererWithSAO.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
                    }
                }
            } else if (this._state.normalsBuf) {
                if (this._renderers.colorRendererWithSAO) {
                    this._renderers.colorRendererWithSAO.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
                }
            } else {
                if (this._renderers.flatColorRendererWithSAO) {
                    this._renderers.flatColorRendererWithSAO.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
                }
            }
        } else {
            if (frameCtx.pbrEnabled && this.model.pbrEnabled && this._state.pbrSupported) {
                if (this._renderers.pbrRenderer) {
                    this._renderers.pbrRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
                }
            } else if (frameCtx.colorTextureEnabled && this.model.colorTextureEnabled && this._state.colorTextureSupported) {
                if (useAlphaCutoff) {
                    if (this._renderers.colorTextureRendererAlphaCutoff) {
                        this._renderers.colorTextureRendererAlphaCutoff.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
                    }
                } else {
                    if (this._renderers.colorTextureRenderer) {
                        this._renderers.colorTextureRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
                    }
                }
            } else if (this._state.normalsBuf) {
                if (this._renderers.colorRenderer) {
                    this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
                }
            } else {
                if (this._renderers.flatColorRenderer) {
                    this._renderers.flatColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
                }
            }
        }
    }

    _updateBackfaceCull(renderFlags, frameCtx) {
        const backfaces = true; // See XCD-230
        if (frameCtx.backfaces !== backfaces) {
            const gl = frameCtx.gl;
            {
                gl.disable(gl.CULL_FACE);
            }
            frameCtx.backfaces = backfaces;
        }
    }

    drawColorTransparent(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === 0 || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (frameCtx.pbrEnabled && this.model.pbrEnabled && this._state.pbrSupported) {
            if (this._renderers.pbrRenderer) {
                this._renderers.pbrRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT);
            }
        } else if (frameCtx.colorTextureEnabled && this.model.colorTextureEnabled && this._state.colorTextureSupported) {
            const useAlphaCutoff = this._state.textureSet && (typeof(this._state.textureSet.alphaCutoff) === "number");
            if (useAlphaCutoff) {
                if (this._renderers.colorTextureRendererAlphaCutoff) {
                    this._renderers.colorTextureRendererAlphaCutoff.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT);
                }
            } else {
                if (this._renderers.colorTextureRenderer) {
                    this._renderers.colorTextureRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT);
                }
            }
        } else if (this._state.normalsBuf) {
            if (this._renderers.colorRenderer) {
                this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT);
            }
        } else {
            if (this._renderers.flatColorRenderer) {
                this._renderers.flatColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT);
            }
        }
    }

    // ---------------------- RENDERING SAO POST EFFECT TARGETS --------------

    drawDepth(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.depthRenderer) {
            this._renderers.depthRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); // Assume whatever post-effect uses depth (eg SAO) does not apply to transparent objects
        }
    }

    drawNormals(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.normalsRenderer) {
            this._renderers.normalsRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);  // Assume whatever post-effect uses normals (eg SAO) does not apply to transparent objects
        }
    }

    // ---------------------- SILHOUETTE RENDERING -----------------------------------

    drawSilhouetteXRayed(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_XRAYED);
        }
    }

    drawSilhouetteHighlighted(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_HIGHLIGHTED);
        }
    }

    drawSilhouetteSelected(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_SELECTED);
        }
    }

    // ---------------------- EDGES RENDERING -----------------------------------

    drawEdgesColorOpaque(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numEdgesLayerPortions === 0) {
            return;
        }
        if (this._renderers.edgesColorRenderer) {
            this._renderers.edgesColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_COLOR_OPAQUE);
        }
    }

    drawEdgesColorTransparent(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numEdgesLayerPortions === 0 || this._numTransparentLayerPortions === 0) {
            return;
        }
        if (this._renderers.edgesColorRenderer) {
            this._renderers.edgesColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_COLOR_TRANSPARENT);
        }
    }

    drawEdgesHighlighted(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) {
            return;
        }
        if (this._renderers.edgesRenderer) {
            this._renderers.edgesRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_HIGHLIGHTED);
        }
    }

    drawEdgesSelected(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) {
            return;
        }
        if (this._renderers.edgesRenderer) {
            this._renderers.edgesRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_SELECTED);
        }
    }

    drawEdgesXRayed(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) {
            return;
        }
        if (this._renderers.edgesRenderer) {
            this._renderers.edgesRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_XRAYED);
        }
    }

    // ---------------------- OCCLUSION CULL RENDERING -----------------------------------

    drawOcclusion(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.occlusionRenderer) {
            this._renderers.occlusionRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
        }
    }

    // ---------------------- SHADOW BUFFER RENDERING -----------------------------------

    drawShadow(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.shadowRenderer) {
            this._renderers.shadowRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
        }
    }

    //---- PICKING ----------------------------------------------------------------------------------------------------

    drawPickMesh(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.pickMeshRenderer) {
            this._renderers.pickMeshRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawPickDepths(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.pickDepthRenderer) {
            this._renderers.pickDepthRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawPickNormals(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);

        ////////////////////////////////////////////////////////////////////////////////////////////////////
        // TODO
        // if (this._state.normalsBuf) {
        //     if (this._renderers.pickNormalsRenderer) {
        //         this._renderers.pickNormalsRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        //     }
        ////////////////////////////////////////////////////////////////////////////////////////////////////
        // } else {
        if (this._renderers.pickNormalsFlatRenderer) {
            this._renderers.pickNormalsFlatRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
        // }
    }

    drawSnapInit(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.snapInitRenderer) {
            this._renderers.snapInitRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawSnap(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.snapRenderer) {
            this._renderers.snapRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    //------------------------------------------------------------------------------------------------

    precisionRayPickSurface(portionId, worldRayOrigin, worldRayDir, worldSurfacePos, worldNormal) {

        if (!this.model.scene.readableGeometryEnabled) {
            return false;
        }

        const state = this._state;
        const portion = this._portions[portionId];

        if (!portion) {
            this.model.error("portion not found: " + portionId);
            return false;
        }

        const positions = portion.quantizedPositions;
        const indices = portion.indices;
        const origin = state.origin;
        const offset = portion.offset;

        const rtcRayOrigin = tempVec3a$C;
        const rtcRayDir = tempVec3b$y;

        rtcRayOrigin.set(origin ? math.subVec3(worldRayOrigin, origin, tempVec3c$t) : worldRayOrigin);  // World -> RTC
        rtcRayDir.set(worldRayDir);

        if (offset) {
            math.subVec3(rtcRayOrigin, offset);
        }

        math.transformRay(this.model.worldNormalMatrix, rtcRayOrigin, rtcRayDir, rtcRayOrigin, rtcRayDir); // RTC -> local

        const a = tempVec3d$c;
        const b = tempVec3e$2;
        const c = tempVec3f$2;

        let gotIntersect = false;
        let closestDist = 0;
        const closestIntersectPos = tempVec3g$1;

        for (let i = 0, len = indices.length; i < len; i += 3) {

            const ia = indices[i] * 3;
            const ib = indices[i + 1] * 3;
            const ic = indices[i + 2] * 3;

            a[0] = positions[ia];
            a[1] = positions[ia + 1];
            a[2] = positions[ia + 2];

            b[0] = positions[ib];
            b[1] = positions[ib + 1];
            b[2] = positions[ib + 2];

            c[0] = positions[ic];
            c[1] = positions[ic + 1];
            c[2] = positions[ic + 2];

            math.decompressPosition(a, state.positionsDecodeMatrix);
            math.decompressPosition(b, state.positionsDecodeMatrix);
            math.decompressPosition(c, state.positionsDecodeMatrix);

            if (math.rayTriangleIntersect(rtcRayOrigin, rtcRayDir, a, b, c, closestIntersectPos)) {

                math.transformPoint3(this.model.worldMatrix, closestIntersectPos, closestIntersectPos);

                if (offset) {
                    math.addVec3(closestIntersectPos, offset);
                }

                if (origin) {
                    math.addVec3(closestIntersectPos, origin);
                }

                const dist = Math.abs(math.lenVec3(math.subVec3(closestIntersectPos, worldRayOrigin, [])));

                if (!gotIntersect || dist > closestDist) {
                    closestDist = dist;
                    worldSurfacePos.set(closestIntersectPos);
                    if (worldNormal) { // Not that wasteful to eagerly compute - unlikely to hit >2 surfaces on most geometry
                        math.triangleNormal(a, b, c, worldNormal);
                    }
                    gotIntersect = true;
                }
            }
        }

        if (gotIntersect && worldNormal) {
            math.transformVec3(this.model.worldNormalMatrix, worldNormal, worldNormal);
            math.normalizeVec3(worldNormal);
        }

        return gotIntersect;
    }

    // ---------

    destroy() {
        const state = this._state;
        if (state.positionsBuf) {
            state.positionsBuf.destroy();
            state.positionsBuf = null;
        }
        if (state.offsetsBuf) {
            state.offsetsBuf.destroy();
            state.offsetsBuf = null;
        }
        if (state.normalsBuf) {
            state.normalsBuf.destroy();
            state.normalsBuf = null;
        }
        if (state.colorsBuf) {
            state.colorsBuf.destroy();
            state.colorsBuf = null;
        }
        if (state.metallicRoughnessBuf) {
            state.metallicRoughnessBuf.destroy();
            state.metallicRoughnessBuf = null;
        }
        if (state.flagsBuf) {
            state.flagsBuf.destroy();
            state.flagsBuf = null;
        }
        if (state.pickColorsBuf) {
            state.pickColorsBuf.destroy();
            state.pickColorsBuf = null;
        }
        if (state.indicesBuf) {
            state.indicesBuf.destroy();
            state.indicessBuf = null;
        }
        if (state.edgeIndicesBuf) {
            state.edgeIndicesBuf.destroy();
            state.edgeIndicessBuf = null;
        }
        state.destroy();
    }
}

/**
 * @private
 */
class TrianglesInstancingRenderer extends VBORenderer {
    constructor(scene, withSAO, {edges = false, useAlphaCutoff = false} = {}) {
        super(scene, withSAO, {instancing: true, edges, useAlphaCutoff});
    }

    _draw(drawCfg) {
        const {gl} = this._scene.canvas;

        const {
            state,
            frameCtx,
            incrementDrawState
        } = drawCfg;

        if (this._edges) {
            if (state.edgeIndicesBuf) {
                gl.drawElementsInstanced(gl.LINES, state.edgeIndicesBuf.numItems, state.edgeIndicesBuf.itemType, 0, state.numInstances);
            }
        } else {
            gl.drawElementsInstanced(gl.TRIANGLES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0, state.numInstances);

            if (incrementDrawState) {
                frameCtx.drawElements++;
            }
        }
    }
}

/**
 * @private
 */
class TrianglesColorRenderer extends TrianglesInstancingRenderer {
    _getHash() {
        const scene = this._scene;
        return [scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";");
    }

    drawLayer(frameCtx, layer, renderPass) {
        super.drawLayer(frameCtx, layer, renderPass, { incrementDrawState: true });
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const lightsState = scene._lightsState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        let i;
        let len;
        let light;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing geometry drawing vertex shader");

        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        src.push("in vec2 normal;");
        src.push("in vec4 color;");
        src.push("in float flags;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        src.push("in vec4 modelNormalMatrixCol0;");
        src.push("in vec4 modelNormalMatrixCol1;");
        src.push("in vec4 modelNormalMatrixCol2;");

        this._addMatricesUniformBlockLines(src, true);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        src.push("uniform vec4 lightAmbient;");

        for (i = 0, len = lightsState.lights.length; i < len; i++) {
            light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            src.push("uniform vec4 lightColor" + i + ";");
            if (light.type === "dir") {
                src.push("uniform vec3 lightDir" + i + ";");
            }
            if (light.type === "point") {
                src.push("uniform vec3 lightPos" + i + ";");
            }
            if (light.type === "spot") {
                src.push("uniform vec3 lightPos" + i + ";");
                src.push("uniform vec3 lightDir" + i + ";");
            }
        }

        src.push("vec3 octDecode(vec2 oct) {");
        src.push("    vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));");
        src.push("    if (v.z < 0.0) {");
        src.push("        v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);");
        src.push("    }");
        src.push("    return normalize(v);");
        src.push("}");

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec4 vColor;");

        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE | COLOR_TRANSPARENT

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");

        src.push("vec4 worldPosition =  positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("worldPosition.xyz = worldPosition.xyz + offset;");
        }

        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");

        src.push("vec4 modelNormal = vec4(octDecode(normal.xy), 0.0); ");
        src.push("vec4 worldNormal = worldNormalMatrix * vec4(dot(modelNormal, modelNormalMatrixCol0), dot(modelNormal, modelNormalMatrixCol1), dot(modelNormal, modelNormalMatrixCol2), 0.0);");
        src.push("vec3 viewNormal = normalize(vec4(viewNormalMatrix * worldNormal).xyz);");

        src.push("vec3 reflectedColor = vec3(0.0, 0.0, 0.0);");
        src.push("vec3 viewLightDir = vec3(0.0, 0.0, -1.0);");

        src.push("float lambertian = 1.0;");
        for (i = 0, len = lightsState.lights.length; i < len; i++) {
            light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            if (light.type === "dir") {
                if (light.space === "view") {
                    src.push("viewLightDir = normalize(lightDir" + i + ");");
                } else {
                    src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "point") {
                if (light.space === "view") {
                    src.push("viewLightDir = -normalize(lightPos" + i + " - viewPosition.xyz);");
                } else {
                    src.push("viewLightDir = -normalize((viewMatrix * vec4(lightPos" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "spot") {
                if (light.space === "view") {
                    src.push("viewLightDir = normalize(lightDir" + i + ");");
                } else {
                    src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else {
                continue;
            }
            src.push("lambertian = max(dot(-viewNormal, viewLightDir), 0.0);");
            src.push("reflectedColor += lambertian * (lightColor" + i + ".rgb * lightColor" + i + ".a);");
        }

        src.push("vec3 rgb = (vec3(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0));");
        src.push("vColor =  vec4((lightAmbient.rgb * lightAmbient.a * rgb) + (reflectedColor * rgb), float(color.a) / 255.0);");

        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }

        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }

        src.push("gl_Position = clipPos;");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing geometry drawing fragment shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (this._withSAO) {
            src.push("uniform sampler2D uOcclusionTexture;");
            src.push("uniform vec4      uSAOParams;");
            src.push("const float       packUpscale = 256. / 255.;");
            src.push("const float       unpackDownScale = 255. / 256.;");
            src.push("const vec3        packFactors = vec3( 256. * 256. * 256., 256. * 256.,  256. );");
            src.push("const vec4        unPackFactors = unpackDownScale / vec4( packFactors, 1. );");

            src.push("float unpackRGBToFloat( const in vec4 v ) {");
            src.push("    return dot( v, unPackFactors );");
            src.push("}");
        }

        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
            src.push("uniform float sliceThickness;");
            src.push("uniform vec4 sliceColor;");
        }
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        src.push("  vec4 newColor;");
        src.push("  newColor = vColor;");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > sliceThickness) { ");
            src.push("      discard;");
            src.push("  }");
            src.push("  if (dist > 0.0) { ");
            src.push("      newColor = sliceColor;");
            src.push("  }");
            src.push("}");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    float dx = dFdx(vFragDepth);");
            src.push("    float dy = dFdy(vFragDepth);");
            src.push("    float diff = sqrt(dx*dx+dy*dy);");
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth + diff ) * logDepthBufFC * 0.5;");
        } else {
            src.push("    float dx = dFdx(gl_FragCoord.z);");
            src.push("    float dy = dFdy(gl_FragCoord.z);");
            src.push("    float diff = sqrt(dx*dx+dy*dy);");
            src.push("    gl_FragDepth = gl_FragCoord.z + diff;");
        }

        // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top
        // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject

        if (this._withSAO) {
            src.push("   float viewportWidth     = uSAOParams[0];");
            src.push("   float viewportHeight    = uSAOParams[1];");
            src.push("   float blendCutoff       = uSAOParams[2];");
            src.push("   float blendFactor       = uSAOParams[3];");
            src.push("   vec2 uv                 = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);");
            src.push("   float ambient           = smoothstep(blendCutoff, 1.0, unpackRGBToFloat(texture(uOcclusionTexture, uv))) * blendFactor;");
            src.push("   outColor                = vec4(newColor.rgb * ambient, 1.0);");
        } else {
            src.push("    outColor           = newColor;");
        }
        src.push("}");
        return src;
    }
}

/**
 * @private
 */


class TrianglesFlatColorRenderer extends TrianglesInstancingRenderer {
    _getHash() {
        const scene = this._scene;
        return [scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";");
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing geometry flat-shading drawing vertex shader");

        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        src.push("in vec4 color;");
        src.push("in float flags;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        this._addMatricesUniformBlockLines(src);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }

        src.push("out vec4 vViewPosition;");
        src.push("out vec4 vColor;");

        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE | COLOR_TRANSPARENT

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");

        src.push("vec4 worldPosition =  positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }

        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("vViewPosition = viewPosition;");
        src.push("vColor = vec4(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0, float(color.a) / 255.0);");

        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }

        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }

        src.push("gl_Position = clipPos;");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const lightsState = scene._lightsState;
        let i;
        let len;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing geometry flat-shading drawing fragment shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (this._withSAO) {
            src.push("uniform sampler2D uOcclusionTexture;");
            src.push("uniform vec4      uSAOParams;");
            src.push("const float       packUpscale = 256. / 255.;");
            src.push("const float       unpackDownScale = 255. / 256.;");
            src.push("const vec3        packFactors = vec3( 256. * 256. * 256., 256. * 256.,  256. );");
            src.push("const vec4        unPackFactors = unpackDownScale / vec4( packFactors, 1. );");

            src.push("float unpackRGBToFloat( const in vec4 v ) {");
            src.push("    return dot( v, unPackFactors );");
            src.push("}");
        }

        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
            src.push("uniform float sliceThickness;");
            src.push("uniform vec4 sliceColor;");
        }

        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec4 lightAmbient;");

        for (i = 0, len = lightsState.lights.length; i < len; i++) {
            const light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            src.push("uniform vec4 lightColor" + i + ";");
            if (light.type === "dir") {
                src.push("uniform vec3 lightDir" + i + ";");
            }
            if (light.type === "point") {
                src.push("uniform vec3 lightPos" + i + ";");
            }
            if (light.type === "spot") {
                src.push("uniform vec3 lightPos" + i + ";");
                src.push("uniform vec3 lightDir" + i + ";");
            }
        }

        src.push("in vec4 vViewPosition;");
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");

        src.push("void main(void) {");
        src.push("  vec4 newColor;");
        src.push("  newColor = vColor;");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > sliceThickness) { ");
            src.push("      discard;");
            src.push("  }");
            src.push("  if (dist > 0.0) { ");
            src.push("      newColor = sliceColor;");
            src.push("  }");
            src.push("}");
        }

        src.push("vec3 reflectedColor = vec3(0.0, 0.0, 0.0);");
        src.push("vec3 viewLightDir = vec3(0.0, 0.0, -1.0);");

        src.push("float lambertian = 1.0;");

        src.push("vec3 xTangent = dFdx( vViewPosition.xyz );");
        src.push("vec3 yTangent = dFdy( vViewPosition.xyz );");
        src.push("vec3 viewNormal = normalize( cross( xTangent, yTangent ) );");

        for (i = 0, len = lightsState.lights.length; i < len; i++) {
            const light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            if (light.type === "dir") {
                if (light.space === "view") {
                    src.push("viewLightDir = normalize(lightDir" + i + ");");
                } else {
                    src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "point") {
                if (light.space === "view") {
                    src.push("viewLightDir = -normalize(lightPos" + i + " - viewPosition.xyz);");
                } else {
                    src.push("viewLightDir = -normalize((viewMatrix * vec4(lightPos" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "spot") {
                if (light.space === "view") {
                    src.push("viewLightDir = normalize(lightDir" + i + ");");
                } else {
                    src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else {
                continue;
            }
            src.push("lambertian = max(dot(-viewNormal, viewLightDir), 0.0);");
            src.push("reflectedColor += lambertian * (lightColor" + i + ".rgb * lightColor" + i + ".a);");
        }

        src.push("vec4 fragColor = vec4((lightAmbient.rgb * lightAmbient.a * newColor.rgb) + (reflectedColor * newColor.rgb), newColor.a);");

        if (this._withSAO) {
            // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top
            // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject
            src.push("   float viewportWidth     = uSAOParams[0];");
            src.push("   float viewportHeight    = uSAOParams[1];");
            src.push("   float blendCutoff       = uSAOParams[2];");
            src.push("   float blendFactor       = uSAOParams[3];");
            src.push("   vec2 uv                 = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);");
            src.push("   float ambient           = smoothstep(blendCutoff, 1.0, unpackRGBToFloat(texture(uOcclusionTexture, uv))) * blendFactor;");
            src.push("   outColor            = vec4(fragColor.rgb * ambient, 1.0);");
        } else {
            src.push("    outColor           = fragColor;");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        } else {
            src.push("    float dx = dFdx(gl_FragCoord.z);");
            src.push("    float dy = dFdy(gl_FragCoord.z);");
            src.push("    float diff = sqrt(dx*dx+dy*dy);");
            src.push("    gl_FragDepth = gl_FragCoord.z + diff;");
        }

        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class TrianglesSilhouetteRenderer extends TrianglesInstancingRenderer {

    drawLayer(frameCtx, instancingLayer, renderPass) {
        // TODO color uniform true ???
        super.drawLayer(frameCtx, instancingLayer, renderPass, { colorUniform: true });
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing silhouette vertex shader");

        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("in vec4 color;");

        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec4 silhouetteColor;");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }

        src.push("out vec4 vColor;");

        src.push("void main(void) {");

        // silhouetteFlag = NOT_RENDERED | SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED
        // renderPass = SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED

        src.push(`int silhouetteFlag = int(flags) >> 4 & 0xF;`);
        src.push(`if (silhouetteFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");

        src.push("vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");

        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("vColor = vec4(silhouetteColor.r, silhouetteColor.g, silhouetteColor.b, min(silhouetteColor.a, float(color.a) / 255.0));");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing fill fragment shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
            src.push("uniform float sliceThickness;");
            src.push("uniform vec4 sliceColor;");
        }
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        src.push("  vec4 newColor;");
        src.push("  newColor = vColor;");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > sliceThickness) { ");
            src.push("      discard;");
            src.push("  }");
            src.push("  if (dist > 0.0) { ");
            src.push("      newColor = sliceColor;");
            src.push("  }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outColor = newColor;");
        src.push("}");
        return src;
    }
}

/**
 * @private
 */


class EdgesRenderer extends TrianglesInstancingRenderer {
    constructor(scene, withSAO) {
        super(scene, withSAO, {instancing: true, edges: true});
    }
}

/**
 * @private
 */


class EdgesEmphasisRenderer extends EdgesRenderer {

    drawLayer(frameCtx, instancingLayer, renderPass) {
        super.drawLayer(frameCtx, instancingLayer, renderPass, {colorUniform: true});
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// EdgesEmphasisRenderer vertex shader");

        src.push("uniform int renderPass;");
        src.push("uniform vec4 color;");

        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        this._addMatricesUniformBlockLines(src);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }

        src.push("out vec4 vColor;");

        src.push("void main(void) {");

        // edgeFlag = NOT_RENDERED | EDGES_COLOR_OPAQUE | EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED
        // renderPass = EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED

        src.push(`int edgeFlag = int(flags) >> 8 & 0xF;`);
        src.push(`if (edgeFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");

        src.push("vec4 worldPosition = worldMatrix * positionsDecodeMatrix * vec4(position, 1.0); ");

        src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("vColor = vec4(color.r, color.g, color.b, color.a);");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// EdgesEmphasisRenderer fragment shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > 0.0) { discard; }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outColor = vColor;");
        src.push("}");
        return src;
    }
}

/**
 * @private
 */


class EdgesColorRenderer extends EdgesRenderer {

    drawLayer(frameCtx, batchingLayer, renderPass) {
        super.drawLayer(frameCtx, batchingLayer, renderPass, { colorUniform: false });
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// EdgesColorRenderer vertex shader");

        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        src.push("in vec4 color;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        this._addMatricesUniformBlockLines(src);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }

        src.push("out vec4 vColor;");

        src.push("void main(void) {");

        // edgeFlag = NOT_RENDERED | EDGES_COLOR_OPAQUE | EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED
        // renderPass = EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED

        src.push(`int edgeFlag = int(flags) >> 8 & 0xF;`);
        src.push(`if (edgeFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");
        src.push("vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        //   src.push("vColor = vec4(float(color.r-100.0) / 255.0, float(color.g-100.0) / 255.0, float(color.b-100.0) / 255.0, float(color.a) / 255.0);");
        src.push("vColor = vec4(float(color.r*0.5) / 255.0, float(color.g*0.5) / 255.0, float(color.b*0.5) / 255.0, float(color.a) / 255.0);");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// EdgesColorRenderer fragment shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > 0.0) { discard; }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outColor = vColor;");
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class TrianglesPickMeshRenderer extends TrianglesInstancingRenderer {

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing geometry picking vertex shader");
        
        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("in vec4 pickColor;");

        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        this._addRemapClipPosLines(src);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec4 vPickColor;");
        src.push("void main(void) {");

        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK

        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");


        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }

        src.push("  vec4 viewPosition  = viewMatrix * worldPosition; ");

        src.push("  vPickColor = vec4(float(pickColor.r) / 255.0, float(pickColor.g) / 255.0, float(pickColor.b) / 255.0, float(pickColor.a) / 255.0);");
        if (clipping) {
            src.push("  vWorldPosition = worldPosition;");
            src.push("  vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
           src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = remapClipPos(clipPos);");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Batched geometry picking fragment shader");
        
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vPickColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outColor = vPickColor; ");
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class TrianglesPickDepthRenderer extends TrianglesInstancingRenderer {

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing geometry depth vertex shader");


        src.push("uniform int renderPass;");

        src.push("in vec3 position;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        src.push("in float flags;");
        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        this._addRemapClipPosLines(src);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }

        src.push("out vec4 vViewPosition;");
        src.push("void main(void) {");

        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK

        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");
        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");

        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("  vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("  vWorldPosition = worldPosition;");
            src.push("  vFlags = flags;");
        }
        src.push("  vViewPosition = viewPosition;");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = remapClipPos(clipPos);");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Batched geometry depth fragment shader");


        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }

        src.push("uniform float pickZNear;");
        src.push("uniform float pickZFar;");

        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vViewPosition;");
        src.push("vec4 packDepth(const in float depth) {");
        src.push("  const vec4 bitShift = vec4(256.0*256.0*256.0, 256.0*256.0, 256.0, 1.0);");
        src.push("  const vec4 bitMask  = vec4(0.0, 1.0/256.0, 1.0/256.0, 1.0/256.0);");
        src.push("  vec4 res = fract(depth * bitShift);");
        src.push("  res -= res.xxyz * bitMask;");
        src.push("  return res;");
        src.push("}");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("    float zNormalizedDepth = abs((pickZNear + vViewPosition.z) / (pickZFar - pickZNear));");
        src.push("    outColor = packDepth(zNormalizedDepth); "); // Must be linear depth
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class TrianglesPickNormalsRenderer extends TrianglesInstancingRenderer {

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing geometry normals vertex shader");
        
        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in vec2 normal;");
        src.push("in float flags;");
        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");
        src.push("in vec4 modelNormalMatrixCol0;");
        src.push("in vec4 modelNormalMatrixCol1;");
        src.push("in vec4 modelNormalMatrixCol2;");
        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        this._addRemapClipPosLines(src, 3);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }
        src.push("vec3 octDecode(vec2 oct) {");
        src.push("    vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));");
        src.push("    if (v.z < 0.0) {");
        src.push("        v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);");
        src.push("    }");
        src.push("    return normalize(v);");
        src.push("}");
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec3 vWorldNormal;");
        src.push("void main(void) {");

        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK

        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");
        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("  vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("  vec4 modelNormal = vec4(octDecode(normal.xy), 0.0); ");
        src.push("  vec3 worldNormal = vec3(dot(modelNormal, modelNormalMatrixCol0), dot(modelNormal, modelNormalMatrixCol1), dot(modelNormal, modelNormalMatrixCol2));");
        src.push("  vWorldNormal = worldNormal;");
        if (clipping) {
            src.push("  vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
           src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = remapClipPos(clipPos);");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Batched geometry normals fragment shader");

        

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }

        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec3 vWorldNormal;");
        src.push("out highp ivec4 outNormal;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push(`    outNormal = ivec4(vWorldNormal * float(${math.MAX_INT}), 1.0);`);
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class TrianglesOcclusionRenderer extends TrianglesInstancingRenderer {

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// TrianglesInstancingOcclusionRenderer vertex shader");

        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in vec4 color;");
        src.push("in float flags;");
        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        this._addMatricesUniformBlockLines(src);
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE | COLOR_TRANSPARENT

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("      gl_Position = vec4(0.0, 0.0, 0.0, 0.0);");

        src.push("} else {");
        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("  vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("  vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        src.push("gl_Position = clipPos;");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// TrianglesInstancingOcclusionRenderer fragment shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        src.push("   outColor = vec4(0.0, 0.0, 1.0, 1.0); "); // Occluders are blue
        src.push("}");
        return src;
    }
}

/**
 * @private
 */


class TrianglesDepthRenderer extends TrianglesInstancingRenderer {

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing geometry depth drawing vertex shader");

        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("in vec4 modelMatrixCol0;");
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        this._addMatricesUniformBlockLines(src);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec2 vHighPrecisionZW;");
        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");
        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("  vec4 viewPosition  = viewMatrix * worldPosition; ");

        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("vHighPrecisionZW = gl_Position.zw;");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        let i;
        let len;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing geometry depth drawing fragment shader");

        src.push("precision highp float;");
        src.push("precision highp int;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec2 vHighPrecisionZW;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("float fragCoordZ = 0.5 * vHighPrecisionZW[0] / vHighPrecisionZW[1] + 0.5;");
        src.push("    outColor = vec4(vec3(1.0 - fragCoordZ), 1.0); ");
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class TrianglesNormalsRenderer extends TrianglesInstancingRenderer {

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing geometry normals drawing vertex shader");
        
        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in vec3 normal;");
        src.push("in vec4 color;");
        src.push("in float flags;");
        src.push("in vec4 modelMatrixCol0;");
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        this._addMatricesUniformBlockLines(src, true);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }
        src.push("vec3 octDecode(vec2 oct) {");
        src.push("    vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));");
        src.push("    if (v.z < 0.0) {");
        src.push("        v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);");
        src.push("    }");
        src.push("    return normalize(v);");
        src.push("}");
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec3 vViewNormal;");
        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("      gl_Position = vec4(0.0, 0.0, 0.0, 0.0);");
        src.push("} else {");
        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("  vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("  vec4 worldNormal    = worldNormalMatrix * vec4(octDecode(normal.xy), 0.0); ");
        src.push("  vec3 viewNormal     = normalize((viewNormalMatrix * worldNormal).xyz);");

        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("  vViewNormal = viewNormal;");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
           src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing geometry depth drawing fragment shader");
        
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec3 vViewNormal;");
        src.push("vec3 packNormalToRGB( const in vec3 normal ) {");
        src.push("    return normalize( normal ) * 0.5 + 0.5;");
        src.push("}");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("    outColor = vec4(packNormalToRGB(vViewNormal), 1.0); ");
        src.push("}");
        return src;
    }
}

/**
 * Renders InstancingLayer fragment depths to a shadow map.
 *
 * @private
 */
class TrianglesShadowRenderer extends TrianglesInstancingRenderer {

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing geometry shadow drawing vertex shader");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in vec4 color;");
        src.push("in float flags;");
        src.push("in vec4 modelMatrixCol0;");
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");
        src.push("uniform mat4 shadowViewMatrix;");
        src.push("uniform mat4 shadowProjMatrix;");

        this._addMatricesUniformBlockLines(src);

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("void main(void) {");
        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push("bool visible = (colorFlag > 0);");
        src.push("bool transparent  = ((float(color.a) / 255.0) < 1.0);");
        src.push(`if (!visible || transparent) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("} else {");
        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("  vec4 viewPosition  = shadowViewMatrix * worldPosition; ");

        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("  gl_Position = shadowProjMatrix * viewPosition;");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing geometry depth drawing fragment shader");
        
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec3 vViewNormal;");
        src.push("vec3 packNormalToRGB( const in vec3 normal ) {");
        src.push("    return normalize( normal ) * 0.5 + 0.5;");
        src.push("}");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("    outColor = vec4(packNormalToRGB(vViewNormal), 1.0); ");
        src.push("}");
        return src;
    }
}

const TEXTURE_DECODE_FUNCS = {};
TEXTURE_DECODE_FUNCS[LinearEncoding] = "linearToLinear";
TEXTURE_DECODE_FUNCS[sRGBEncoding] = "sRGBToLinear";

/**
 * @private
 */
class TrianglesPBRRenderer extends TrianglesInstancingRenderer {
    _getHash() {
        const scene = this._scene;
        return [scene.gammaOutput, scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";");
    }

    drawLayer(frameCtx, layer, renderPass) {
        super.drawLayer(frameCtx, layer, renderPass, { incrementDrawState: true });
    }

    _buildVertexShader() {

        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const lightsState = scene._lightsState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const clippingCaps = sectionPlanesState.clippingCaps;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing geometry quality drawing vertex shader");


        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        src.push("in vec3 normal;");
        src.push("in vec4 color;");
        src.push("in vec2 uv;");
        src.push("in vec2 metallicRoughness;");
        src.push("in float flags;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        src.push("in vec4 modelNormalMatrixCol0;");
        src.push("in vec4 modelNormalMatrixCol1;");
        src.push("in vec4 modelNormalMatrixCol2;");

        this._addMatricesUniformBlockLines(src, true);

        src.push("uniform mat3 uvDecodeMatrix;");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        src.push("vec3 octDecode(vec2 oct) {");
        src.push("    vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));");
        src.push("    if (v.z < 0.0) {");
        src.push("        v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);");
        src.push("    }");
        src.push("    return normalize(v);");
        src.push("}");

        src.push("out vec4 vViewPosition;");
        src.push("out vec3 vViewNormal;");
        src.push("out vec4 vColor;");
        src.push("out vec2 vUV;");
        src.push("out vec2 vMetallicRoughness;");

        if (lightsState.lightMaps.length > 0) {
            src.push("out vec3 vWorldNormal;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
            if (clippingCaps) {
                src.push("out vec4 vClipPosition;");
            }
        }

        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE | COLOR_TRANSPARENT

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");

        src.push("vec4 worldPosition =  positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }

        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");

        src.push("vec4 modelNormal = vec4(octDecode(normal.xy), 0.0); ");
        src.push("vec4 worldNormal = worldNormalMatrix * vec4(dot(modelNormal, modelNormalMatrixCol0), dot(modelNormal, modelNormalMatrixCol1), dot(modelNormal, modelNormalMatrixCol2), 1.0);");
        src.push("vec3 viewNormal = vec4(viewNormalMatrix * worldNormal).xyz;");

        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }

        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
            if (clippingCaps) {
                src.push("vClipPosition = clipPos;");
            }
        }

        src.push("vViewPosition = viewPosition;");
        src.push("vViewNormal = viewNormal;");
        src.push("vColor = color;");
        src.push("vUV = (uvDecodeMatrix * vec3(uv, 1.0)).xy;");
        src.push("vMetallicRoughness = metallicRoughness;");

        if (lightsState.lightMaps.length > 0) {
            src.push("vWorldNormal = worldNormal.xyz;");
        }

        src.push("gl_Position = clipPos;");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {

        const scene = this._scene;
        const gammaOutput = scene.gammaOutput; // If set, then it expects that all textures and colors need to be outputted in premultiplied gamma. Default is false.
        const sectionPlanesState = scene._sectionPlanesState;
        const lightsState = scene._lightsState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const clippingCaps = sectionPlanesState.clippingCaps;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing geometry quality drawing fragment shader");


        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }

        src.push("uniform sampler2D uColorMap;");
        src.push("uniform sampler2D uMetallicRoughMap;");
        src.push("uniform sampler2D uEmissiveMap;");
        src.push("uniform sampler2D uNormalMap;");

        if (this._withSAO) {
            src.push("uniform sampler2D uOcclusionTexture;");
            src.push("uniform vec4      uSAOParams;");

            src.push("const float       packUpscale = 256. / 255.;");
            src.push("const float       unpackDownScale = 255. / 256.;");
            src.push("const vec3        packFactors = vec3( 256. * 256. * 256., 256. * 256.,  256. );");
            src.push("const vec4        unPackFactors = unpackDownScale / vec4( packFactors, 1. );");

            src.push("float unpackRGBToFloat( const in vec4 v ) {");
            src.push("    return dot( v, unPackFactors );");
            src.push("}");
        }

        if (lightsState.reflectionMaps.length > 0) {
            src.push("uniform samplerCube reflectionMap;");
        }

        if (lightsState.lightMaps.length > 0) {
            src.push("uniform samplerCube lightMap;");
        }

        src.push("uniform vec4 lightAmbient;");

        for (let i = 0, len = lightsState.lights.length; i < len; i++) {
            const light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            src.push("uniform vec4 lightColor" + i + ";");
            if (light.type === "dir") {
                src.push("uniform vec3 lightDir" + i + ";");
            }
            if (light.type === "point") {
                src.push("uniform vec3 lightPos" + i + ";");
            }
            if (light.type === "spot") {
                src.push("uniform vec3 lightPos" + i + ";");
                src.push("uniform vec3 lightDir" + i + ";");
            }
        }

        src.push("uniform float gammaFactor;");
        src.push("vec4 linearToLinear( in vec4 value ) {");
        src.push("  return value;");
        src.push("}");
        src.push("vec4 sRGBToLinear( in vec4 value ) {");
        src.push("  return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.w );");
        src.push("}");
        src.push("vec4 gammaToLinear( in vec4 value) {");
        src.push("  return vec4( pow( value.xyz, vec3( gammaFactor ) ), value.w );");
        src.push("}");
        if (gammaOutput) {
            src.push("vec4 linearToGamma( in vec4 value, in float gammaFactor ) {");
            src.push("  return vec4( pow( value.xyz, vec3( 1.0 / gammaFactor ) ), value.w );");
            src.push("}");
        }

        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            if (clippingCaps) {
                src.push("in vec4 vClipPosition;");
            }
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }

        src.push("in vec4 vViewPosition;");
        src.push("in vec3 vViewNormal;");
        src.push("in vec4 vColor;");
        src.push("in vec2 vUV;");
        src.push("in vec2 vMetallicRoughness;");

        if (lightsState.lightMaps.length > 0) {
            src.push("in vec3 vWorldNormal;");
        }

        this._addMatricesUniformBlockLines(src, true);

        // CONSTANT DEFINITIONS

        src.push("#define PI 3.14159265359");
        src.push("#define RECIPROCAL_PI 0.31830988618");
        src.push("#define RECIPROCAL_PI2 0.15915494");
        src.push("#define EPSILON 1e-6");

        src.push("#define saturate(a) clamp( a, 0.0, 1.0 )");

        // UTILITY DEFINITIONS

        src.push("vec3 perturbNormal2Arb( vec3 eye_pos, vec3 surf_norm, vec2 uv ) {");
        src.push("       vec3 texel = texture( uNormalMap, uv ).xyz;");
        src.push("       if (texel.r == 0.0 && texel.g == 0.0 && texel.b == 0.0) {");
        src.push("              return normalize(surf_norm );");
        src.push("       }");
        src.push("      vec3 q0 = vec3( dFdx( eye_pos.x ), dFdx( eye_pos.y ), dFdx( eye_pos.z ) );");
        src.push("      vec3 q1 = vec3( dFdy( eye_pos.x ), dFdy( eye_pos.y ), dFdy( eye_pos.z ) );");
        src.push("      vec2 st0 = dFdx( uv.st );");
        src.push("      vec2 st1 = dFdy( uv.st );");
        src.push("      vec3 S = normalize( q0 * st1.t - q1 * st0.t );");
        src.push("      vec3 T = normalize( -q0 * st1.s + q1 * st0.s );");
        src.push("      vec3 N = normalize( surf_norm );");
        src.push("      vec3 mapN = texel.xyz * 2.0 - 1.0;");
        src.push("      mat3 tsn = mat3( S, T, N );");
        //     src.push("      mapN *= 3.0;");
        src.push("      return normalize( tsn * mapN );");
        src.push("}");

        src.push("vec3 inverseTransformDirection(in vec3 dir, in mat4 matrix) {");
        src.push("   return normalize( ( vec4( dir, 0.0 ) * matrix ).xyz );");
        src.push("}");

        // STRUCTURES

        src.push("struct IncidentLight {");
        src.push("   vec3 color;");
        src.push("   vec3 direction;");
        src.push("};");

        src.push("struct ReflectedLight {");
        src.push("   vec3 diffuse;");
        src.push("   vec3 specular;");
        src.push("};");

        src.push("struct Geometry {");
        src.push("   vec3 position;");
        src.push("   vec3 viewNormal;");
        src.push("   vec3 worldNormal;");
        src.push("   vec3 viewEyeDir;");
        src.push("};");

        src.push("struct Material {");
        src.push("   vec3    diffuseColor;");
        src.push("   float   specularRoughness;");
        src.push("   vec3    specularColor;");
        src.push("   float   shine;"); // Only used for Phong
        src.push("};");

        // IRRADIANCE EVALUATION

        src.push("float GGXRoughnessToBlinnExponent(const in float ggxRoughness) {");
        src.push("   float r = ggxRoughness + 0.0001;");
        src.push("   return (2.0 / (r * r) - 2.0);");
        src.push("}");

        src.push("float getSpecularMIPLevel(const in float blinnShininessExponent, const in int maxMIPLevel) {");
        src.push("   float maxMIPLevelScalar = float( maxMIPLevel );");
        src.push("   float desiredMIPLevel = maxMIPLevelScalar - 0.79248 - 0.5 * log2( ( blinnShininessExponent * blinnShininessExponent ) + 1.0 );");
        src.push("   return clamp( desiredMIPLevel, 0.0, maxMIPLevelScalar );");
        src.push("}");

        if (lightsState.reflectionMaps.length > 0) {
            src.push("vec3 getLightProbeIndirectRadiance(const in vec3 reflectVec, const in float blinnShininessExponent, const in int maxMIPLevel) {");
            src.push("   float mipLevel = 0.5 * getSpecularMIPLevel(blinnShininessExponent, maxMIPLevel);"); //TODO: a random factor - fix this
            src.push("   vec3 envMapColor = " + TEXTURE_DECODE_FUNCS[lightsState.reflectionMaps[0].encoding] + "(texture(reflectionMap, reflectVec, mipLevel)).rgb;");
            src.push("  return envMapColor;");
            src.push("}");
        }

        // SPECULAR BRDF EVALUATION

        src.push("vec3 F_Schlick(const in vec3 specularColor, const in float dotLH) {");
        src.push("   float fresnel = exp2( ( -5.55473 * dotLH - 6.98316 ) * dotLH );");
        src.push("   return ( 1.0 - specularColor ) * fresnel + specularColor;");
        src.push("}");

        src.push("float G_GGX_Smith(const in float alpha, const in float dotNL, const in float dotNV) {");
        src.push("   float a2 = ( alpha * alpha );");
        src.push("   float gl = dotNL + sqrt( a2 + ( 1.0 - a2 ) * ( dotNL * dotNL ) );");
        src.push("   float gv = dotNV + sqrt( a2 + ( 1.0 - a2 ) * ( dotNV * dotNV ) );");
        src.push("   return 1.0 / ( gl * gv );");
        src.push("}");

        src.push("float G_GGX_SmithCorrelated(const in float alpha, const in float dotNL, const in float dotNV) {");
        src.push("   float a2 = ( alpha * alpha );");
        src.push("   float gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * ( dotNV * dotNV ) );");
        src.push("   float gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * ( dotNL * dotNL ) );");
        src.push("   return 0.5 / max( gv + gl, EPSILON );");
        src.push("}");

        src.push("float D_GGX(const in float alpha, const in float dotNH) {");
        src.push("   float a2 = ( alpha * alpha );");
        src.push("   float denom = ( dotNH * dotNH) * ( a2 - 1.0 ) + 1.0;");
        src.push("   return RECIPROCAL_PI * a2 / ( denom * denom);");
        src.push("}");

        src.push("vec3 BRDF_Specular_GGX(const in IncidentLight incidentLight, const in Geometry geometry, const in vec3 specularColor, const in float roughness) {");
        src.push("   float alpha = ( roughness * roughness );");
        src.push("   vec3 halfDir = normalize( incidentLight.direction + geometry.viewEyeDir );");
        src.push("   float dotNL = saturate( dot( geometry.viewNormal, incidentLight.direction ) );");
        src.push("   float dotNV = saturate( dot( geometry.viewNormal, geometry.viewEyeDir ) );");
        src.push("   float dotNH = saturate( dot( geometry.viewNormal, halfDir ) );");
        src.push("   float dotLH = saturate( dot( incidentLight.direction, halfDir ) );");
        src.push("   vec3  F = F_Schlick( specularColor, dotLH );");
        src.push("   float G = G_GGX_SmithCorrelated( alpha, dotNL, dotNV );");
        src.push("   float D = D_GGX( alpha, dotNH );");
        src.push("   return F * (G * D);");
        src.push("}");

        src.push("vec3 BRDF_Specular_GGX_Environment(const in Geometry geometry, const in vec3 specularColor, const in float roughness) {");
        src.push("   float dotNV = saturate(dot(geometry.viewNormal, geometry.viewEyeDir));");
        src.push("   const vec4 c0 = vec4( -1, -0.0275, -0.572,  0.022);");
        src.push("   const vec4 c1 = vec4(  1,  0.0425,   1.04, -0.04);");
        src.push("   vec4 r = roughness * c0 + c1;");
        src.push("   float a004 = min(r.x * r.x, exp2(-9.28 * dotNV)) * r.x + r.y;");
        src.push("   vec2 AB    = vec2(-1.04, 1.04) * a004 + r.zw;");
        src.push("   return specularColor * AB.x + AB.y;");
        src.push("}");

        if (lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0) {

            src.push("void computePBRLightMapping(const in Geometry geometry, const in Material material, inout ReflectedLight reflectedLight) {");

            if (lightsState.lightMaps.length > 0) {
                src.push("   vec3 irradiance = " + TEXTURE_DECODE_FUNCS[lightsState.lightMaps[0].encoding] + "(texture(lightMap, geometry.worldNormal)).rgb;");
                src.push("   irradiance *= PI;");
                src.push("   vec3 diffuseBRDFContrib = (RECIPROCAL_PI * material.diffuseColor);");
                src.push("   reflectedLight.diffuse += irradiance * diffuseBRDFContrib;");
            }

            if (lightsState.reflectionMaps.length > 0) {
                src.push("   vec3 reflectVec             = reflect(geometry.viewEyeDir, geometry.viewNormal);");
                src.push("   reflectVec                  = inverseTransformDirection(reflectVec, viewMatrix);");
                src.push("   float blinnExpFromRoughness = GGXRoughnessToBlinnExponent(material.specularRoughness);");
                src.push("   vec3 radiance               = getLightProbeIndirectRadiance(reflectVec, blinnExpFromRoughness, 8);");
                src.push("   vec3 specularBRDFContrib    = BRDF_Specular_GGX_Environment(geometry, material.specularColor, material.specularRoughness);");
                src.push("   reflectedLight.specular     += radiance * specularBRDFContrib;");
            }

            src.push("}");
        }

        // MAIN LIGHTING COMPUTATION FUNCTION

        src.push("void computePBRLighting(const in IncidentLight incidentLight, const in Geometry geometry, const in Material material, inout ReflectedLight reflectedLight) {");
        src.push("   float dotNL     = saturate(dot(geometry.viewNormal, incidentLight.direction));");
        src.push("   vec3 irradiance = dotNL * incidentLight.color * PI;");
        src.push("   reflectedLight.diffuse  += irradiance * (RECIPROCAL_PI * material.diffuseColor);");
        src.push("   reflectedLight.specular += irradiance * BRDF_Specular_GGX(incidentLight, geometry, material.specularColor, material.specularRoughness);");
        src.push("}");

        src.push("out vec4 outColor;");

        src.push("void main(void) {");

        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            if (clippingCaps) {
                src.push("  if (dist > (0.002 * vClipPosition.w)) {");
                src.push("      discard;");
                src.push("  }");
                src.push("  if (dist > 0.0) { ");
                src.push("      outColor=vec4(1.0, 0.0, 0.0, 1.0);");
                if (scene.logarithmicDepthBufferEnabled) {
                    src.push("  gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
                }
                src.push("  return;");
                src.push("}");
            } else {
                src.push("  if (dist > 0.0) { ");
                src.push("      discard;");
                src.push("  }");
            }
            src.push("}");
        }

        src.push("IncidentLight  light;");
        src.push("Material       material;");
        src.push("Geometry       geometry;");
        src.push("ReflectedLight reflectedLight = ReflectedLight(vec3(0.0,0.0,0.0), vec3(0.0,0.0,0.0));");

        src.push("vec3 rgb = (vec3(float(vColor.r) / 255.0, float(vColor.g) / 255.0, float(vColor.b) / 255.0));");
        src.push("float opacity = float(vColor.a) / 255.0;");

        src.push("vec3  baseColor = rgb;");
        src.push("float specularF0 = 1.0;");
        src.push("float metallic = float(vMetallicRoughness.r) / 255.0;");
        src.push("float roughness = float(vMetallicRoughness.g) / 255.0;");
        src.push("float dielectricSpecular = 0.16 * specularF0 * specularF0;");

        src.push("vec4 colorTexel = sRGBToLinear(texture(uColorMap, vUV));");
        src.push("baseColor *= colorTexel.rgb;");
        // src.push("opacity = colorTexel.a;");

        src.push("vec3 metalRoughTexel = texture(uMetallicRoughMap, vUV).rgb;");
        src.push("metallic *= metalRoughTexel.b;");
        src.push("roughness *= metalRoughTexel.g;");

        src.push("vec3 viewNormal = perturbNormal2Arb( vViewPosition.xyz, normalize(vViewNormal), vUV );");

        src.push("material.diffuseColor      = baseColor * (1.0 - dielectricSpecular) * (1.0 - metallic);");
        src.push("material.specularRoughness = clamp(roughness, 0.04, 1.0);");
        src.push("material.specularColor     = mix(vec3(dielectricSpecular), baseColor, metallic);");

        src.push("geometry.position      = vViewPosition.xyz;");
        src.push("geometry.viewNormal    = -normalize(viewNormal);");
        src.push("geometry.viewEyeDir    = normalize(vViewPosition.xyz);");

        if (lightsState.lightMaps.length > 0) {
            src.push("geometry.worldNormal   = normalize(vWorldNormal);");
        }

        if (lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0) {
            src.push("computePBRLightMapping(geometry, material, reflectedLight);");
        }

        for (let i = 0, len = lightsState.lights.length; i < len; i++) {

            const light = lightsState.lights[i];

            if (light.type === "ambient") {
                continue;
            }
            if (light.type === "dir") {
                if (light.space === "view") {
                    src.push("light.direction = normalize(lightDir" + i + ");");
                } else {
                    src.push("light.direction = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "point") {
                if (light.space === "view") {
                    src.push("light.direction = normalize(lightPos" + i + " - vViewPosition.xyz);");
                } else {
                    src.push("light.direction = normalize((viewMatrix * vec4(lightPos" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "spot") {
                if (light.space === "view") {
                    src.push("light.direction = normalize(lightDir" + i + ");");
                } else {
                    src.push("light.direction = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else {
                continue;
            }

            src.push("light.color =  lightColor" + i + ".rgb * lightColor" + i + ".a;"); // a is intensity

            src.push("computePBRLighting(light, geometry, material, reflectedLight);");
        }

        src.push("vec3 emissiveColor = sRGBToLinear(texture(uEmissiveMap, vUV)).rgb;"); // TODO: correct gamma function

        src.push("vec3 outgoingLight = (lightAmbient.rgb * lightAmbient.a * baseColor * opacity * rgb) + (reflectedLight.diffuse) + (reflectedLight.specular) + emissiveColor;");
        src.push("vec4 fragColor;");

        if (this._withSAO) {
            // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top
            // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject
            src.push("   float viewportWidth     = uSAOParams[0];");
            src.push("   float viewportHeight    = uSAOParams[1];");
            src.push("   float blendCutoff       = uSAOParams[2];");
            src.push("   float blendFactor       = uSAOParams[3];");
            src.push("   vec2 uv                 = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);");
            src.push("   float ambient           = smoothstep(blendCutoff, 1.0, unpackRGBToFloat(texture(uOcclusionTexture, uv))) * blendFactor;");
            src.push("   fragColor            = vec4(outgoingLight.rgb * ambient, opacity);");
        } else {
            src.push("   fragColor            = vec4(outgoingLight.rgb, opacity);");
        }

        if (gammaOutput) {
            src.push("fragColor = linearToGamma(fragColor, gammaFactor);");
        }

        src.push("outColor = fragColor;");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }

        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class TrianglesPickNormalsFlatRenderer extends TrianglesInstancingRenderer {

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing geometry normals vertex shader");
        
        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");
        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        this._addRemapClipPosLines(src, 3);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }
        if (clipping) {
            src.push("out float vFlags;");
        }
        src.push("out vec4 vWorldPosition;");
        src.push("void main(void) {");

        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK

        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("} else {");
        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("  vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("  vWorldPosition = worldPosition;");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
           src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }

        if (clipping) {
            src.push("vFlags = flags;");
        }

        src.push("gl_Position = remapClipPos(clipPos);");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Batched geometry normals fragment shader");
        
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("in vec4 vWorldPosition;");
        if (clipping) {
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("out highp ivec4 outNormal;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("  vec3 xTangent = dFdx( vWorldPosition.xyz );");
        src.push("  vec3 yTangent = dFdy( vWorldPosition.xyz );");
        src.push("  vec3 worldNormal = normalize( cross( xTangent, yTangent ) );");
        src.push(`  outNormal = ivec4(worldNormal * float(${math.MAX_INT}), 1.0);`);
        src.push("}");
        return src;
    }
}

/**
 * @private
 */

class TrianglesColorTextureRenderer extends TrianglesInstancingRenderer {
    _getHash() {
        const scene = this._scene;
        return [scene.gammaOutput, scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";");
    }

    drawLayer(frameCtx, layer, renderPass) {
        super.drawLayer(frameCtx, layer, renderPass, { incrementDrawState: true });
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing geometry drawing vertex shader");

        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        src.push("in vec4 color;");
        src.push("in vec2 uv;");
        src.push("in float flags;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform mat3 uvDecodeMatrix;");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec4 vViewPosition;");
        src.push("out vec4 vColor;");
        src.push("out vec2 vUV;");

        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE | COLOR_TRANSPARENT

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");

        src.push("vec4 worldPosition =  positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("worldPosition.xyz = worldPosition.xyz + offset;");
        }

        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("vViewPosition = viewPosition;");
        src.push("vColor = vec4(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0, float(color.a) / 255.0);");
        src.push("vUV = (uvDecodeMatrix * vec3(uv, 1.0)).xy;");

        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }

        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }

        src.push("gl_Position = clipPos;");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const gammaOutput = scene.gammaOutput; // If set, then it expects that all textures and colors need to be outputted in premultiplied gamma. Default is false.
        const lightsState = scene._lightsState;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const useAlphaCutoff = this._useAlphaCutoff;
        const src = [];
        src.push("#version 300 es");
        src.push("// Instancing geometry drawing fragment shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("uniform sampler2D uColorMap;");
        if (this._withSAO) {
            src.push("uniform sampler2D uOcclusionTexture;");
            src.push("uniform vec4      uSAOParams;");

            src.push("const float       packUpscale = 256. / 255.;");
            src.push("const float       unpackDownScale = 255. / 256.;");
            src.push("const vec3        packFactors = vec3( 256. * 256. * 256., 256. * 256.,  256. );");
            src.push("const vec4        unPackFactors = unpackDownScale / vec4( packFactors, 1. );");

            src.push("float unpackRGBToFloat( const in vec4 v ) {");
            src.push("    return dot( v, unPackFactors );");
            src.push("}");
        }
        src.push("uniform float gammaFactor;");
        src.push("vec4 linearToLinear( in vec4 value ) {");
        src.push("  return value;");
        src.push("}");
        src.push("vec4 sRGBToLinear( in vec4 value ) {");
        src.push("  return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.w );");
        src.push("}");
        src.push("vec4 gammaToLinear( in vec4 value) {");
        src.push("  return vec4( pow( value.xyz, vec3( gammaFactor ) ), value.w );");
        src.push("}");
        if (gammaOutput) {
            src.push("vec4 linearToGamma( in vec4 value, in float gammaFactor ) {");
            src.push("  return vec4( pow( value.xyz, vec3( 1.0 / gammaFactor ) ), value.w );");
            src.push("}");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
            src.push("uniform float sliceThickness;");
            src.push("uniform vec4 sliceColor;");
        }
        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec4 lightAmbient;");
        for (let i = 0, len = lightsState.lights.length; i < len; i++) {
            const light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            src.push("uniform vec4 lightColor" + i + ";");
            if (light.type === "dir") {
                src.push("uniform vec3 lightDir" + i + ";");
            }
            if (light.type === "point") {
                src.push("uniform vec3 lightPos" + i + ";");
            }
            if (light.type === "spot") {
                src.push("uniform vec3 lightPos" + i + ";");
                src.push("uniform vec3 lightDir" + i + ";");
            }
        }

        if (useAlphaCutoff) {
            src.push("uniform float materialAlphaCutoff;");
        }

        src.push("in vec4 vViewPosition;");
        src.push("in vec4 vColor;");
        src.push("in vec2 vUV;");
        src.push("out vec4 outColor;");

        src.push("void main(void) {");
        src.push("  vec4 newColor;");
        src.push("  newColor = vColor;");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > sliceThickness) { ");
            src.push("      discard;");
            src.push("  }");
            src.push("  if (dist > 0.0) { ");
            src.push("      newColor = sliceColor;");
            src.push("  }");
            src.push("}");
        }

        src.push("vec3 reflectedColor = vec3(0.0, 0.0, 0.0);");
        src.push("vec3 viewLightDir = vec3(0.0, 0.0, -1.0);");

        src.push("float lambertian = 1.0;");

        src.push("vec3 xTangent = dFdx( vViewPosition.xyz );");
        src.push("vec3 yTangent = dFdy( vViewPosition.xyz );");
        src.push("vec3 viewNormal = normalize( cross( xTangent, yTangent ) );");

        for (let i = 0, len = lightsState.lights.length; i < len; i++) {
            const light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            if (light.type === "dir") {
                if (light.space === "view") {
                    src.push("viewLightDir = normalize(lightDir" + i + ");");
                } else {
                    src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "point") {
                if (light.space === "view") {
                    src.push("viewLightDir = -normalize(lightPos" + i + " - viewPosition.xyz);");
                } else {
                    src.push("viewLightDir = -normalize((viewMatrix * vec4(lightPos" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "spot") {
                if (light.space === "view") {
                    src.push("viewLightDir = normalize(lightDir" + i + ");");
                } else {
                    src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else {
                continue;
            }

            src.push("lambertian = max(dot(-viewNormal, viewLightDir), 0.0);");
            src.push("reflectedColor += lambertian * (lightColor" + i + ".rgb * lightColor" + i + ".a);");
        }

        src.push("vec4 color =  vec4((lightAmbient.rgb * lightAmbient.a * newColor.rgb) + (reflectedColor * newColor.rgb), newColor.a);");

        src.push("vec4 sampleColor = texture(uColorMap, vUV);");

        if (useAlphaCutoff) {
            src.push("if (sampleColor.a < materialAlphaCutoff) {");
            src.push("   discard;");
            src.push("}");
        }

        if (gammaOutput) {
            src.push("sampleColor = sRGBToLinear(sampleColor);");
        }

        src.push("vec4 colorTexel = color * sampleColor;");

        src.push("float opacity = color.a;");

        if (this._withSAO) {
            // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top
            // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject
            src.push("   float viewportWidth     = uSAOParams[0];");
            src.push("   float viewportHeight    = uSAOParams[1];");
            src.push("   float blendCutoff       = uSAOParams[2];");
            src.push("   float blendFactor       = uSAOParams[3];");
            src.push("   vec2 uv                 = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);");
            src.push("   float ambient           = smoothstep(blendCutoff, 1.0, unpackRGBToFloat(texture(uOcclusionTexture, uv))) * blendFactor;");

            src.push("   outColor                = vec4(colorTexel.rgb * ambient, opacity);");
        } else {
            src.push("   outColor                = vec4(colorTexel.rgb, opacity);");
        }

        if (gammaOutput) {
            src.push("outColor = linearToGamma(outColor, gammaFactor);");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }

        src.push("}");
        return src;
    }
}

const tempVec3a$B = math.vec3();
const tempVec3b$x = math.vec3();
const tempVec3c$s = math.vec3();
const tempVec3d$b = math.vec3();
const tempMat4a$o = math.mat4();

/**
 * @private
 */
class TrianglesSnapInitRenderer extends VBORenderer {

    constructor(scene) {
        super(scene, false, { instancing: true });
    }

    drawLayer(frameCtx, instancingLayer, renderPass) {

        if (!this._program) {
            this._allocate();
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram();
        }

        const model = instancingLayer.model;
        const scene = model.scene;
        const gl = scene.canvas.gl;
        const camera = scene.camera;
        const state = instancingLayer._state;
        const origin = instancingLayer._state.origin;
        const {position, rotationMatrix} = model;
        const aabb = instancingLayer.aabb; // Per-layer AABB for best RTC accuracy
        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;

        if (this._vaoCache.has(instancingLayer)) {
            gl.bindVertexArray(this._vaoCache.get(instancingLayer));
        } else {
            this._vaoCache.set(instancingLayer, this._makeVAO(state));
        }

        const coordinateScaler = tempVec3a$B;
        coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT;
        coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT;
        coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT;

        frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]);
        frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]);
        frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]);

        let rtcViewMatrix;
        let rtcCameraEye;
        if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) {
            const rtcOrigin = tempVec3b$x;
            if (origin) {
                const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3c$s);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$o);
            rtcCameraEye = tempVec3d$b;
            rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0];
            rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1];
            rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2];
            frameCtx.snapPickOrigin[0] = rtcOrigin[0];
            frameCtx.snapPickOrigin[1] = rtcOrigin[1];
            frameCtx.snapPickOrigin[2] = rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            rtcCameraEye = camera.eye;
            frameCtx.snapPickOrigin[0] = 0;
            frameCtx.snapPickOrigin[1] = 0;
            frameCtx.snapPickOrigin[2] = 0;
        }

        gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye);
        gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA);
        gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB);
        gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber);
        gl.uniform3fv(this._uCoordinateScaler, coordinateScaler);
        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible);

        let offset = 0;
        const mat4Size = 4 * 4;

        this._matricesUniformBlockBufferData.set(rotationMatrix, 0);
        this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size);

        gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer);
        gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW);

        gl.bindBufferBase(
            gl.UNIFORM_BUFFER,
            this._matricesUniformBlockBufferBindingPoint,
            this._matricesUniformBlockBuffer);


        {
            const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }

        this.setSectionPlanesStateUniforms(instancingLayer);

        state.indicesBuf.bind();
        gl.drawElementsInstanced(gl.TRIANGLES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0, state.numInstances);
        state.indicesBuf.unbind();

        gl.bindVertexArray(null);
    }

    _allocate() {
        super._allocate();

        const program = this._program;

        {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
        this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc");
        this.uVectorA = program.getLocation("snapVectorA");
        this.uInverseVectorAB = program.getLocation("snapInvVectorAB");
        this._uLayerNumber = program.getLocation("layerNumber");
        this._uCoordinateScaler = program.getLocation("coordinateScaler");
    }

    _bindProgram() {
        this._program.bind();

    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// SnapInstancingDepthBufInitRenderer vertex shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");
        src.push("uniform int renderPass;");
        src.push("in vec4 pickColor;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");
        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec3 uCameraEyeRtc;");
        src.push("uniform vec2 snapVectorA;");
        src.push("uniform vec2 snapInvVectorAB;");
        {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }
        src.push("vec2 remapClipPos(vec2 clipPos) {");
        src.push("    float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;");
        src.push("    float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;");
        src.push("    return vec2(x, y);");
        src.push("}");
        src.push("flat out vec4 vPickColor;");
        src.push("out vec4 vWorldPosition;");
        if (clipping) {
            src.push("out float vFlags;");
        }
        src.push("out highp vec3 relativeToOriginPosition;");
        src.push("void main(void) {");
        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK
        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("} else {");
        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("relativeToOriginPosition = worldPosition.xyz;");
        src.push("  vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("  vWorldPosition = worldPosition;");
        if (clipping) {
            src.push("  vFlags = flags;");
        }
        src.push("vPickColor = pickColor;");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        src.push("float tmp = clipPos.w;");
        src.push("clipPos.xyzw /= tmp;");
        src.push("clipPos.xy = remapClipPos(clipPos.xy);");
        src.push("clipPos.xyzw *= tmp;");
        {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// Points instancing pick depth fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("uniform int layerNumber;");
        src.push("uniform vec3 coordinateScaler;");
        src.push("in vec4 vWorldPosition;");
        src.push("flat in vec4 vPickColor;");
        if (clipping) {
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in highp vec3 relativeToOriginPosition;");
        src.push("layout(location = 0) out highp ivec4 outCoords;");
        src.push("layout(location = 1) out highp ivec4 outNormal;");
        src.push("layout(location = 2) out lowp uvec4 outPickColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        {
            src.push("    float dx = dFdx(vFragDepth);");
            src.push("    float dy = dFdy(vFragDepth);");
            src.push("    float diff = sqrt(dx*dx+dy*dy);");
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth + diff ) * logDepthBufFC * 0.5;");
        }
        src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, -layerNumber);");

        src.push("vec3 xTangent = dFdx( vWorldPosition.xyz );");
        src.push("vec3 yTangent = dFdy( vWorldPosition.xyz );");
        src.push("vec3 worldNormal = normalize( cross( xTangent, yTangent ) );");
        src.push(`outNormal = ivec4(worldNormal * float(${math.MAX_INT}), 1.0);`);
        src.push("outPickColor = uvec4(vPickColor);");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

const tempVec3a$A = math.vec3();
const tempVec3b$w = math.vec3();
const tempVec3c$r = math.vec3();
const tempVec3d$a = math.vec3();
const tempMat4a$n = math.mat4();

/**
 * @private
 */
class TrianglesSnapRenderer extends VBORenderer {

    constructor(scene) {
        super(scene, false, { instancing: true });
    }

    drawLayer(frameCtx, instancingLayer, renderPass) {

        if (!this._program) {
            this._allocate(instancingLayer);
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram();
        }

        const model = instancingLayer.model;
        const scene = model.scene;
        const camera = scene.camera;
        const gl = scene.canvas.gl;
        const state = instancingLayer._state;
        const origin = instancingLayer._state.origin;
        const {position, rotationMatrix} = model;
        const aabb = instancingLayer.aabb; // Per-layer AABB for best RTC accuracy
        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;

        if (this._vaoCache.has(instancingLayer)) {
            gl.bindVertexArray(this._vaoCache.get(instancingLayer));
        } else {
            this._vaoCache.set(instancingLayer, this._makeVAO(state));
        }

        const coordinateScaler = tempVec3a$A;
        coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT;
        coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT;
        coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT;

        frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]);
        frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]);
        frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]);

        let rtcViewMatrix;
        let rtcCameraEye;

        if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) {
            const rtcOrigin = tempVec3b$w;
            if (origin) {
                const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3c$r);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$n);
            rtcCameraEye = tempVec3d$a;
            rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0];
            rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1];
            rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2];
            frameCtx.snapPickOrigin[0] = rtcOrigin[0];
            frameCtx.snapPickOrigin[1] = rtcOrigin[1];
            frameCtx.snapPickOrigin[2] = rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            rtcCameraEye = camera.eye;
            frameCtx.snapPickOrigin[0] = 0;
            frameCtx.snapPickOrigin[1] = 0;
            frameCtx.snapPickOrigin[2] = 0;
        }

        gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye);
        gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA);
        gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB);
        gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber);
        gl.uniform3fv(this._uCoordinateScaler, coordinateScaler);
        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible);

        let offset = 0;
        const mat4Size = 4 * 4;

        this._matricesUniformBlockBufferData.set(rotationMatrix, 0);
        this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size);

        gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer);
        gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW);

        gl.bindBufferBase(
            gl.UNIFORM_BUFFER,
            this._matricesUniformBlockBufferBindingPoint,
            this._matricesUniformBlockBuffer);

        {
            const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }

        this.setSectionPlanesStateUniforms(instancingLayer);

        if (frameCtx.snapMode === "edge" ) {
            if (state.edgeIndicesBuf) {
                state.edgeIndicesBuf.bind();
                gl.drawElementsInstanced(gl.LINES, state.edgeIndicesBuf.numItems, state.edgeIndicesBuf.itemType, 0, state.numInstances);
                state.edgeIndicesBuf.unbind(); // needed?
            }
        } else {
            gl.drawArraysInstanced(gl.POINTS, 0, state.positionsBuf.numItems, state.numInstances);
        }

        gl.bindVertexArray(null);
    }

    _allocate() {
        super._allocate();

        const program = this._program;

        {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
        this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); 
        this.uVectorA = program.getLocation("snapVectorA"); 
        this.uInverseVectorAB = program.getLocation("snapInvVectorAB"); 
        this._uLayerNumber = program.getLocation("layerNumber"); 
        this._uCoordinateScaler = program.getLocation("coordinateScaler"); 
    }

    _bindProgram() {
        this._program.bind();

    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// SnapInstancingDepthRenderer vertex shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");
        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");
        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec3 uCameraEyeRtc;"); 
        src.push("uniform vec2 snapVectorA;"); 
        src.push("uniform vec2 snapInvVectorAB;"); 
        {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }
        src.push("vec2 remapClipPos(vec2 clipPos) {");
        src.push("    float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;");
        src.push("    float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;");
        src.push("    return vec2(x, y);");
        src.push("}");
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out highp vec3 relativeToOriginPosition;");
        src.push("void main(void) {");
        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK
        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("   gl_Position = vec4(2.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("} else {");
        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("relativeToOriginPosition = worldPosition.xyz;");
        src.push("  vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("  vWorldPosition = worldPosition;");
            src.push("  vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        src.push("float tmp = clipPos.w;");
        src.push("clipPos.xyzw /= tmp;");
        src.push("clipPos.xy = remapClipPos(clipPos.xy);");
        src.push("clipPos.xyzw *= tmp;");
        {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("gl_PointSize = 1.0;"); // Windows needs this?
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// SnapInstancingDepthRenderer fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("uniform int layerNumber;"); 
        src.push("uniform vec3 coordinateScaler;"); 
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in highp vec3 relativeToOriginPosition;");
        src.push("out highp ivec4 outCoords;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, layerNumber);");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

/**
 * @private
 */
class Renderers {

    constructor(scene) {
        this._scene = scene;
    }

    _compile() {
        if (this._colorRenderer && (!this._colorRenderer.getValid())) {
            this._colorRenderer.destroy();
            this._colorRenderer = null;
        }
        if (this._colorRendererWithSAO && (!this._colorRendererWithSAO.getValid())) {
            this._colorRendererWithSAO.destroy();
            this._colorRendererWithSAO = null;
        }
        if (this._flatColorRenderer && (!this._flatColorRenderer.getValid())) {
            this._flatColorRenderer.destroy();
            this._flatColorRenderer = null;
        }
        if (this._flatColorRendererWithSAO && (!this._flatColorRendererWithSAO.getValid())) {
            this._flatColorRendererWithSAO.destroy();
            this._flatColorRendererWithSAO = null;
        }
        if (this._colorTextureRenderer && (!this._colorTextureRenderer.getValid())) {
            this._colorTextureRenderer.destroy();
            this._colorTextureRenderer = null;
        }
        if (this._colorTextureRendererWithSAO && (!this._colorTextureRendererWithSAO.getValid())) {
            this._colorTextureRendererWithSAO.destroy();
            this._colorTextureRendererWithSAO = null;
        }
        if (this._colorTextureRendererAlphaCutoff && (!this._colorTextureRendererAlphaCutoff.getValid())) {
            this._colorTextureRendererAlphaCutoff.destroy();
            this._colorTextureRendererAlphaCutoff = null;
        }
        if (this._colorTextureRendererWithSAOAlphaCutoff && (!this._colorTextureRendererWithSAOAlphaCutoff.getValid())) {
            this._colorTextureRendererWithSAOAlphaCutoff.destroy();
            this._colorTextureRendererWithSAOAlphaCutoff = null;
        }
        if (this._pbrRenderer && (!this._pbrRenderer.getValid())) {
            this._pbrRenderer.destroy();
            this._pbrRenderer = null;
        }
        if (this._pbrRendererWithSAO && (!this._pbrRendererWithSAO.getValid())) {
            this._pbrRendererWithSAO.destroy();
            this._pbrRendererWithSAO = null;
        }
        if (this._depthRenderer && (!this._depthRenderer.getValid())) {
            this._depthRenderer.destroy();
            this._depthRenderer = null;
        }
        if (this._normalsRenderer && (!this._normalsRenderer.getValid())) {
            this._normalsRenderer.destroy();
            this._normalsRenderer = null;
        }
        if (this._silhouetteRenderer && (!this._silhouetteRenderer.getValid())) {
            this._silhouetteRenderer.destroy();
            this._silhouetteRenderer = null;
        }
        if (this._edgesRenderer && (!this._edgesRenderer.getValid())) {
            this._edgesRenderer.destroy();
            this._edgesRenderer = null;
        }
        if (this._edgesColorRenderer && (!this._edgesColorRenderer.getValid())) {
            this._edgesColorRenderer.destroy();
            this._edgesColorRenderer = null;
        }
        if (this._pickMeshRenderer && (!this._pickMeshRenderer.getValid())) {
            this._pickMeshRenderer.destroy();
            this._pickMeshRenderer = null;
        }
        if (this._pickDepthRenderer && (!this._pickDepthRenderer.getValid())) {
            this._pickDepthRenderer.destroy();
            this._pickDepthRenderer = null;
        }
        if (this._pickNormalsRenderer && this._pickNormalsRenderer.getValid() === false) {
            this._pickNormalsRenderer.destroy();
            this._pickNormalsRenderer = null;
        }
        if (this._pickNormalsFlatRenderer && this._pickNormalsFlatRenderer.getValid() === false) {
            this._pickNormalsFlatRenderer.destroy();
            this._pickNormalsFlatRenderer = null;
        }
        if (this._occlusionRenderer && this._occlusionRenderer.getValid() === false) {
            this._occlusionRenderer.destroy();
            this._occlusionRenderer = null;
        }
        if (this._shadowRenderer && (!this._shadowRenderer.getValid())) {
            this._shadowRenderer.destroy();
            this._shadowRenderer = null;
        }
        if (this._snapInitRenderer && (!this._snapInitRenderer.getValid())) {
            this._snapInitRenderer.destroy();
            this._snapInitRenderer = null;
        }
        if (this._snapRenderer && (!this._snapRenderer.getValid())) {
            this._snapRenderer.destroy();
            this._snapRenderer = null;
        }
    }

    eagerCreateRenders() {

        // Pre-initialize certain renderers that would otherwise be lazy-initialised
        // on user interaction, such as picking or emphasis, so that there is no delay
        // when user first begins interacting with the viewer.

        if (!this._silhouetteRenderer) { // Used for highlighting and selection
            this._silhouetteRenderer = new TrianglesSilhouetteRenderer(this._scene);
        }
        if (!this._pickMeshRenderer) {
            this._pickMeshRenderer = new TrianglesPickMeshRenderer(this._scene);
        }
        if (!this._pickDepthRenderer) {
            this._pickDepthRenderer = new TrianglesPickDepthRenderer(this._scene);
        }
        if (!this._snapInitRenderer) {
            this._snapInitRenderer = new TrianglesSnapInitRenderer(this._scene, false);
        }
        if (!this._snapRenderer) {
            this._snapRenderer = new TrianglesSnapRenderer(this._scene);
        }
    }

    get colorRenderer() {
        if (!this._colorRenderer) {
            this._colorRenderer = new TrianglesColorRenderer(this._scene, false);
        }
        return this._colorRenderer;
    }

    get colorRendererWithSAO() {
        if (!this._colorRendererWithSAO) {
            this._colorRendererWithSAO = new TrianglesColorRenderer(this._scene, true);
        }
        return this._colorRendererWithSAO;
    }

    get flatColorRenderer() {
        if (!this._flatColorRenderer) {
            this._flatColorRenderer = new TrianglesFlatColorRenderer(this._scene, false);
        }
        return this._flatColorRenderer;
    }

    get flatColorRendererWithSAO() {
        if (!this._flatColorRendererWithSAO) {
            this._flatColorRendererWithSAO = new TrianglesFlatColorRenderer(this._scene, true);
        }
        return this._flatColorRendererWithSAO;
    }

    get colorTextureRenderer() {
        if (!this._colorTextureRenderer) {
            this._colorTextureRenderer = new TrianglesColorTextureRenderer(this._scene, false);
        }
        return this._colorTextureRenderer;
    }

    get colorTextureRendererWithSAO() {
        if (!this._colorTextureRendererWithSAO) {
            this._colorTextureRendererWithSAO = new TrianglesColorTextureRenderer(this._scene, true);
        }
        return this._colorTextureRendererWithSAO;
    }

    get colorTextureRendererAlphaCutoff() {
        if (!this._colorTextureRendererAlphaCutoff) {
            this._colorTextureRendererAlphaCutoff = new TrianglesColorTextureRenderer(this._scene, false, { useAlphaCutoff: true });
        }
        return this._colorTextureRendererAlphaCutoff;
    }

    get colorTextureRendererWithSAOAlphaCutoff() {
        if (!this._colorTextureRendererWithSAOAlphaCutoff) {
            this._colorTextureRendererWithSAOAlphaCutoff = new TrianglesColorTextureRenderer(this._scene, true, { useAlphaCutoff: true });
        }
        return this._colorTextureRendererWithSAOAlphaCutoff;
    }

    get pbrRenderer() {
        if (!this._pbrRenderer) {
            this._pbrRenderer = new TrianglesPBRRenderer(this._scene, false);
        }
        return this._pbrRenderer;
    }

    get pbrRendererWithSAO() {
        if (!this._pbrRendererWithSAO) {
            this._pbrRendererWithSAO = new TrianglesPBRRenderer(this._scene, true);
        }
        return this._pbrRendererWithSAO;
    }

    get silhouetteRenderer() {
        if (!this._silhouetteRenderer) {
            this._silhouetteRenderer = new TrianglesSilhouetteRenderer(this._scene);
        }
        return this._silhouetteRenderer;
    }

    get depthRenderer() {
        if (!this._depthRenderer) {
            this._depthRenderer = new TrianglesDepthRenderer(this._scene);
        }
        return this._depthRenderer;
    }

    get normalsRenderer() {
        if (!this._normalsRenderer) {
            this._normalsRenderer = new TrianglesNormalsRenderer(this._scene);
        }
        return this._normalsRenderer;
    }

    get edgesRenderer() {
        if (!this._edgesRenderer) {
            this._edgesRenderer = new EdgesEmphasisRenderer(this._scene);
        }
        return this._edgesRenderer;
    }

    get edgesColorRenderer() {
        if (!this._edgesColorRenderer) {
            this._edgesColorRenderer = new EdgesColorRenderer(this._scene);
        }
        return this._edgesColorRenderer;
    }

    get pickMeshRenderer() {
        if (!this._pickMeshRenderer) {
            this._pickMeshRenderer = new TrianglesPickMeshRenderer(this._scene);
        }
        return this._pickMeshRenderer;
    }

    get pickNormalsRenderer() {
        if (!this._pickNormalsRenderer) {
            this._pickNormalsRenderer = new TrianglesPickNormalsRenderer(this._scene);
        }
        return this._pickNormalsRenderer;
    }

    get pickNormalsFlatRenderer() {
        if (!this._pickNormalsFlatRenderer) {
            this._pickNormalsFlatRenderer = new TrianglesPickNormalsFlatRenderer(this._scene);
        }
        return this._pickNormalsFlatRenderer;
    }

    get pickDepthRenderer() {
        if (!this._pickDepthRenderer) {
            this._pickDepthRenderer = new TrianglesPickDepthRenderer(this._scene);
        }
        return this._pickDepthRenderer;
    }

    get occlusionRenderer() {
        if (!this._occlusionRenderer) {
            this._occlusionRenderer = new TrianglesOcclusionRenderer(this._scene);
        }
        return this._occlusionRenderer;
    }

    get shadowRenderer() {
        if (!this._shadowRenderer) {
            this._shadowRenderer = new TrianglesShadowRenderer(this._scene);
        }
        return this._shadowRenderer;
    }

    get snapRenderer() {
        if (!this._snapRenderer) {
            this._snapRenderer = new TrianglesSnapRenderer(this._scene);
        }
        return this._snapRenderer;
    }

    get snapInitRenderer() {
        if (!this._snapInitRenderer) {
            this._snapInitRenderer = new TrianglesSnapInitRenderer(this._scene);
        }
        return this._snapInitRenderer;
    }

    _destroy() {
        if (this._colorRenderer) {
            this._colorRenderer.destroy();
        }
        if (this._colorRendererWithSAO) {
            this._colorRendererWithSAO.destroy();
        }
        if (this._flatColorRenderer) {
            this._flatColorRenderer.destroy();
        }
        if (this._flatColorRendererWithSAO) {
            this._flatColorRendererWithSAO.destroy();
        }
        if (this._colorTextureRenderer) {
            this._colorTextureRenderer.destroy();
        }
        if (this._colorTextureRendererWithSAO) {
            this._colorTextureRendererWithSAO.destroy();
        }
        if (this._colorTextureRendererAlphaCutoff) {
            this._colorTextureRendererAlphaCutoff.destroy();
        }
        if (this._colorTextureRendererWithSAOAlphaCutoff) {
            this._colorTextureRendererWithSAOAlphaCutoff.destroy();
        }
        if (this._pbrRenderer) {
            this._pbrRenderer.destroy();
        }
        if (this._pbrRendererWithSAO) {
            this._pbrRendererWithSAO.destroy();
        }
        if (this._depthRenderer) {
            this._depthRenderer.destroy();
        }
        if (this._normalsRenderer) {
            this._normalsRenderer.destroy();
        }
        if (this._silhouetteRenderer) {
            this._silhouetteRenderer.destroy();
        }
        if (this._edgesRenderer) {
            this._edgesRenderer.destroy();
        }
        if (this._edgesColorRenderer) {
            this._edgesColorRenderer.destroy();
        }
        if (this._pickMeshRenderer) {
            this._pickMeshRenderer.destroy();
        }
        if (this._pickDepthRenderer) {
            this._pickDepthRenderer.destroy();
        }
        if (this._pickNormalsRenderer) {
            this._pickNormalsRenderer.destroy();
        }
        if (this._pickNormalsFlatRenderer) {
            this._pickNormalsFlatRenderer.destroy();
        }
        if (this._occlusionRenderer) {
            this._occlusionRenderer.destroy();
        }
        if (this._shadowRenderer) {
            this._shadowRenderer.destroy();
        }
        if (this._snapInitRenderer) {
            this._snapInitRenderer.destroy();
        }
        if (this._snapRenderer) {
            this._snapRenderer.destroy();
        }
    }
}

const cachedRenderers$5 = {};

/**
 * @private
 */
function getRenderers$6(scene) {
    const sceneId = scene.id;
    let renderers = cachedRenderers$5[sceneId];
    if (!renderers) {
        renderers = new Renderers(scene);
        cachedRenderers$5[sceneId] = renderers;
        renderers._compile();
        renderers.eagerCreateRenders();
        scene.on("compile", () => {
            renderers._compile();
            renderers.eagerCreateRenders();
        });
        scene.on("destroyed", () => {
            delete cachedRenderers$5[sceneId];
            renderers._destroy();
        });
    }
    return renderers;
}

const tempUint8Vec4$2 = new Uint8Array(4);
const tempFloat32$2 = new Float32Array(1);
const tempVec4a$c = math.vec4([0, 0, 0, 1]);
const tempVec3fa$2 = new Float32Array(3);

const tempVec3a$z = math.vec3();
const tempVec3b$v = math.vec3();
const tempVec3c$q = math.vec3();
const tempVec3d$9 = math.vec3();
const tempVec3e$1 = math.vec3();
const tempVec3f$1 = math.vec3();
const tempVec3g = math.vec3();

const tempFloat32Vec4$2 = new Float32Array(4);

/**
 * @private
 */
class VBOInstancingTrianglesLayer {

    /**
     * @param cfg
     * @param cfg.layerIndex
     * @param cfg.model
     * @param cfg.geometry
     * @param cfg.textureSet
     * @param cfg.origin
     */
    constructor(cfg) {

        //   console.info("Creating VBOInstancingTrianglesLayer");

        /**
         * Owner model
         * @type {VBOSceneModel}
         */
        this.model = cfg.model;

        /**
         * State sorting key.
         * @type {string}
         */
        this.sortId = "TrianglesInstancingLayer" + (cfg.solid ? "-solid" : "-surface") + (cfg.normals ? "-normals" : "-autoNormals");

        /**
         * Index of this InstancingLayer in VBOSceneModel#_layerList
         * @type {Number}
         */
        this.layerIndex = cfg.layerIndex;

        this._renderers = getRenderers$6(cfg.model.scene);

        this._aabb = math.collapseAABB3();

        this._state = new RenderState({
            numInstances: 0,
            obb: math.OBB3(),
            origin: math.vec3(),
            geometry: cfg.geometry,
            textureSet: cfg.textureSet,
            pbrSupported: false, // Set in #finalize if we have enough to support quality rendering
            positionsDecodeMatrix: cfg.geometry.positionsDecodeMatrix, // So we can null the geometry for GC
            colorsBuf: null,
            metallicRoughnessBuf: null,
            flagsBuf: null,
            offsetsBuf: null,
            modelMatrixBuf: null,
            modelMatrixCol0Buf: null,
            modelMatrixCol1Buf: null,
            modelMatrixCol2Buf: null,
            modelNormalMatrixCol0Buf: null,
            modelNormalMatrixCol1Buf: null,
            modelNormalMatrixCol2Buf: null,
            pickColorsBuf: null
        });

        // These counts are used to avoid unnecessary render passes
        this._numPortions = 0;
        this._numVisibleLayerPortions = 0;
        this._numTransparentLayerPortions = 0;
        this._numXRayedLayerPortions = 0;
        this._numHighlightedLayerPortions = 0;
        this._numSelectedLayerPortions = 0;
        this._numClippableLayerPortions = 0;
        this._numEdgesLayerPortions = 0;
        this._numPickableLayerPortions = 0;
        this._numCulledLayerPortions = 0;

        /** @private */
        this.numIndices = cfg.geometry.numIndices;

        // Vertex arrays
        this._colors = [];
        this._metallicRoughness = [];
        this._pickColors = [];
        this._offsets = [];

        // Modeling matrix per instance, array for each column

        this._modelMatrix = [];

        this._modelMatrixCol0 = [];
        this._modelMatrixCol1 = [];
        this._modelMatrixCol2 = [];

        // Modeling normal matrix per instance, array for each column
        this._modelNormalMatrixCol0 = [];
        this._modelNormalMatrixCol1 = [];
        this._modelNormalMatrixCol2 = [];

        this._portions = [];
        this._meshes = [];

        this._aabb = math.collapseAABB3();
        this.aabbDirty = true;

        if (cfg.origin) {
            this._state.origin.set(cfg.origin);
        }

        this._finalized = false;

        /**
         * When true, this layer contains solid triangle meshes, otherwise this layer contains surface triangle meshes
         * @type {boolean}
         */
        this.solid = !!cfg.solid;

        /**
         * The number of indices in this layer.
         * @type {number|*}
         */
        this.numIndices = cfg.geometry.numIndices;

        /**
         * The type of primitives in this layer.
         */
        this.primitive = cfg.geometry.primitive;
    }

    get aabb() {
        if (this.aabbDirty) {
            math.collapseAABB3(this._aabb);
            for (let i = 0, len = this._meshes.length; i < len; i++) {
                math.expandAABB3(this._aabb, this._meshes[i].aabb);
            }
            this.aabbDirty = false;
        }
        return this._aabb;
    }

    /**
     * Creates a new portion within this InstancingLayer, returns the new portion ID.
     *
     * The portion will instance this InstancingLayer's geometry.
     *
     * Gives the portion the specified color and matrix.
     *
     * @param mesh The SceneModelMesh that owns the portion
     * @param cfg Portion params
     * @param cfg.color Color [0..255,0..255,0..255]
     * @param cfg.metallic Metalness factor [0..255]
     * @param cfg.roughness Roughness factor [0..255]
     * @param cfg.opacity Opacity [0..255].
     * @param cfg.meshMatrix Flat float 4x4 matrix.
     * @param [cfg.worldMatrix] Flat float 4x4 matrix.
     * @param cfg.pickColor Quantized pick color
     * @returns {number} Portion ID.
     */
    createPortion(mesh, cfg) {

        const color = cfg.color;
        const metallic = cfg.metallic;
        const roughness = cfg.roughness;
        const opacity = cfg.opacity !== null && cfg.opacity !== undefined ? cfg.opacity : 255;
        const meshMatrix = cfg.meshMatrix;
        const pickColor = cfg.pickColor;

        if (this._finalized) {
            throw "Already finalized";
        }

        const r = color[0]; // Color is pre-quantized by SceneModel
        const g = color[1];
        const b = color[2];

        this._colors.push(r);
        this._colors.push(g);
        this._colors.push(b);
        this._colors.push(opacity);

        this._metallicRoughness.push((metallic !== null && metallic !== undefined) ? metallic : 0);
        this._metallicRoughness.push((roughness !== null && roughness !== undefined) ? roughness : 255);

        if (this.model.scene.entityOffsetsEnabled) {
            this._offsets.push(0);
            this._offsets.push(0);
            this._offsets.push(0);
        }

        this._modelMatrixCol0.push(meshMatrix[0]);
        this._modelMatrixCol0.push(meshMatrix[4]);
        this._modelMatrixCol0.push(meshMatrix[8]);
        this._modelMatrixCol0.push(meshMatrix[12]);

        this._modelMatrixCol1.push(meshMatrix[1]);
        this._modelMatrixCol1.push(meshMatrix[5]);
        this._modelMatrixCol1.push(meshMatrix[9]);
        this._modelMatrixCol1.push(meshMatrix[13]);

        this._modelMatrixCol2.push(meshMatrix[2]);
        this._modelMatrixCol2.push(meshMatrix[6]);
        this._modelMatrixCol2.push(meshMatrix[10]);
        this._modelMatrixCol2.push(meshMatrix[14]);

        if (this._state.geometry.normals) {

            // Note: order of inverse and transpose doesn't matter

            let transposedMat = math.transposeMat4(meshMatrix, math.mat4()); // TODO: Use cached matrix
            let normalMatrix = math.inverseMat4(transposedMat);

            this._modelNormalMatrixCol0.push(normalMatrix[0]);
            this._modelNormalMatrixCol0.push(normalMatrix[4]);
            this._modelNormalMatrixCol0.push(normalMatrix[8]);
            this._modelNormalMatrixCol0.push(normalMatrix[12]);

            this._modelNormalMatrixCol1.push(normalMatrix[1]);
            this._modelNormalMatrixCol1.push(normalMatrix[5]);
            this._modelNormalMatrixCol1.push(normalMatrix[9]);
            this._modelNormalMatrixCol1.push(normalMatrix[13]);

            this._modelNormalMatrixCol2.push(normalMatrix[2]);
            this._modelNormalMatrixCol2.push(normalMatrix[6]);
            this._modelNormalMatrixCol2.push(normalMatrix[10]);
            this._modelNormalMatrixCol2.push(normalMatrix[14]);
        }

        // Per-vertex pick colors

        this._pickColors.push(pickColor[0]);
        this._pickColors.push(pickColor[1]);
        this._pickColors.push(pickColor[2]);
        this._pickColors.push(pickColor[3]);

        this._state.numInstances++;

        const portionId = this._portions.length;

        const portion = {};

        if (this.model.scene.readableGeometryEnabled) {
            portion.matrix = meshMatrix.slice();
            portion.inverseMatrix = null; // Lazy-computed in precisionRayPickSurface
            portion.normalMatrix = null; // Lazy-computed in precisionRayPickSurface
        }

        this._portions.push(portion);

        this._numPortions++;
        this.model.numPortions++;
        this._meshes.push(mesh);
        return portionId;
    }

    finalize() {

        if (this._finalized) {
            return;
        }

        const state = this._state;
        const geometry = state.geometry;
        const textureSet = state.textureSet;
        const gl = this.model.scene.canvas.gl;
        const colorsLength = this._colors.length;
        const flagsLength = colorsLength / 4;

        if (colorsLength > 0) {
            let notNormalized = false;
            state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Uint8Array(this._colors), this._colors.length, 4, gl.DYNAMIC_DRAW, notNormalized);
            this._colors = []; // Release memory
        }

        if (this._metallicRoughness.length > 0) {
            const metallicRoughness = new Uint8Array(this._metallicRoughness);
            let normalized = false;
            state.metallicRoughnessBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, metallicRoughness, this._metallicRoughness.length, 2, gl.STATIC_DRAW, normalized);
        }

        if (flagsLength > 0) {
            // Because we only build flags arrays here,
            // get their length from the colors array
            let notNormalized = false;
            state.flagsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(flagsLength), flagsLength, 1, gl.DYNAMIC_DRAW, notNormalized);
        }

        if (this.model.scene.entityOffsetsEnabled) {
            if (this._offsets.length > 0) {
                const notNormalized = false;
                state.offsetsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._offsets), this._offsets.length, 3, gl.DYNAMIC_DRAW, notNormalized);
                this._offsets = []; // Release memory
            }
        }

        if (geometry.positionsCompressed && geometry.positionsCompressed.length > 0) {
            const normalized = false;
            state.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, geometry.positionsCompressed, geometry.positionsCompressed.length, 3, gl.STATIC_DRAW, normalized);
            state.positionsDecodeMatrix = math.mat4(geometry.positionsDecodeMatrix);
        }
        // if (geometry.normalsCompressed && geometry.normalsCompressed.length > 0) {
        //     const normalized = true; // For oct-encoded UInt8
        //     state.normalsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, geometry.normalsCompressed, geometry.normalsCompressed.length, 3, gl.STATIC_DRAW, normalized);
        // }
        if (geometry.colorsCompressed && geometry.colorsCompressed.length > 0) {
            const colorsCompressed = new Uint8Array(geometry.colorsCompressed);
            const notNormalized = false;
            state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, colorsCompressed, colorsCompressed.length, 4, gl.STATIC_DRAW, notNormalized);
        }
        if (geometry.uvCompressed && geometry.uvCompressed.length > 0) {
            const uvCompressed = geometry.uvCompressed;
            state.uvDecodeMatrix = geometry.uvDecodeMatrix;
            state.uvBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, uvCompressed, uvCompressed.length, 2, gl.STATIC_DRAW, false);
        }
        if (geometry.indices && geometry.indices.length > 0) {
            state.indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(geometry.indices), geometry.indices.length, 1, gl.STATIC_DRAW);
            state.numIndices = geometry.indices.length;
        }
        if (geometry.edgeIndices && geometry.edgeIndices.length > 0) {
            state.edgeIndicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(geometry.edgeIndices), geometry.edgeIndices.length, 1, gl.STATIC_DRAW);
        }

        if (this._modelMatrixCol0.length > 0) {

            const normalized = false;

            state.modelMatrixCol0Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelMatrixCol0), this._modelMatrixCol0.length, 4, gl.STATIC_DRAW, normalized);
            state.modelMatrixCol1Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelMatrixCol1), this._modelMatrixCol1.length, 4, gl.STATIC_DRAW, normalized);
            state.modelMatrixCol2Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelMatrixCol2), this._modelMatrixCol2.length, 4, gl.STATIC_DRAW, normalized);
            this._modelMatrixCol0 = [];
            this._modelMatrixCol1 = [];
            this._modelMatrixCol2 = [];

            if (state.normalsBuf) {
                state.modelNormalMatrixCol0Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelNormalMatrixCol0), this._modelNormalMatrixCol0.length, 4, gl.STATIC_DRAW, normalized);
                state.modelNormalMatrixCol1Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelNormalMatrixCol1), this._modelNormalMatrixCol1.length, 4, gl.STATIC_DRAW, normalized);
                state.modelNormalMatrixCol2Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelNormalMatrixCol2), this._modelNormalMatrixCol2.length, 4, gl.STATIC_DRAW, normalized);
                this._modelNormalMatrixCol0 = [];
                this._modelNormalMatrixCol1 = [];
                this._modelNormalMatrixCol2 = [];
            }
        }

        if (this._pickColors.length > 0) {
            const normalized = false;
            state.pickColorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Uint8Array(this._pickColors), this._pickColors.length, 4, gl.STATIC_DRAW, normalized);
            this._pickColors = []; // Release memory
        }

        state.pbrSupported
            = !!state.metallicRoughnessBuf
            && !!state.uvBuf
            && !!state.normalsBuf
            && !!textureSet
            && !!textureSet.colorTexture
            && !!textureSet.metallicRoughnessTexture;

        state.colorTextureSupported
            = !!state.uvBuf
            && !!textureSet
            && !!textureSet.colorTexture;


        if (!this.model.scene.readableGeometryEnabled) {
            this._state.geometry = null;
        }

        this._finalized = true;
    }

    // The following setters are called by VBOSceneModelMesh, in turn called by VBOSceneModelNode, only after the layer is finalized.
    // It's important that these are called after finalize() in order to maintain integrity of counts like _numVisibleLayerPortions etc.

    initFlags(portionId, flags, meshTransparent) {
        if (flags & ENTITY_FLAGS.VISIBLE) {
            this._numVisibleLayerPortions++;
            this.model.numVisibleLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.HIGHLIGHTED) {
            this._numHighlightedLayerPortions++;
            this.model.numHighlightedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.XRAYED) {
            this._numXRayedLayerPortions++;
            this.model.numXRayedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.SELECTED) {
            this._numSelectedLayerPortions++;
            this.model.numSelectedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.CLIPPABLE) {
            this._numClippableLayerPortions++;
            this.model.numClippableLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.EDGES) {
            this._numEdgesLayerPortions++;
            this.model.numEdgesLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.PICKABLE) {
            this._numPickableLayerPortions++;
            this.model.numPickableLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.CULLED) {
            this._numCulledLayerPortions++;
            this.model.numCulledLayerPortions++;
        }
        if (meshTransparent) {
            this._numTransparentLayerPortions++;
            this.model.numTransparentLayerPortions++;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setVisible(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.VISIBLE) {
            this._numVisibleLayerPortions++;
            this.model.numVisibleLayerPortions++;
        } else {
            this._numVisibleLayerPortions--;
            this.model.numVisibleLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setHighlighted(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.HIGHLIGHTED) {
            this._numHighlightedLayerPortions++;
            this.model.numHighlightedLayerPortions++;
        } else {
            this._numHighlightedLayerPortions--;
            this.model.numHighlightedLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setXRayed(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.XRAYED) {
            this._numXRayedLayerPortions++;
            this.model.numXRayedLayerPortions++;
        } else {
            this._numXRayedLayerPortions--;
            this.model.numXRayedLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setSelected(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.SELECTED) {
            this._numSelectedLayerPortions++;
            this.model.numSelectedLayerPortions++;
        } else {
            this._numSelectedLayerPortions--;
            this.model.numSelectedLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setEdges(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.EDGES) {
            this._numEdgesLayerPortions++;
            this.model.numEdgesLayerPortions++;
        } else {
            this._numEdgesLayerPortions--;
            this.model.numEdgesLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setClippable(portionId, flags) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.CLIPPABLE) {
            this._numClippableLayerPortions++;
            this.model.numClippableLayerPortions++;
        } else {
            this._numClippableLayerPortions--;
            this.model.numClippableLayerPortions--;
        }
        this._setFlags(portionId, flags);
    }

    setCollidable(portionId, flags) {
        if (!this._finalized) {
            throw "Not finalized";
        }
    }

    setPickable(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.PICKABLE) {
            this._numPickableLayerPortions++;
            this.model.numPickableLayerPortions++;
        } else {
            this._numPickableLayerPortions--;
            this.model.numPickableLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setCulled(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.CULLED) {
            this._numCulledLayerPortions++;
            this.model.numCulledLayerPortions++;
        } else {
            this._numCulledLayerPortions--;
            this.model.numCulledLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setColor(portionId, color) { // RGBA color is normalized as ints
        if (!this._finalized) {
            throw "Not finalized";
        }
        tempUint8Vec4$2[0] = color[0];
        tempUint8Vec4$2[1] = color[1];
        tempUint8Vec4$2[2] = color[2];
        tempUint8Vec4$2[3] = color[3];
        if (this._state.colorsBuf) {
            this._state.colorsBuf.setData(tempUint8Vec4$2, portionId * 4);
        }
    }

    setTransparent(portionId, flags, transparent) {
        if (transparent) {
            this._numTransparentLayerPortions++;
            this.model.numTransparentLayerPortions++;
        } else {
            this._numTransparentLayerPortions--;
            this.model.numTransparentLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    // setMatrix(portionId, matrix) {
    //
    //     if (!this._finalized) {
    //         throw "Not finalized";
    //     }
    //
    //     var offset = portionId * 4;
    //
    //     tempFloat32Vec4[0] = matrix[0];
    //     tempFloat32Vec4[1] = matrix[4];
    //     tempFloat32Vec4[2] = matrix[8];
    //     tempFloat32Vec4[3] = matrix[12];
    //
    //     this._state.modelMatrixCol0Buf.setData(tempFloat32Vec4, offset);
    //
    //     tempFloat32Vec4[0] = matrix[1];
    //     tempFloat32Vec4[1] = matrix[5];
    //     tempFloat32Vec4[2] = matrix[9];
    //     tempFloat32Vec4[3] = matrix[13];
    //
    //     this._state.modelMatrixCol1Buf.setData(tempFloat32Vec4, offset);
    //
    //     tempFloat32Vec4[0] = matrix[2];
    //     tempFloat32Vec4[1] = matrix[6];
    //     tempFloat32Vec4[2] = matrix[10];
    //     tempFloat32Vec4[3] = matrix[14];
    //
    //     this._state.modelMatrixCol2Buf.setData(tempFloat32Vec4, offset);
    // }

    /**
     * flags are 4bits values encoded on a 32bit base. color flag on the first 4 bits, silhouette flag on the next 4 bits and so on for edge, pick and clippable.
     */
    _setFlags(portionId, flags, meshTransparent) {

        if (!this._finalized) {
            throw "Not finalized";
        }

        const visible = !!(flags & ENTITY_FLAGS.VISIBLE);
        const xrayed = !!(flags & ENTITY_FLAGS.XRAYED);
        const highlighted = !!(flags & ENTITY_FLAGS.HIGHLIGHTED);
        const selected = !!(flags & ENTITY_FLAGS.SELECTED);
        const edges = !!(flags & ENTITY_FLAGS.EDGES);
        const pickable = !!(flags & ENTITY_FLAGS.PICKABLE);
        const culled = !!(flags & ENTITY_FLAGS.CULLED);

        let colorFlag;
        if (!visible || culled || xrayed
            || (highlighted && !this.model.scene.highlightMaterial.glowThrough)
            || (selected && !this.model.scene.selectedMaterial.glowThrough)) {
            colorFlag = RENDER_PASSES.NOT_RENDERED;
        } else {
            if (meshTransparent) {
                colorFlag = RENDER_PASSES.COLOR_TRANSPARENT;
            } else {
                colorFlag = RENDER_PASSES.COLOR_OPAQUE;
            }
        }

        let silhouetteFlag;
        if (!visible || culled) {
            silhouetteFlag = RENDER_PASSES.NOT_RENDERED;
        } else if (selected) {
            silhouetteFlag = RENDER_PASSES.SILHOUETTE_SELECTED;
        } else if (highlighted) {
            silhouetteFlag = RENDER_PASSES.SILHOUETTE_HIGHLIGHTED;
        } else if (xrayed) {
            silhouetteFlag = RENDER_PASSES.SILHOUETTE_XRAYED;
        } else {
            silhouetteFlag = RENDER_PASSES.NOT_RENDERED;
        }

        let edgeFlag = 0;
        if (!visible || culled) {
            edgeFlag = RENDER_PASSES.NOT_RENDERED;
        } else if (selected) {
            edgeFlag = RENDER_PASSES.EDGES_SELECTED;
        } else if (highlighted) {
            edgeFlag = RENDER_PASSES.EDGES_HIGHLIGHTED;
        } else if (xrayed) {
            edgeFlag = RENDER_PASSES.EDGES_XRAYED;
        } else if (edges) {
            if (meshTransparent) {
                edgeFlag = RENDER_PASSES.EDGES_COLOR_TRANSPARENT;
            } else {
                edgeFlag = RENDER_PASSES.EDGES_COLOR_OPAQUE;
            }
        } else {
            edgeFlag = RENDER_PASSES.NOT_RENDERED;
        }

        const pickFlag = (visible && !culled && pickable) ? RENDER_PASSES.PICK : RENDER_PASSES.NOT_RENDERED;

        const clippableFlag = !!(flags & ENTITY_FLAGS.CLIPPABLE) ? 1 : 0;

        let vertFlag = 0;
        vertFlag |= colorFlag;
        vertFlag |= silhouetteFlag << 4;
        vertFlag |= edgeFlag << 8;
        vertFlag |= pickFlag << 12;
        vertFlag |= clippableFlag << 16;

        tempFloat32$2[0] = vertFlag;

        if (this._state.flagsBuf) {
            this._state.flagsBuf.setData(tempFloat32$2, portionId);
        }
    }

    setOffset(portionId, offset) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (!this.model.scene.entityOffsetsEnabled) {
            this.model.error("Entity#offset not enabled for this Viewer"); // See Viewer entityOffsetsEnabled
            return;
        }
        tempVec3fa$2[0] = offset[0];
        tempVec3fa$2[1] = offset[1];
        tempVec3fa$2[2] = offset[2];
        if (this._state.offsetsBuf) {
            this._state.offsetsBuf.setData(tempVec3fa$2, portionId * 3);
        }
    }

    getEachVertex(portionId, callback) {
        if (!this.model.scene.readableGeometryEnabled) {
            return false;
        }
        const state = this._state;
        const geometry = state.geometry;
        const portion = this._portions[portionId];
        if (!portion) {
            this.model.error("portion not found: " + portionId);
            return;
        }
        const positions = geometry.positionsCompressed;
        const sceneModelMatrix = this.model.matrix;
        const origin = math.vec4();
        origin.set(state.origin, 0);
        origin[3] = 1;
        math.mulMat4v4(sceneModelMatrix, origin, origin);
        const offsetX = origin[0];
        const offsetY = origin[1];
        const offsetZ = origin[2];
        const worldPos = tempVec4a$c;
        const portionMatrix = portion.matrix;
        const positionsDecodeMatrix = state.positionsDecodeMatrix;
        for (let i = 0, len = positions.length; i < len; i += 3) {
            worldPos[0] = positions[i];
            worldPos[1] = positions[i + 1];
            worldPos[2] = positions[i + 2];
            math.decompressPosition(worldPos, positionsDecodeMatrix);
            math.transformPoint3(portionMatrix, worldPos, worldPos);
            worldPos[3] = 1;
            math.mulMat4v4(sceneModelMatrix, worldPos, worldPos);
            worldPos[0] += offsetX;
            worldPos[1] += offsetY;
            worldPos[2] += offsetZ;
            callback(worldPos);
        }
    }

    getEachIndex(portionId, callback) {
        if (!this.model.scene.readableGeometryEnabled) {
            return false;
        }
        const state = this._state;
        const geometry = state.geometry;
        const portion = this._portions[portionId];
        if (!portion) {
            this.model.error("portion not found: " + portionId);
            return;
        }
        const indices = geometry.indices;
        for (let i = 0, len = indices.length; i < len; i++) {
            callback(indices[i]);
        }
    }

    setMatrix(portionId, matrix) {
        if (!this._finalized) {
            throw "Not finalized";
        }

        ////////////////////////////////////////
        // TODO: Update portion matrix
        ////////////////////////////////////////

        var offset = portionId * 4;

        tempFloat32Vec4$2[0] = matrix[0];
        tempFloat32Vec4$2[1] = matrix[4];
        tempFloat32Vec4$2[2] = matrix[8];
        tempFloat32Vec4$2[3] = matrix[12];

        this._state.modelMatrixCol0Buf.setData(tempFloat32Vec4$2, offset);

        tempFloat32Vec4$2[0] = matrix[1];
        tempFloat32Vec4$2[1] = matrix[5];
        tempFloat32Vec4$2[2] = matrix[9];
        tempFloat32Vec4$2[3] = matrix[13];

        this._state.modelMatrixCol1Buf.setData(tempFloat32Vec4$2, offset);

        tempFloat32Vec4$2[0] = matrix[2];
        tempFloat32Vec4$2[1] = matrix[6];
        tempFloat32Vec4$2[2] = matrix[10];
        tempFloat32Vec4$2[3] = matrix[14];

        this._state.modelMatrixCol2Buf.setData(tempFloat32Vec4$2, offset);
    }

    readGeometryData(portionId) {
        if (!this._finalized) {
            throw "Not finalized";
        }

        const state = this._state;
        const sceneModelMatrix = this.model.matrix;

        const col0 = state.modelMatrixCol0Buf.getData(portionId, 1);
        const col1 = state.modelMatrixCol1Buf.getData(portionId, 1);
        const col2 = state.modelMatrixCol2Buf.getData(portionId, 1);

        const portionMatrix = [
            col0[0], col1[0], col2[0], 0,
            col0[1], col1[1], col2[1], 0,
            col0[2], col1[2], col2[2], 0,
            col0[3], col1[3], col2[3], 1,
        ];

        const positionsDecodeMatrix = state.positionsDecodeMatrix;

        const origin = math.vec4();
        origin.set(state.origin, 0);
        origin[3] = 1;
        math.mulMat4v4(sceneModelMatrix, origin, origin);

        const indices = state.indicesBuf.getData();

        const matrix = math.mulMat4(
            portionMatrix,
            positionsDecodeMatrix,
            new Array(16)
        );

        math.mulMat4(
            sceneModelMatrix,
            matrix,
            matrix
        );

        matrix[12] += origin[0];
        matrix[13] += origin[1];
        matrix[14] += origin[2];

        const positionsQuantized = state.positionsBuf.getData();

        const positions = math.transformPositions3(
            matrix,
            positionsQuantized,
            new Array(positionsQuantized.length)
        );

        // const aabb = math.positions3ToAABB3(positions);
        // console.log({aabbToniInstancing: aabb});

        return { indices, positions };
    }

    // ---------------------- COLOR RENDERING -----------------------------------

    drawColorOpaque(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        const useAlphaCutoff = this._state.textureSet && (typeof (this._state.textureSet.alphaCutoff) === "number");
        if (frameCtx.withSAO && this.model.saoEnabled) {
            if (frameCtx.pbrEnabled && this.model.pbrEnabled && this._state.pbrSupported) {
                if (this._renderers.pbrRendererWithSAO) {
                    this._renderers.pbrRendererWithSAO.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
                }
            } else if (frameCtx.colorTextureEnabled && this.model.colorTextureEnabled && this._state.colorTextureSupported) {
                if (useAlphaCutoff) {
                    if (this._renderers.colorTextureRendererWithSAOAlphaCutoff) {
                        this._renderers.colorTextureRendererWithSAOAlphaCutoff.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
                    }
                } else {
                    if (this._renderers.colorTextureRendererWithSAO) {
                        this._renderers.colorTextureRendererWithSAO.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
                    }
                }
            } else if (this._state.normalsBuf) {
                if (this._renderers.colorRendererWithSAO) {
                    this._renderers.colorRendererWithSAO.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
                }
            } else {
                if (this._renderers.flatColorRendererWithSAO) {
                    this._renderers.flatColorRendererWithSAO.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
                }
            }
        } else if (frameCtx.pbrEnabled && this.model.pbrEnabled && this._state.pbrSupported) {
            if (this._renderers.pbrRenderer) {
                this._renderers.pbrRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
            }
        } else if (frameCtx.colorTextureEnabled && this.model.colorTextureEnabled && this._state.colorTextureSupported) {
            if (useAlphaCutoff) {
                if (this._renderers.colorTextureRendererAlphaCutoff) {
                    this._renderers.colorTextureRendererAlphaCutoff.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
                }
            } else {
                if (this._renderers.colorTextureRenderer) {
                    this._renderers.colorTextureRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
                }
            }
        } else if (this._state.normalsBuf) {
            if (this._renderers.colorRenderer) {
                this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
            }
        } else {
            if (this._renderers.flatColorRenderer) {
                this._renderers.flatColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
            }
        }
    }

    _updateBackfaceCull(renderFlags, frameCtx) {
        const backfaces = true; // See XCD-230
        if (frameCtx.backfaces !== backfaces) {
            const gl = frameCtx.gl;
            {
                gl.disable(gl.CULL_FACE);
            }
            frameCtx.backfaces = backfaces;
        }
    }

    drawColorTransparent(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === 0 || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (frameCtx.pbrEnabled && this.model.pbrEnabled && this._state.pbrSupported) {
            if (this._renderers.pbrRenderer) {
                this._renderers.pbrRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT);
            }
        } else if (frameCtx.colorTextureEnabled && this.model.colorTextureEnabled && this._state.colorTextureSupported) {
            if (this._renderers.colorTextureRenderer) {
                this._renderers.colorTextureRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT);
            }
        } else if (this._state.normalsBuf) {
            if (this._renderers.colorRenderer) {
                this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT);
            }
        } else {
            if (this._renderers.flatColorRenderer) {
                this._renderers.flatColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT);
            }
        }
    }

    // ---------------------- RENDERING SAO POST EFFECT TARGETS --------------

    drawDepth(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.depthRenderer) {
            this._renderers.depthRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); // Assume whatever post-effect uses depth (eg SAO) does not apply to transparent objects
        }
    }

    drawNormals(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.normalsRenderer) {
            this._renderers.normalsRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); // Assume whatever post-effect uses normals (eg SAO) does not apply to transparent objects
        }
    }

    // ---------------------- SILHOUETTE RENDERING -----------------------------------

    drawSilhouetteXRayed(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_XRAYED);
        }
    }

    drawSilhouetteHighlighted(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_HIGHLIGHTED);
        }
    }

    drawSilhouetteSelected(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_SELECTED);
        }
    }

    // ---------------------- EDGES RENDERING -----------------------------------

    drawEdgesColorOpaque(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numEdgesLayerPortions === 0) {
            return;
        }
        if (this._renderers.edgesColorRenderer) {
            this._renderers.edgesColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_COLOR_OPAQUE);
        }
    }

    drawEdgesColorTransparent(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numEdgesLayerPortions === 0) {
            return;
        }
        if (this._renderers.edgesColorRenderer) {
            this._renderers.edgesColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_COLOR_TRANSPARENT);
        }
    }

    drawEdgesXRayed(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) {
            return;
        }
        if (this._renderers.edgesRenderer) {
            this._renderers.edgesRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_XRAYED);
        }
    }

    drawEdgesHighlighted(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) {
            return;
        }
        if (this._renderers.edgesRenderer) {
            this._renderers.edgesRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_HIGHLIGHTED);
        }
    }

    drawEdgesSelected(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) {
            return;
        }
        if (this._renderers.edgesRenderer) {
            this._renderers.edgesRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_SELECTED);
        }
    }

    // ---------------------- OCCLUSION CULL RENDERING -----------------------------------

    drawOcclusion(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.occlusionRenderer) {
            // Only opaque, filled objects can be occluders
            this._renderers.occlusionRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
        }
    }

    // ---------------------- SHADOW BUFFER RENDERING -----------------------------------

    drawShadow(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.shadowRenderer) {
            this._renderers.shadowRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
        }
    }

    //---- PICKING ----------------------------------------------------------------------------------------------------

    drawPickMesh(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.pickMeshRenderer) {
            this._renderers.pickMeshRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawPickDepths(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.pickDepthRenderer) {
            this._renderers.pickDepthRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawPickNormals(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);

        ////////////////////////////////////////////////////////////////////////////////////////////////////
        // TODO
        // if (this._state.normalsBuf) {
        //     if (this._renderers.pickNormalsRenderer) {
        //         this._renderers.pickNormalsRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        //     }
        ////////////////////////////////////////////////////////////////////////////////////////////////////
        // } else {
        if (this._renderers.pickNormalsFlatRenderer) {
            this._renderers.pickNormalsFlatRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
        // }
    }

    drawSnapInit(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.snapInitRenderer) {
            this._renderers.snapInitRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawSnap(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.snapRenderer) {
            this._renderers.snapRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    //-----------------------------------------------------------------------------------------

    precisionRayPickSurface(portionId, worldRayOrigin, worldRayDir, worldSurfacePos, worldNormal) {

        if (!this.model.scene.readableGeometryEnabled) {
            return false;
        }

        const geometry = this._state.geometry;
        const state = this._state;
        const portion = this._portions[portionId];

        if (!portion) {
            this.model.error("portion not found: " + portionId);
            return false;
        }

        if (!portion.inverseMatrix) {
            portion.inverseMatrix = math.inverseMat4(portion.matrix, math.mat4());
        }

        if (worldNormal && !portion.normalMatrix) {
            portion.normalMatrix = math.transposeMat4(portion.inverseMatrix, math.mat4());
        }

        const quantizedPositions = geometry.quantizedPositions;
        const indices = geometry.indices;
        const origin = state.origin;
        const offset = portion.offset;

        const rtcRayOrigin = tempVec3a$z;
        const rtcRayDir = tempVec3b$v;

        rtcRayOrigin.set(origin ? math.subVec3(worldRayOrigin, origin, tempVec3c$q) : worldRayOrigin);  // World -> RTC
        rtcRayDir.set(worldRayDir);

        if (offset) {
            math.subVec3(rtcRayOrigin, offset);
        }

        math.transformRay(this.model.worldNormalMatrix, rtcRayOrigin, rtcRayDir, rtcRayOrigin, rtcRayDir);

        math.transformRay(portion.inverseMatrix, rtcRayOrigin, rtcRayDir, rtcRayOrigin, rtcRayDir);

        const a = tempVec3d$9;
        const b = tempVec3e$1;
        const c = tempVec3f$1;

        let gotIntersect = false;
        let closestDist = 0;
        const closestIntersectPos = tempVec3g;

        for (let i = 0, len = indices.length; i < len; i += 3) {

            const ia = indices[i + 0] * 3;
            const ib = indices[i + 1] * 3;
            const ic = indices[i + 2] * 3;

            a[0] = quantizedPositions[ia];
            a[1] = quantizedPositions[ia + 1];
            a[2] = quantizedPositions[ia + 2];

            b[0] = quantizedPositions[ib];
            b[1] = quantizedPositions[ib + 1];
            b[2] = quantizedPositions[ib + 2];

            c[0] = quantizedPositions[ic];
            c[1] = quantizedPositions[ic + 1];
            c[2] = quantizedPositions[ic + 2];

            const {positionsDecodeMatrix} = state.geometry;

            math.decompressPosition(a, positionsDecodeMatrix);
            math.decompressPosition(b, positionsDecodeMatrix);
            math.decompressPosition(c, positionsDecodeMatrix);

            if (math.rayTriangleIntersect(rtcRayOrigin, rtcRayDir, a, b, c, closestIntersectPos)) {

                math.transformPoint3(portion.matrix, closestIntersectPos, closestIntersectPos);

                math.transformPoint3(this.model.worldMatrix, closestIntersectPos, closestIntersectPos);

                if (offset) {
                    math.addVec3(closestIntersectPos, offset);
                }

                if (origin) {
                    math.addVec3(closestIntersectPos, origin);
                }

                const dist = Math.abs(math.lenVec3(math.subVec3(closestIntersectPos, worldRayOrigin, [])));

                if (!gotIntersect || dist > closestDist) {
                    closestDist = dist;
                    worldSurfacePos.set(closestIntersectPos);
                    if (worldNormal) { // Not that wasteful to eagerly compute - unlikely to hit >2 surfaces on most geometry
                        math.triangleNormal(a, b, c, worldNormal);
                    }
                    gotIntersect = true;
                }
            }
        }

        if (gotIntersect && worldNormal) {
            math.transformVec3(portion.normalMatrix, worldNormal, worldNormal);
            math.transformVec3(this.model.worldNormalMatrix, worldNormal, worldNormal);
            math.normalizeVec3(worldNormal);
        }

        return gotIntersect;
    }

    destroy() {
        const state = this._state;
        if (state.colorsBuf) {
            state.colorsBuf.destroy();
            state.colorsBuf = null;
        }
        if (state.metallicRoughnessBuf) {
            state.metallicRoughnessBuf.destroy();
            state.metallicRoughnessBuf = null;
        }
        if (state.flagsBuf) {
            state.flagsBuf.destroy();
            state.flagsBuf = null;
        }
        if (state.offsetsBuf) {
            state.offsetsBuf.destroy();
            state.offsetsBuf = null;
        }
        if (state.modelMatrixCol0Buf) {
            state.modelMatrixCol0Buf.destroy();
            state.modelMatrixCol0Buf = null;
        }
        if (state.modelMatrixCol1Buf) {
            state.modelMatrixCol1Buf.destroy();
            state.modelMatrixCol1Buf = null;
        }
        if (state.modelMatrixCol2Buf) {
            state.modelMatrixCol2Buf.destroy();
            state.modelMatrixCol2Buf = null;
        }
        if (state.modelNormalMatrixCol0Buf) {
            state.modelNormalMatrixCol0Buf.destroy();
            state.modelNormalMatrixCol0Buf = null;
        }
        if (state.modelNormalMatrixCol1Buf) {
            state.modelNormalMatrixCol1Buf.destroy();
            state.modelNormalMatrixCol1Buf = null;
        }
        if (state.modelNormalMatrixCol2Buf) {
            state.modelNormalMatrixCol2Buf.destroy();
            state.modelNormalMatrixCol2Buf = null;
        }
        if (state.pickColorsBuf) {
            state.pickColorsBuf.destroy();
            state.pickColorsBuf = null;
        }
        state.destroy();
        this._state = null;
    }
}

/**
 * @private
 */


class VBOSceneModelLineBatchingRenderer extends VBORenderer {
    _draw(drawCfg) {
        const {gl} = this._scene.canvas;

        const {
            state,
            frameCtx,
            incrementDrawState
        } = drawCfg;

        gl.drawElements(gl.LINES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0);

        if (incrementDrawState) {
            frameCtx.drawElements++;
        }
    }
}

/**
 * @private
 */
class VBOBatchingLinesColorRenderer extends VBOSceneModelLineBatchingRenderer {

    drawLayer(frameCtx, layer, renderPass) {
        super.drawLayer(frameCtx, layer, renderPass, { incrementDrawState: true });
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Lines batching color vertex shader");
        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        src.push("in vec4 color;");
        src.push("in float flags;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        this._addMatricesUniformBlockLines(src);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
        }
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec4 vColor;");
        src.push("void main(void) {");
        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE
        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("} else {");
        src.push("vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("vColor = vec4(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0, float(color.a) / 255.0);");
        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
        }
        src.push("gl_Position = clipPos;");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Lines batching color fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > 0.0) { discard; }");
            src.push("}");
        }
        src.push("   outColor            = vColor;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class VBOBatchingLinesSilhouetteRenderer extends VBOSceneModelLineBatchingRenderer {

    drawLayer(frameCtx, batchingLayer, renderPass) {
        super.drawLayer(frameCtx, batchingLayer, renderPass, { colorUniform: true });
    }

    _buildVertexShader() {

        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;

        const src = [];
        src.push('#version 300 es');
        src.push("// Lines batching silhouette vertex shader");

        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec4 color;");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }

        src.push("void main(void) {");

        // silhouetteFlag = NOT_RENDERED | SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED
        // renderPass = SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED

        src.push(`int silhouetteFlag = int(flags) >> 4 & 0xF;`);
        src.push(`if (silhouetteFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("} else {");

        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
        }
        src.push("gl_Position = clipPos;");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {

        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Lines batching silhouette fragment shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("uniform vec4 color;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > 0.0) { discard; }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outColor = color;");
        src.push("}");
        return src;
    }
}

const tempVec3a$y = math.vec3();
const tempVec3b$u = math.vec3();
const tempVec3c$p = math.vec3();
const tempVec3d$8 = math.vec3();
const tempMat4a$m = math.mat4();

/**
 * @private
 */
class VBOBatchingLinesSnapInitRenderer extends VBORenderer {
    drawLayer(frameCtx, batchingLayer, renderPass) {

        if (!this._program) {
            this._allocate();
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram();
        }

        const model = batchingLayer.model;
        const scene = model.scene;
        const camera = scene.camera;
        const gl = scene.canvas.gl;
        const state = batchingLayer._state;
        const origin = batchingLayer._state.origin;
        const {position, rotationMatrix} = model;
        const aabb = batchingLayer.aabb; // Per-layer AABB for best RTC accuracy
        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;

        if (this._vaoCache.has(batchingLayer)) {
            gl.bindVertexArray(this._vaoCache.get(batchingLayer));
        } else {
            this._vaoCache.set(batchingLayer, this._makeVAO(state));
        }

        const coordinateScaler = tempVec3a$y;
        coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT;
        coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT;
        coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT;

        frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]);
        frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]);
        frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]);

        let rtcViewMatrix;
        let rtcCameraEye;

        if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) {
            const rtcOrigin = tempVec3b$u;
            if (origin) {
                const rotatedOrigin = tempVec3c$p;
                math.transformPoint3(rotationMatrix, origin, rotatedOrigin);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$m);
            rtcCameraEye = tempVec3d$8;
            rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0];
            rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1];
            rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2];
            frameCtx.snapPickOrigin[0] = rtcOrigin[0];
            frameCtx.snapPickOrigin[1] = rtcOrigin[1];
            frameCtx.snapPickOrigin[2] = rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            rtcCameraEye = camera.eye;
            frameCtx.snapPickOrigin[0] = 0;
            frameCtx.snapPickOrigin[1] = 0;
            frameCtx.snapPickOrigin[2] = 0;
        }

        gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye);
        gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA);
        gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB);
        gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber);
        gl.uniform3fv(this._uCoordinateScaler, coordinateScaler);
        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible);

        let offset = 0;
        const mat4Size = 4 * 4;

        this._matricesUniformBlockBufferData.set(rotationMatrix, 0);
        this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size);

        gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer);
        gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW);

        gl.bindBufferBase(
            gl.UNIFORM_BUFFER,
            this._matricesUniformBlockBufferBindingPoint,
            this._matricesUniformBlockBuffer);

        {
            const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix?
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }

        this.setSectionPlanesStateUniforms(batchingLayer);
        //=============================================================
        // TODO: Use drawElements count and offset to draw only one entity
        //=============================================================

        state.indicesBuf.bind();
        gl.drawElements(gl.LINES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0);
        state.indicesBuf.unbind();

        gl.bindVertexArray(null);
    }

    _allocate() {
        super._allocate();

        const program = this._program;

        {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
        this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc");
        this.uVectorA = program.getLocation("snapVectorA");
        this.uInverseVectorAB = program.getLocation("snapInvVectorAB");
        this._uLayerNumber = program.getLocation("layerNumber");
        this._uCoordinateScaler = program.getLocation("coordinateScaler");
    }

    _bindProgram() {
        this._program.bind();
    }

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// VBO SnapBatchingDepthBufInitRenderer vertex shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");
        src.push("uniform int renderPass;");
        src.push("in vec4 pickColor;");
        src.push("in vec3 position;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec3 uCameraEyeRtc;");
        src.push("uniform vec2 snapVectorA;");
        src.push("uniform vec2 snapInvVectorAB;");
        {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("out float isPerspective;");
        }
        src.push("bool isPerspectiveMatrix(mat4 m) {");
        src.push("    return (m[2][3] == - 1.0);");
        src.push("}");
        src.push("vec2 remapClipPos(vec2 clipPos) {");
        src.push("    float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;");
        src.push("    float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;");
        src.push("    return vec2(x, y);");
        src.push("}");
        src.push("flat out vec4 vPickColor;");
        src.push("out vec4 vWorldPosition;");
        if (clipping) {
            src.push("out float vFlags;");
        }
        src.push("out highp vec3 relativeToOriginPosition;");
        src.push("void main(void) {");
        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK
        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("      gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("  } else {");
        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("      relativeToOriginPosition = worldPosition.xyz;");
        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("      vWorldPosition = worldPosition;");
        if (clipping) {
            src.push("      vFlags = flags;");
        }
        src.push("vPickColor = pickColor;");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        src.push("float tmp = clipPos.w;");
        src.push("clipPos.xyzw /= tmp;");
        src.push("clipPos.xy = remapClipPos(clipPos.xy);");
        src.push("clipPos.xyzw *= tmp;");
        {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// VBO SnapBatchingDepthBufInitRenderer fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("uniform int layerNumber;");
        src.push("uniform vec3 coordinateScaler;");
        src.push("in vec4 vWorldPosition;");
        src.push("flat in vec4 vPickColor;");
        if (clipping) {
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in highp vec3 relativeToOriginPosition;");
        src.push("layout(location = 0) out highp ivec4 outCoords;");
        src.push("layout(location = 1) out highp ivec4 outNormal;");
        src.push("layout(location = 2) out lowp uvec4 outPickColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        {
            src.push("    float dx = dFdx(vFragDepth);");
            src.push("    float dy = dFdy(vFragDepth);");
            src.push("    float diff = sqrt(dx*dx+dy*dy);");
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth + diff ) * logDepthBufFC * 0.5;");
        }
        src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, -layerNumber);");

        src.push("vec3 xTangent = dFdx( vWorldPosition.xyz );");
        src.push("vec3 yTangent = dFdy( vWorldPosition.xyz );");
        src.push("vec3 worldNormal = normalize( cross( xTangent, yTangent ) );");
        src.push(`outNormal = ivec4(worldNormal * float(${math.MAX_INT}), 1.0);`);
        src.push("outPickColor = uvec4(vPickColor);");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

const tempVec3a$x = math.vec3();
const tempVec3b$t = math.vec3();
const tempVec3c$o = math.vec3();
const tempVec3d$7 = math.vec3();
const tempMat4a$l = math.mat4();

/**
 * @private
 */
class VBOBatchingLinesSnapRenderer extends VBORenderer{
    _getHash() {
        return this._scene._sectionPlanesState.getHash() + (this._scene.pointsMaterial.hash);
    }

    drawLayer(frameCtx, batchingLayer, renderPass) {

        if (!this._program) {
            this._allocate();
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram();
        }

        const model = batchingLayer.model;
        const scene = model.scene;
        const camera = scene.camera;
        const gl = scene.canvas.gl;
        const state = batchingLayer._state;
        const origin = batchingLayer._state.origin;
        const {position, rotationMatrix} = model;
        const aabb = batchingLayer.aabb; // Per-layer AABB for best RTC accuracy
        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;

        if (this._vaoCache.has(batchingLayer)) {
            gl.bindVertexArray(this._vaoCache.get(batchingLayer));
        } else {
            this._vaoCache.set(batchingLayer, this._makeVAO(state));
        }

        const coordinateScaler = tempVec3a$x;
        coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT;
        coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT;
        coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT;

        frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]);
        frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]);
        frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]);

        let rtcViewMatrix;
        let rtcCameraEye;

        if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) {
            const rtcOrigin = tempVec3b$t;
            if (origin) {
                const rotatedOrigin = tempVec3c$o;
                math.transformPoint3(rotationMatrix, origin, rotatedOrigin);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$l);
            rtcCameraEye = tempVec3d$7;
            rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0];
            rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1];
            rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2];
            frameCtx.snapPickOrigin[0] = rtcOrigin[0];
            frameCtx.snapPickOrigin[1] = rtcOrigin[1];
            frameCtx.snapPickOrigin[2] = rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            rtcCameraEye = camera.eye;
            frameCtx.snapPickOrigin[0] = 0;
            frameCtx.snapPickOrigin[1] = 0;
            frameCtx.snapPickOrigin[2] = 0;
        }

        gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye);
        gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA);
        gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB);
        gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber);
        gl.uniform3fv(this._uCoordinateScaler, coordinateScaler);
        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible);

        let offset = 0;
        const mat4Size = 4 * 4;

        this._matricesUniformBlockBufferData.set(rotationMatrix, 0);
        this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size);

        gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer);
        gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW);

        gl.bindBufferBase(
            gl.UNIFORM_BUFFER,
            this._matricesUniformBlockBufferBindingPoint,
            this._matricesUniformBlockBuffer);

        {
            const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix?
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }

        this.setSectionPlanesStateUniforms(batchingLayer);

        //=============================================================
        // TODO: Use drawElements count and offset to draw only one entity
        //=============================================================

        if (frameCtx.snapMode === "edge") {
            state.indicesBuf.bind();
            gl.drawElements(gl.LINES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0);
            state.indicesBuf.unbind(); // needed?
        } else {
            gl.drawArrays(gl.POINTS, 0, state.positionsBuf.numItems);
        }

        gl.bindVertexArray(null);
    }

    _allocate() {
        super._allocate();

        const program = this._program;

        {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }

        this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); 
        this.uVectorA = program.getLocation("snapVectorA"); 
        this.uInverseVectorAB = program.getLocation("snapInvVectorAB"); 
        this._uLayerNumber = program.getLocation("layerNumber"); 
        this._uCoordinateScaler = program.getLocation("coordinateScaler"); 
    }

    _bindProgram() {
        this._program.bind();
    }

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        scene.pointsMaterial._state;
        const src = [];
        src.push ('#version 300 es');
        src.push("// SnapBatchingDepthRenderer vertex shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");
        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec3 uCameraEyeRtc;"); 
        src.push("uniform vec2 snapVectorA;"); 
        src.push("uniform vec2 snapInvVectorAB;"); 
        {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }
        src.push("vec2 remapClipPos(vec2 clipPos) {");
        src.push("    float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;");
        src.push("    float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;");
        src.push("    return vec2(x, y);");
        src.push("}");
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out highp vec3 relativeToOriginPosition;");
        src.push("void main(void) {");

        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK

        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("      gl_Position = vec4(2.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("  } else {");
        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("relativeToOriginPosition = worldPosition.xyz;");
        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("      vWorldPosition = worldPosition;");
            src.push("      vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        src.push("float tmp = clipPos.w;");
        src.push("clipPos.xyzw /= tmp;");
        src.push("clipPos.xy = remapClipPos(clipPos.xy);");
        src.push("clipPos.xyzw *= tmp;");
        {
           src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("gl_PointSize = 1.0;"); // Windows needs this?
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// SnapBatchingDepthRenderer fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("uniform int layerNumber;"); 
        src.push("uniform vec3 coordinateScaler;"); 
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in highp vec3 relativeToOriginPosition;");
        src.push("out highp ivec4 outCoords;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, layerNumber);");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

/**
 * @private
 */
class VBOBatchingLinesRenderers {

    constructor(scene) {
        this._scene = scene;
    }

    _compile() {
        if (this._colorRenderer && (!this._colorRenderer.getValid())) {
            this._colorRenderer.destroy();
            this._colorRenderer = null;
        }
        if (this._silhouetteRenderer && (!this._silhouetteRenderer.getValid())) {
            this._silhouetteRenderer.destroy();
            this._silhouetteRenderer = null;
        }
        if (this._snapInitRenderer && (!this._snapInitRenderer.getValid())) {
            this._snapInitRenderer.destroy();
            this._snapInitRenderer = null;
        }
        if (this._snapRenderer && (!this._snapRenderer.getValid())) {
            this._snapRenderer.destroy();
            this._snapRenderer = null;
        }
    }

    get colorRenderer() {
        if (!this._colorRenderer) {
            this._colorRenderer = new VBOBatchingLinesColorRenderer(this._scene, false);
        }
        return this._colorRenderer;
    }

    get silhouetteRenderer() {
        if (!this._silhouetteRenderer) {
            this._silhouetteRenderer = new VBOBatchingLinesSilhouetteRenderer(this._scene);
        }
        return this._silhouetteRenderer;
    }

    get snapInitRenderer() {
        if (!this._snapInitRenderer) {
            this._snapInitRenderer = new VBOBatchingLinesSnapInitRenderer(this._scene, false);
        }
        return this._snapInitRenderer;
    }

    get snapRenderer() {
        if (!this._snapRenderer) {
            this._snapRenderer = new VBOBatchingLinesSnapRenderer(this._scene);
        }
        return this._snapRenderer;
    }

    _destroy() {
        if (this._colorRenderer) {
            this._colorRenderer.destroy();
        }
        if (this._silhouetteRenderer) {
            this._silhouetteRenderer.destroy();
        }
        if (this._snapInitRenderer) {
            this._snapInitRenderer.destroy();
        }
        if (this._snapRenderer) {
            this._snapRenderer.destroy();
        }
    }
}

const cachedRenderers$4 = {};

/**
 * @private
 */
function getRenderers$5(scene) {
    const sceneId = scene.id;
    let batchingRenderers = cachedRenderers$4[sceneId];
    if (!batchingRenderers) {
        batchingRenderers = new VBOBatchingLinesRenderers(scene);
        cachedRenderers$4[sceneId] = batchingRenderers;
        batchingRenderers._compile();
        scene.on("compile", () => {
            batchingRenderers._compile();
        });
        scene.on("destroyed", () => {
            delete cachedRenderers$4[sceneId];
            batchingRenderers._destroy();
        });
    }
    return batchingRenderers;
}

/**
 * @private
 */
class VBOBatchingLinesBuffer {

    constructor(maxGeometryBatchSize = 5000000) {

        if (maxGeometryBatchSize > 5000000) {
            maxGeometryBatchSize = 5000000;
        }

        this.maxVerts = maxGeometryBatchSize;
        this.maxIndices = maxGeometryBatchSize * 3; // Rough rule-of-thumb
        this.positions = [];
        this.colors = [];
        this.offsets = [];
        this.indices = [];
    }
}

/**
 * @private
 */
class VBOBatchingLinesLayer {

    /**
     * @param model
     * @param cfg
     * @param cfg.layerIndex
     * @param cfg.positionsDecodeMatrix
     * @param cfg.maxGeometryBatchSize
     * @param cfg.origin
     * @param cfg.scratchMemory
     */
    constructor(cfg) {

       // console.info("Creating VBOBatchingLinesLayer");

        /**
         * Index of this LinesBatchingLayer in {@link VBOSceneModel#_layerList}.
         * @type {Number}
         */
        this.layerIndex = cfg.layerIndex;

        this._renderers = getRenderers$5(cfg.model.scene);
        this.model = cfg.model;
        this._buffer = new VBOBatchingLinesBuffer(cfg.maxGeometryBatchSize);
        this._scratchMemory = cfg.scratchMemory;

        this._state = new RenderState({
            positionsBuf: null,
            offsetsBuf: null,
            colorsBuf: null,
            flagsBuf: null,
            indicesBuf: null,
            positionsDecodeMatrix: math.mat4(),
            origin: null
        });

        // These counts are used to avoid unnecessary render passes
        this._numPortions = 0;
        this._numVisibleLayerPortions = 0;
        this._numTransparentLayerPortions = 0;
        this._numXRayedLayerPortions = 0;
        this._numSelectedLayerPortions = 0;
        this._numHighlightedLayerPortions = 0;
        this._numClippableLayerPortions = 0;
        this._numEdgesLayerPortions = 0;
        this._numPickableLayerPortions = 0;
        this._numCulledLayerPortions = 0;

        this._modelAABB = math.collapseAABB3(); // Model-space AABB
        this._portions = [];
        this._meshes = [];
        this._numVerts = 0;

        this._aabb = math.collapseAABB3();
        this.aabbDirty = true;

        this._finalized = false;

        if (cfg.positionsDecodeMatrix) {
            this._state.positionsDecodeMatrix.set(cfg.positionsDecodeMatrix);
            this._preCompressedPositionsExpected = true;
        } else {
            this._preCompressedPositionsExpected = false;
        }

        if (cfg.origin) {
            this._state.origin = math.vec3(cfg.origin);
        }

        /**
         * The type of primitives in this layer.
         */
        this.primitive = cfg.primitive;
    }

    get aabb() {
        if (this.aabbDirty) {
            math.collapseAABB3(this._aabb);
            for (let i = 0, len = this._meshes.length; i < len; i++) {
                math.expandAABB3(this._aabb, this._meshes[i].aabb);
            }
            this.aabbDirty = false;
        }
        return this._aabb;
    }

    /**
     * Tests if there is room for another portion in this LinesBatchingLayer.
     *
     * @param lenPositions Number of positions we'd like to create in the portion.
     * @param lenIndices Number of indices we'd like to create in this portion.
     * @returns {Boolean} True if OK to create another portion.
     */
    canCreatePortion(lenPositions, lenIndices) {
        if (this._finalized) {
            throw "Already finalized";
        }
        return ((this._buffer.positions.length + lenPositions) < (this._buffer.maxVerts * 3) && (this._buffer.indices.length + lenIndices) < (this._buffer.maxIndices));
    }

    /**
     * Creates a new portion within this LinesBatchingLayer, returns the new portion ID.
     *
     * Gives the portion the specified geometry, color and matrix.
     *
     * @param mesh The SceneModelMesh that owns the portion
     * @param cfg.positions Flat float Local-space positions array.
     * @param cfg.positionsCompressed Flat quantized positions array - decompressed with TrianglesBatchingLayer positionsDecodeMatrix
     * @param cfg.indices  Flat int indices array.
     * @param cfg.color Quantized RGB color [0..255,0..255,0..255,0..255]
     * @param cfg.opacity Opacity [0..255]
     * @param [cfg.meshMatrix] Flat float 4x4 matrix
     * @param cfg.aabb Flat float AABB World-space AABB
     * @param cfg.pickColor Quantized pick color
     * @returns {number} Portion ID
     */
    createPortion(mesh, cfg) {

        if (this._finalized) {
            throw "Already finalized";
        }

        const positions = cfg.positions;
        const positionsCompressed = cfg.positionsCompressed;
        const indices = cfg.indices;
        const color = cfg.color;
        const opacity = cfg.opacity;

        const buffer = this._buffer;
        const positionsIndex = buffer.positions.length;
        const vertsIndex = positionsIndex / 3;

        let numVerts;

        math.expandAABB3(this._modelAABB, cfg.aabb);

        if (this._preCompressedPositionsExpected) {
            if (!positionsCompressed) {
                throw "positionsCompressed expected";
            }
            numVerts = positionsCompressed.length / 3;
            for (let i = 0, len = positionsCompressed.length; i < len; i++) {
                buffer.positions.push(positionsCompressed[i]);
            }
        } else {
            if (!positions) {
                throw "positions expected";
            }
            numVerts = positions.length / 3;
            for (let i = 0, len = positions.length; i < len; i++) {
                buffer.positions.push(positions[i]);
            }
        }

        if (color) {

            const r = color[0]; // Color is pre-quantized by VBOSceneModel
            const g = color[1];
            const b = color[2];
            const a = opacity;

            for (let i = 0; i < numVerts; i++) {
                buffer.colors.push(r);
                buffer.colors.push(g);
                buffer.colors.push(b);
                buffer.colors.push(a);
            }
        }

        if (indices) {
            for (let i = 0, len = indices.length; i < len; i++) {
                buffer.indices.push(indices[i] + vertsIndex);
            }
        }

        if (this.model.scene.entityOffsetsEnabled) {
            for (let i = 0; i < numVerts; i++) {
                buffer.offsets.push(0);
                buffer.offsets.push(0);
                buffer.offsets.push(0);
            }
        }

        const portionId = this._portions.length / 2;

        this._portions.push(vertsIndex);
        this._portions.push(numVerts);

        this._numPortions++;
        this.model.numPortions++;

        this._numVerts += numVerts;

        this._meshes.push(mesh);

        return portionId;
    }

    /**
     * Builds batch VBOs from appended geometries.
     * No more portions can then be created.
     */
    finalize() {

        if (this._finalized) {
            return;
        }

        const state = this._state;
        const gl = this.model.scene.canvas.gl;
        const buffer = this._buffer;

        if (buffer.positions.length > 0) {
            if (this._preCompressedPositionsExpected) {
                const positions = new Uint16Array(buffer.positions);
                state.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, positions, buffer.positions.length, 3, gl.STATIC_DRAW);
            } else {
                const positions = new Float32Array(buffer.positions);
                const quantizedPositions = quantizePositions(positions, this._modelAABB, state.positionsDecodeMatrix);
                state.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, quantizedPositions, buffer.positions.length, 3, gl.STATIC_DRAW);
            }
        }

        if (buffer.colors.length > 0) {
            const colors = new Uint8Array(buffer.colors);
            let normalized = false;
            state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, colors, buffer.colors.length, 4, gl.DYNAMIC_DRAW, normalized);
        }

        if (buffer.colors.length > 0) { // Because we build flags arrays here, get their length from the colors array
            const flagsLength = buffer.colors.length / 4;
            const flags = new Float32Array(flagsLength);
            let notNormalized = false;
            state.flagsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, flags, flags.length, 1, gl.DYNAMIC_DRAW, notNormalized);
        }

        if (this.model.scene.entityOffsetsEnabled) {
            if (buffer.offsets.length > 0) {
                const offsets = new Float32Array(buffer.offsets);
                state.offsetsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, offsets, buffer.offsets.length, 3, gl.DYNAMIC_DRAW);
            }
        }

        if (buffer.indices.length > 0) {
            const indices = new Uint32Array(buffer.indices);
            state.indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, indices, buffer.indices.length, 1, gl.STATIC_DRAW);
        }

        this._buffer = null;
        this._finalized = true;
    }

    initFlags(portionId, flags, meshTransparent) {
        if (flags & ENTITY_FLAGS.VISIBLE) {
            this._numVisibleLayerPortions++;
            this.model.numVisibleLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.HIGHLIGHTED) {
            this._numHighlightedLayerPortions++;
            this.model.numHighlightedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.XRAYED) {
            this._numXRayedLayerPortions++;
            this.model.numXRayedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.SELECTED) {
            this._numSelectedLayerPortions++;
            this.model.numSelectedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.CLIPPABLE) {
            this._numClippableLayerPortions++;
            this.model.numClippableLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.EDGES) {
            this._numEdgesLayerPortions++;
            this.model.numEdgesLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.PICKABLE) {
            this._numPickableLayerPortions++;
            this.model.numPickableLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.CULLED) {
            this._numCulledLayerPortions++;
            this.model.numCulledLayerPortions++;
        }
        if (meshTransparent) {
            this._numTransparentLayerPortions++;
            this.model.numTransparentLayerPortions++;
        }
        const deferred = true;
        this._setFlags(portionId, flags, meshTransparent, deferred);
    }

    flushInitFlags() {
        this._setDeferredFlags();
    }

    setVisible(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.VISIBLE) {
            this._numVisibleLayerPortions++;
            this.model.numVisibleLayerPortions++;
        } else {
            this._numVisibleLayerPortions--;
            this.model.numVisibleLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setHighlighted(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.HIGHLIGHTED) {
            this._numHighlightedLayerPortions++;
            this.model.numHighlightedLayerPortions++;
        } else {
            this._numHighlightedLayerPortions--;
            this.model.numHighlightedLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setXRayed(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.XRAYED) {
            this._numXRayedLayerPortions++;
            this.model.numXRayedLayerPortions++;
        } else {
            this._numXRayedLayerPortions--;
            this.model.numXRayedLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setSelected(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.SELECTED) {
            this._numSelectedLayerPortions++;
            this.model.numSelectedLayerPortions++;
        } else {
            this._numSelectedLayerPortions--;
            this.model.numSelectedLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setEdges(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.EDGES) {
            this._numEdgesLayerPortions++;
            this.model.numEdgesLayerPortions++;
        } else {
            this._numEdgesLayerPortions--;
            this.model.numEdgesLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setClippable(portionId, flags) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.CLIPPABLE) {
            this._numClippableLayerPortions++;
            this.model.numClippableLayerPortions++;
        } else {
            this._numClippableLayerPortions--;
            this.model.numClippableLayerPortions--;
        }
        this._setFlags(portionId, flags);
    }

    setCulled(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.CULLED) {
            this._numCulledLayerPortions++;
            this.model.numCulledLayerPortions++;
        } else {
            this._numCulledLayerPortions--;
            this.model.numCulledLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setCollidable(portionId, flags) {
        if (!this._finalized) {
            throw "Not finalized";
        }
    }

    setPickable(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.PICKABLE) {
            this._numPickableLayerPortions++;
            this.model.numPickableLayerPortions++;
        } else {
            this._numPickableLayerPortions--;
            this.model.numPickableLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setColor(portionId, color) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        const portionsIdx = portionId * 2;
        const vertexBase = this._portions[portionsIdx];
        const numVerts = this._portions[portionsIdx + 1];
        const firstColor = vertexBase * 4;
        const lenColor = numVerts * 4;
        const tempArray = this._scratchMemory.getUInt8Array(lenColor);
        const r = color[0];
        const g = color[1];
        const b = color[2];
        const a = color[3];
        for (let i = 0; i < lenColor; i += 4) {
            tempArray[i + 0] = r;
            tempArray[i + 1] = g;
            tempArray[i + 2] = b;
            tempArray[i + 3] = a;
        }
        this._state.colorsBuf.setData(tempArray, firstColor, lenColor);
    }

    setTransparent(portionId, flags, transparent) {
        if (transparent) {
            this._numTransparentLayerPortions++;
            this.model.numTransparentLayerPortions++;
        } else {
            this._numTransparentLayerPortions--;
            this.model.numTransparentLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    /**
     * flags are 4bits values encoded on a 32bit base. color flag on the first 4 bits, silhouette flag on the next 4 bits and so on for edge, pick and clippable.
     */
    _setFlags(portionId, flags, transparent, deferred = false) {

        if (!this._finalized) {
            throw "Not finalized";
        }

        const portionsIdx = portionId * 2;
        const vertexBase = this._portions[portionsIdx];
        const numVerts = this._portions[portionsIdx + 1];
        const firstFlag = vertexBase;
        const lenFlags = numVerts;

        const visible = !!(flags & ENTITY_FLAGS.VISIBLE);
        const xrayed = !!(flags & ENTITY_FLAGS.XRAYED);
        const highlighted = !!(flags & ENTITY_FLAGS.HIGHLIGHTED);
        const selected = !!(flags & ENTITY_FLAGS.SELECTED);
        // no edges
        const pickable = !!(flags & ENTITY_FLAGS.PICKABLE);
        const culled = !!(flags & ENTITY_FLAGS.CULLED);

        let colorFlag;
        if (!visible || culled || xrayed
            || (highlighted && !this.model.scene.highlightMaterial.glowThrough)
            || (selected && !this.model.scene.selectedMaterial.glowThrough)) {
            colorFlag = RENDER_PASSES.NOT_RENDERED;
        } else {
            if (transparent) {
                colorFlag = RENDER_PASSES.COLOR_TRANSPARENT;
            } else {
                colorFlag = RENDER_PASSES.COLOR_OPAQUE;
            }
        }

        let silhouetteFlag;
        if (!visible || culled) {
            silhouetteFlag = RENDER_PASSES.NOT_RENDERED;
        } else if (selected) {
            silhouetteFlag = RENDER_PASSES.SILHOUETTE_SELECTED;
        } else if (highlighted) {
            silhouetteFlag = RENDER_PASSES.SILHOUETTE_HIGHLIGHTED;
        } else if (xrayed) {
            silhouetteFlag = RENDER_PASSES.SILHOUETTE_XRAYED;
        } else {
            silhouetteFlag = RENDER_PASSES.NOT_RENDERED;
        }

        let pickFlag = (visible && !culled && pickable) ? RENDER_PASSES.PICK : RENDER_PASSES.NOT_RENDERED;

        const clippableFlag = !!(flags & ENTITY_FLAGS.CLIPPABLE) ? 1 : 0;

        if (deferred) {
            // Avoid zillions of individual WebGL bufferSubData calls - buffer them to apply in one shot
            if (!this._deferredFlagValues) {
                this._deferredFlagValues = new Float32Array(this._numVerts);
            }
            for (let i = firstFlag, len = (firstFlag + lenFlags); i < len; i++) {
                let vertFlag = 0;
                vertFlag |= colorFlag;
                vertFlag |= silhouetteFlag << 4;
                // no edges
                vertFlag |= pickFlag << 12;
                vertFlag |= clippableFlag << 16;

                this._deferredFlagValues[i] = vertFlag;
            }
        } else if (this._state.flagsBuf) {
            const tempArray = this._scratchMemory.getFloat32Array(lenFlags);
            for (let i = 0; i < lenFlags; i++) {
                let vertFlag = 0;
                vertFlag |= colorFlag;
                vertFlag |= silhouetteFlag << 4;
                // no edges
                vertFlag |= pickFlag << 12;
                vertFlag |= clippableFlag << 16;

                tempArray[i] = vertFlag;
            }
            this._state.flagsBuf.setData(tempArray, firstFlag, lenFlags);
        }
    }

    _setDeferredFlags() {
        if (this._deferredFlagValues) {
            this._state.flagsBuf.setData(this._deferredFlagValues);
            this._deferredFlagValues = null;
        }
    }

    setOffset(portionId, offset) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (!this.model.scene.entityOffsetsEnabled) {
            this.model.error("Entity#offset not enabled for this Viewer"); // See Viewer entityOffsetsEnabled
            return;
        }
        const portionsIdx = portionId * 2;
        const vertexBase = this._portions[portionsIdx];
        const numVerts = this._portions[portionsIdx + 1];
        const firstOffset = vertexBase * 3;
        const lenOffsets = numVerts * 3;
        const tempArray = this._scratchMemory.getFloat32Array(lenOffsets);
        const x = offset[0];
        const y = offset[1];
        const z = offset[2];
        for (let i = 0; i < lenOffsets; i += 3) {
            tempArray[i + 0] = x;
            tempArray[i + 1] = y;
            tempArray[i + 2] = z;
        }
        this._state.offsetsBuf.setData(tempArray, firstOffset, lenOffsets);
    }

    //-- RENDERING ----------------------------------------------------------------------------------------------

    drawColorOpaque(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        if (this._renderers.colorRenderer) {
            this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
        }
    }

    drawColorTransparent(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === 0 || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        if (this._renderers.colorRenderer) {
            this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT);
        }
    }

    drawDepth(renderFlags, frameCtx) {
    }

    drawNormals(renderFlags, frameCtx) {
    }

    drawSilhouetteXRayed(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) {
            return;
        }
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_XRAYED);
        }
    }

    drawSilhouetteHighlighted(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) {
            return;
        }
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_HIGHLIGHTED);
        }
    }

    drawSilhouetteSelected(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) {
            return;
        }
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_SELECTED);
        }
    }

    drawEdgesColorOpaque(renderFlags, frameCtx) {
    }

    drawEdgesColorTransparent(renderFlags, frameCtx) {
    }

    drawEdgesHighlighted(renderFlags, frameCtx) {
    }

    drawEdgesSelected(renderFlags, frameCtx) {
    }

    drawEdgesXRayed(renderFlags, frameCtx) {
    }

    drawPickMesh(frameCtx) {
    }

    drawPickDepths(frameCtx) {
    }

    drawPickNormals(frameCtx) {
    }

    drawSnapInit(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        if (this._renderers.snapInitRenderer) {
            this._renderers.snapInitRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawSnap(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        if (this._renderers.snapRenderer) {
            this._renderers.snapRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawOcclusion(frameCtx) {
    }

    drawShadow(frameCtx) {
    }

    destroy() {
        const state = this._state;
        if (state.positionsBuf) {
            state.positionsBuf.destroy();
            state.positionsBuf = null;
        }
        if (state.offsetsBuf) {
            state.offsetsBuf.destroy();
            state.offsetsBuf = null;
        }
        if (state.colorsBuf) {
            state.colorsBuf.destroy();
            state.colorsBuf = null;
        }
        if (state.flagsBuf) {
            state.flagsBuf.destroy();
            state.flagsBuf = null;
        }
        if (state.indicesBuf) {
            state.indicesBuf.destroy();
            state.indicesBuf = null;
        }
        state.destroy();
    }
}

/**
 * @private
 */
class VBOInstancingLinesRenderer extends VBORenderer {
    constructor(scene, withSAO) {
        super(scene, withSAO, {instancing: true});
    }

    _draw(drawCfg) {
        const {gl} = this._scene.canvas;

        const {
            state,
            frameCtx,
            incrementDrawState,
        } = drawCfg;

        gl.drawElementsInstanced(gl.LINES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0, state.numInstances);

        if (incrementDrawState) {
            frameCtx.drawElements++;
        }
    }
}

/**
 * @private
 */
class VBOInstancingLinesColorRenderer extends VBOInstancingLinesRenderer {

    drawLayer(frameCtx, layer, renderPass) {
        super.drawLayer(frameCtx, layer, renderPass, {incrementDrawState: true});
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Lines instancing color vertex shader");
        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        src.push("in vec4 color;");
        src.push("in float flags;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        this._addMatricesUniformBlockLines(src);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
        }

        src.push("uniform vec4 lightAmbient;");

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec4 vColor;");

        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE | COLOR_TRANSPARENT

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");

        src.push("vec4 worldPosition =  positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }

        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");

        src.push("vColor = vec4(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0,  float(color.a) / 255.0);");

        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
        }
        src.push("gl_Position = clipPos;");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        let i;
        let len;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Lines instancing color fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }

        // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top
        // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject

        if (this._withSAO) {
            src.push("   float viewportWidth     = uSAOParams[0];");
            src.push("   float viewportHeight    = uSAOParams[1];");
            src.push("   float blendCutoff       = uSAOParams[2];");
            src.push("   float blendFactor       = uSAOParams[3];");
            src.push("   vec2 uv                 = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);");
            src.push("   float ambient           = smoothstep(blendCutoff, 1.0, unpackRGBAToDepth(texture(uOcclusionTexture, uv))) * blendFactor;");
            src.push("   outColor            = vec4(vColor.rgb * ambient, vColor.a);");
        } else {
            src.push("    outColor           = vColor;");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class VBOInstancingLinesSilhouetteRenderer extends VBOInstancingLinesRenderer {

    drawLayer(frameCtx, instancingLayer, renderPass) {
        super.drawLayer(frameCtx, instancingLayer, renderPass, { colorUniform: true });
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Lines instancing silhouette vertex shader");
        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");

        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        this._addMatricesUniformBlockLines(src);

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
        }

        src.push("uniform vec4 color;");

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }

        src.push("void main(void) {");

        // silhouetteFlag = NOT_RENDERED | SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED
        // renderPass = SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED

        src.push(`int silhouetteFlag = int(flags) >> 4 & 0xF;`);
        src.push(`if (silhouetteFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");

        src.push("vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");

        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
        }
        src.push("gl_Position = clipPos;");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Lines instancing silhouette fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("uniform vec4 color;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outColor = color;");
        src.push("}");
        return src;
    }
}

const tempVec3a$w = math.vec3();
const tempVec3b$s = math.vec3();
const tempVec3c$n = math.vec3();
math.vec3();
const tempMat4a$k = math.mat4();

/**
 * @private
 */
class VBOInstancingLinesSnapInitRenderer extends VBORenderer {

    constructor(scene) {
        super(scene, false, { instancing: true });
    }

    drawLayer(frameCtx, instancingLayer, renderPass) {

        if (!this._program) {
            this._allocate();
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram();
        }

        const model = instancingLayer.model;
        const scene = model.scene;
        const gl = scene.canvas.gl;
        const camera = scene.camera;
        const state = instancingLayer._state;
        const origin = instancingLayer._state.origin;
        const {position, rotationMatrix} = model;
        const aabb = instancingLayer.aabb; // Per-layer AABB for best RTC accuracy
        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;

        if (this._vaoCache.has(instancingLayer)) {
            gl.bindVertexArray(this._vaoCache.get(instancingLayer));
        } else {
            this._vaoCache.set(instancingLayer, this._makeVAO(state));
        }

        const coordinateScaler = tempVec3a$w;
        coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT;
        coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT;
        coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT;

        frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]);
        frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]);
        frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]);

        let rtcViewMatrix;
        if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) {
            const rtcOrigin = tempVec3b$s;
            if (origin) {
                const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3c$n);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$k);
            frameCtx.snapPickOrigin[0] = rtcOrigin[0];
            frameCtx.snapPickOrigin[1] = rtcOrigin[1];
            frameCtx.snapPickOrigin[2] = rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            frameCtx.snapPickOrigin[0] = 0;
            frameCtx.snapPickOrigin[1] = 0;
            frameCtx.snapPickOrigin[2] = 0;
        }

        gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA);
        gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB);
        gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber);
        gl.uniform3fv(this._uCoordinateScaler, coordinateScaler);
        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible);

        let offset = 0;
        const mat4Size = 4 * 4;

        this._matricesUniformBlockBufferData.set(rotationMatrix, 0);
        this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size);

        gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer);
        gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW);

        gl.bindBufferBase(
            gl.UNIFORM_BUFFER,
            this._matricesUniformBlockBufferBindingPoint,
            this._matricesUniformBlockBuffer);


        {
            const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }

        this.setSectionPlanesStateUniforms(instancingLayer);

        state.indicesBuf.bind();
        gl.drawElementsInstanced(gl.LINES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0, state.numInstances);
        state.indicesBuf.unbind();

        gl.bindVertexArray(null);
    }

    _allocate() {
        super._allocate();

        const program = this._program;

        {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
        this.uVectorA = program.getLocation("snapVectorA");
        this.uInverseVectorAB = program.getLocation("snapInvVectorAB");
        this._uLayerNumber = program.getLocation("layerNumber");
        this._uCoordinateScaler = program.getLocation("coordinateScaler");
    }

    _bindProgram() {
        this._program.bind();

    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// SnapInstancingDepthBufInitRenderer vertex shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");
        src.push("uniform int renderPass;");
        src.push("in vec4 pickColor;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");
        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec2 snapVectorA;");
        src.push("uniform vec2 snapInvVectorAB;");
        {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }
        src.push("vec2 remapClipPos(vec2 clipPos) {");
        src.push("    float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;");
        src.push("    float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;");
        src.push("    return vec2(x, y);");
        src.push("}");
        src.push("flat out vec4 vPickColor;");
        src.push("out vec4 vWorldPosition;");
        if (clipping) {
            src.push("out float vFlags;");
        }
        src.push("out highp vec3 relativeToOriginPosition;");
        src.push("void main(void) {");
        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK
        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("} else {");
        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("relativeToOriginPosition = worldPosition.xyz;");
        src.push("  vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("  vWorldPosition = worldPosition;");
        if (clipping) {
            src.push("  vFlags = flags;");
        }
        src.push("vPickColor = pickColor;");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        src.push("float tmp = clipPos.w;");
        src.push("clipPos.xyzw /= tmp;");
        src.push("clipPos.xy = remapClipPos(clipPos.xy);");
        src.push("clipPos.xyzw *= tmp;");
        {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// Points instancing pick depth fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("uniform int layerNumber;");
        src.push("uniform vec3 coordinateScaler;");
        src.push("in vec4 vWorldPosition;");
        src.push("flat in vec4 vPickColor;");
        if (clipping) {
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in highp vec3 relativeToOriginPosition;");
        src.push("layout(location = 0) out highp ivec4 outCoords;");
        ///////////////////////////////////////////
        // TODO: normal placeholder?
        // Primitive type?
        ///////////////////////////////////////////
        src.push("layout(location = 2) out lowp uvec4 outPickColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        {
            src.push("    float dx = dFdx(vFragDepth);");
            src.push("    float dy = dFdy(vFragDepth);");
            src.push("    float diff = sqrt(dx*dx+dy*dy);");
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth + diff ) * logDepthBufFC * 0.5;");
        }
        src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, -layerNumber);");
        src.push("outPickColor = uvec4(vPickColor);");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

const tempVec3a$v = math.vec3();
const tempVec3b$r = math.vec3();
const tempVec3c$m = math.vec3();
math.vec3();
const tempMat4a$j = math.mat4();

/**
 * @private
 */
class VBOInstancingLinesSnapRenderer extends VBORenderer {

    constructor(scene) {
        super(scene, false, { instancing: true });
    }

    drawLayer(frameCtx, instancingLayer, renderPass) {

        if (!this._program) {
            this._allocate(instancingLayer);
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram();
        }

        const model = instancingLayer.model;
        const scene = model.scene;
        const camera = scene.camera;
        const gl = scene.canvas.gl;
        const state = instancingLayer._state;
        const origin = instancingLayer._state.origin;
        const {position, rotationMatrix} = model;
        const aabb = instancingLayer.aabb; // Per-layer AABB for best RTC accuracy
        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;

        if (this._vaoCache.has(instancingLayer)) {
            gl.bindVertexArray(this._vaoCache.get(instancingLayer));
        } else {
            this._vaoCache.set(instancingLayer, this._makeVAO(state));
        }

        const coordinateScaler = tempVec3a$v;
        coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT;
        coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT;
        coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT;

        frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]);
        frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]);
        frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]);

        let rtcViewMatrix;

        if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) {
            const rtcOrigin = tempVec3b$r;
            if (origin) {
                const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3c$m);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$j);
            frameCtx.snapPickOrigin[0] = rtcOrigin[0];
            frameCtx.snapPickOrigin[1] = rtcOrigin[1];
            frameCtx.snapPickOrigin[2] = rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            frameCtx.snapPickOrigin[0] = 0;
            frameCtx.snapPickOrigin[1] = 0;
            frameCtx.snapPickOrigin[2] = 0;
        }

        gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA);
        gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB);
        gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber);
        gl.uniform3fv(this._uCoordinateScaler, coordinateScaler);
        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible);

        let offset = 0;
        const mat4Size = 4 * 4;

        this._matricesUniformBlockBufferData.set(rotationMatrix, 0);
        this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size);

        gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer);
        gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW);

        gl.bindBufferBase(
            gl.UNIFORM_BUFFER,
            this._matricesUniformBlockBufferBindingPoint,
            this._matricesUniformBlockBuffer);

        {
            const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }

        this.setSectionPlanesStateUniforms(instancingLayer);

        if (frameCtx.snapMode === "edge") {
            state.indicesBuf.bind();
            gl.drawElementsInstanced(gl.LINES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0, state.numInstances);
            state.indicesBuf.unbind(); // needed?
        } else {
            gl.drawArraysInstanced(gl.POINTS, 0, state.positionsBuf.numItems, state.numInstances);
        }

        gl.bindVertexArray(null);
    }

    _allocate() {
        super._allocate();

        const program = this._program;

        {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
        this.uVectorA = program.getLocation("snapVectorA");
        this.uInverseVectorAB = program.getLocation("snapInvVectorAB"); 
        this._uLayerNumber = program.getLocation("layerNumber"); 
        this._uCoordinateScaler = program.getLocation("coordinateScaler"); 
    }

    _bindProgram() {
        this._program.bind();

    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// SnapInstancingDepthRenderer vertex shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");
        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");
        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec2 snapVectorA;"); 
        src.push("uniform vec2 snapInvVectorAB;"); 
        {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }
        src.push("vec2 remapClipPos(vec2 clipPos) {");
        src.push("    float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;");
        src.push("    float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;");
        src.push("    return vec2(x, y);");
        src.push("}");
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out highp vec3 relativeToOriginPosition;");
        src.push("void main(void) {");
        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK
        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("   gl_Position = vec4(2.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("} else {");
        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("relativeToOriginPosition = worldPosition.xyz;");
        src.push("  vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("  vWorldPosition = worldPosition;");
            src.push("  vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        src.push("float tmp = clipPos.w;");
        src.push("clipPos.xyzw /= tmp;");
        src.push("clipPos.xy = remapClipPos(clipPos.xy);");
        src.push("clipPos.xyzw *= tmp;");
        {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("gl_PointSize = 1.0;"); // Windows needs this?
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// SnapInstancingDepthRenderer fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("uniform int layerNumber;"); 
        src.push("uniform vec3 coordinateScaler;"); 
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in highp vec3 relativeToOriginPosition;");
        src.push("out highp ivec4 outCoords;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, layerNumber);");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

/**
 * @private
 */
class VBOInstancingLinesRenderers {

    constructor(scene) {
        this._scene = scene;
    }

    _compile() {
        if (this._colorRenderer && (!this._colorRenderer.getValid())) {
            this._colorRenderer.destroy();
            this._colorRenderer = null;
        }
        if (this._silhouetteRenderer && (!this._silhouetteRenderer.getValid())) {
            this._silhouetteRenderer.destroy();
            this._silhouetteRenderer = null;
        }
        if (this._snapInitRenderer && (!this._snapInitRenderer.getValid())) {
            this._snapInitRenderer.destroy();
            this._snapInitRenderer = null;
        }
        if (this._snapRenderer && (!this._snapRenderer.getValid())) {
            this._snapRenderer.destroy();
            this._snapRenderer = null;
        }
    }

    eagerCreateRenders() {

        // Pre-initialize renderers that would otherwise be lazy-initialised
        // on user interaction, such as picking or emphasis, so that there is no delay
        // when user first begins interacting with the viewer.

        if (!this._snapInitRenderer) {
            this._snapInitRenderer = new VBOInstancingLinesSnapInitRenderer(this._scene, false);
        }
        if (!this._snapRenderer) {
            this._snapRenderer = new VBOInstancingLinesSnapRenderer(this._scene);
        }
    }

    get colorRenderer() {
        if (!this._colorRenderer) {
            this._colorRenderer = new VBOInstancingLinesColorRenderer(this._scene);
        }
        return this._colorRenderer;
    }

    get silhouetteRenderer() {
        if (!this._silhouetteRenderer) {
            this._silhouetteRenderer = new VBOInstancingLinesSilhouetteRenderer(this._scene);
        }
        return this._silhouetteRenderer;
    }

    get snapInitRenderer() {
        if (!this._snapInitRenderer) {
            this._snapInitRenderer = new VBOInstancingLinesSnapInitRenderer(this._scene, false);
        }
        return this._snapInitRenderer;
    }

    get snapRenderer() {
        if (!this._snapRenderer) {
            this._snapRenderer = new VBOInstancingLinesSnapRenderer(this._scene);
        }
        return this._snapRenderer;
    }


    _destroy() {
        if (this._colorRenderer) {
            this._colorRenderer.destroy();
        }
        if (this._silhouetteRenderer) {
            this._silhouetteRenderer.destroy();
        }
        if (this._snapInitRenderer) {
            this._snapInitRenderer.destroy();
        }
        if (this._snapRenderer) {
            this._snapRenderer.destroy();
        }
    }
}

const cachedRenderers$3 = {};

/**
 * @private
 */
function getRenderers$4(scene) {
    const sceneId = scene.id;
    let instancingRenderers = cachedRenderers$3[sceneId];
    if (!instancingRenderers) {
        instancingRenderers = new VBOInstancingLinesRenderers(scene);
        cachedRenderers$3[sceneId] = instancingRenderers;
        instancingRenderers._compile();
        scene.on("compile", () => {
            instancingRenderers._compile();
        });
        scene.on("destroyed", () => {
            delete cachedRenderers$3[sceneId];
            instancingRenderers._destroy();
        });
    }
    return instancingRenderers;
}

/*

function getSnapInstancingRenderers(scene) {
    const sceneId = scene.id;
    let instancingRenderers = cachedRenderers[sceneId];
    if (!instancingRenderers) {
        instancingRenderers = new VBOInstancingLineSnapRenderers(scene);
        cachedRenderers[sceneId] = instancingRenderers;
        instancingRenderers._compile();
        instancingRenderers.eagerCreateRenders();
        scene.on("compile", () => {
            instancingRenderers._compile();
            instancingRenderers.eagerCreateRenders();
        });
        scene.on("destroyed", () => {
            delete cachedRenderers[sceneId];
            instancingRenderers._destroy();
        });
    }
    return instancingRenderers;
}

 */

const tempUint8Vec4$1 = new Uint8Array(4);
const tempFloat32$1 = new Float32Array(1);

const tempVec3fa$1 = new Float32Array(3);

const tempFloat32Vec4$1 = new Float32Array(4);

/**
 * @private
 */
class VBOInstancingLinesLayer {

    /**
     * @param cfg
     * @param cfg.layerIndex
     * @param cfg.model
     * @param cfg.geometry
     * @param cfg.material
     * @param cfg.origin
     */
    constructor(cfg) {

     //   console.info("VBOInstancingLinesLayer");

        /**
         * Owner model
         * @type {VBOSceneModel}
         */
        this.model = cfg.model;

        /**
         * Shared material
         * @type {VBOSceneModelGeometry}
         */
        this.material = cfg.material;

        /**
         * State sorting key.
         * @type {string}
         */
        this.sortId = "LinesInstancingLayer";

        /**
         * Index of this InstancingLayer in VBOSceneModel#_layerList
         * @type {Number}
         */
        this.layerIndex = cfg.layerIndex;

        this._renderers = getRenderers$4(cfg.model.scene);

        this._aabb = math.collapseAABB3();

        this._state = new RenderState({
            obb: math.OBB3(),
            numInstances: 0,
            origin: null,
            geometry: cfg.geometry,
            positionsDecodeMatrix: cfg.geometry.positionsDecodeMatrix, // So we can null the geometry for GC
            positionsBuf: null,
            colorsBuf: null,
            flagsBuf: null,
            offsetsBuf: null,
            modelMatrixCol0Buf: null,
            modelMatrixCol1Buf: null,
            modelMatrixCol2Buf: null
        });

        // These counts are used to avoid unnecessary render passes
        this._numPortions = 0;
        this._numVisibleLayerPortions = 0;
        this._numTransparentLayerPortions = 0;
        this._numXRayedLayerPortions = 0;
        this._numHighlightedLayerPortions = 0;
        this._numSelectedLayerPortions = 0;
        this._numClippableLayerPortions = 0;
        this._numEdgesLayerPortions = 0;
        this._numPickableLayerPortions = 0;
        this._numCulledLayerPortions = 0;

        /** @private */
        this.numIndices = cfg.geometry.numIndices;

        // Vertex arrays
        this._colors = [];
        this._offsets = [];

        // Modeling matrix per instance, array for each column
        this._modelMatrixCol0 = [];
        this._modelMatrixCol1 = [];
        this._modelMatrixCol2 = [];

        this._portions = [];
        this._meshes = [];

        this._aabb = math.collapseAABB3();
        this.aabbDirty = true;

        if (cfg.origin) {
            this._state.origin = math.vec3(cfg.origin);
        }

        this._finalized = false;

        /**
         * The type of primitives in this layer.
         */
        this.primitive = cfg.primitive;
    }

    get aabb() {
        if (this.aabbDirty) {
            math.collapseAABB3(this._aabb);
            for (let i = 0, len = this._meshes.length; i < len; i++) {
                math.expandAABB3(this._aabb, this._meshes[i].aabb);
            }
            this.aabbDirty = false;
        }
        return this._aabb;
    }

    /**
     * Creates a new portion within this InstancingLayer, returns the new portion ID.
     *
     * The portion will instance this InstancingLayer's geometry.
     *
     * Gives the portion the specified color and matrix.
     *
     * @param mesh The SceneModelMesh that owns the portion
     * @param cfg Portion params
     * @param cfg.color Color [0..255,0..255,0..255]
     * @param cfg.opacity Opacity [0..255].
     * @param cfg.meshMatrix Flat float 4x4 matrix.
     * @returns {number} Portion ID.
     */
    createPortion(mesh, cfg) {

        const color = cfg.color;
        const opacity = cfg.opacity;
        const meshMatrix = cfg.meshMatrix;

        if (this._finalized) {
            throw "Already finalized";
        }

        const r = color[0]; // Color is pre-quantized by VBOSceneModel
        const g = color[1];
        const b = color[2];
        color[3];

        this._colors.push(r);
        this._colors.push(g);
        this._colors.push(b);
        this._colors.push(opacity);

        if (this.model.scene.entityOffsetsEnabled) {
            this._offsets.push(0);
            this._offsets.push(0);
            this._offsets.push(0);
        }

        this._modelMatrixCol0.push(meshMatrix[0]);
        this._modelMatrixCol0.push(meshMatrix[4]);
        this._modelMatrixCol0.push(meshMatrix[8]);
        this._modelMatrixCol0.push(meshMatrix[12]);

        this._modelMatrixCol1.push(meshMatrix[1]);
        this._modelMatrixCol1.push(meshMatrix[5]);
        this._modelMatrixCol1.push(meshMatrix[9]);
        this._modelMatrixCol1.push(meshMatrix[13]);

        this._modelMatrixCol2.push(meshMatrix[2]);
        this._modelMatrixCol2.push(meshMatrix[6]);
        this._modelMatrixCol2.push(meshMatrix[10]);
        this._modelMatrixCol2.push(meshMatrix[14]);

        this._state.numInstances++;

        const portionId = this._portions.length;
        this._portions.push({});

        this._numPortions++;
        this.model.numPortions++;

        this._meshes.push(mesh);

        return portionId;
    }

    finalize() {
        if (this._finalized) {
            throw "Already finalized";
        }
        const gl = this.model.scene.canvas.gl;
        const state = this._state;
        const geometry = state.geometry;
        const colorsLength = this._colors.length;
        const flagsLength = colorsLength / 4;
        if (colorsLength > 0) {
            let notNormalized = false;
            this._state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Uint8Array(this._colors), this._colors.length, 4, gl.DYNAMIC_DRAW, notNormalized);
            this._colors = []; // Release memory
        }
        if (flagsLength > 0) {
            // Because we only build flags arrays here, 
            // get their length from the colors array
            let notNormalized = false;
            this._state.flagsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(flagsLength), flagsLength, 1, gl.DYNAMIC_DRAW, notNormalized);
        }
        if (this.model.scene.entityOffsetsEnabled) {
            if (this._offsets.length > 0) {
                const notNormalized = false;
                this._state.offsetsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._offsets), this._offsets.length, 3, gl.DYNAMIC_DRAW, notNormalized);
                this._offsets = []; // Release memory
            }
        }
        if (geometry.colorsCompressed && geometry.colorsCompressed.length > 0) {
            const colorsCompressed = new Uint8Array(geometry.colorsCompressed);
            const notNormalized = false;
            state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, colorsCompressed, colorsCompressed.length, 4, gl.STATIC_DRAW, notNormalized);
        }
        if (geometry.positionsCompressed && geometry.positionsCompressed.length > 0) {
            const normalized = false;
            state.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, geometry.positionsCompressed, geometry.positionsCompressed.length, 3, gl.STATIC_DRAW, normalized);
            state.positionsDecodeMatrix = math.mat4(geometry.positionsDecodeMatrix);
        }
        if (geometry.indices && geometry.indices.length > 0) {
            state.indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(geometry.indices), geometry.indices.length, 1, gl.STATIC_DRAW);
            state.numIndices = geometry.indices.length;
        }
        if (this._modelMatrixCol0.length > 0) {
            const normalized = false;
            this._state.modelMatrixCol0Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelMatrixCol0), this._modelMatrixCol0.length, 4, gl.STATIC_DRAW, normalized);
            this._state.modelMatrixCol1Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelMatrixCol1), this._modelMatrixCol1.length, 4, gl.STATIC_DRAW, normalized);
            this._state.modelMatrixCol2Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelMatrixCol2), this._modelMatrixCol2.length, 4, gl.STATIC_DRAW, normalized);
            this._modelMatrixCol0 = [];
            this._modelMatrixCol1 = [];
            this._modelMatrixCol2 = [];
        }
        if (!this.model.scene.readableGeometryEnabled) {
            this._state.geometry = null;
        }
        this._finalized = true;
    }

    // The following setters are called by VBOSceneModelMesh, in turn called by VBOSceneModelNode, only after the layer is finalized.
    // It's important that these are called after finalize() in order to maintain integrity of counts like _numVisibleLayerPortions etc.

    initFlags(portionId, flags, meshTransparent) {
        if (flags & ENTITY_FLAGS.VISIBLE) {
            this._numVisibleLayerPortions++;
            this.model.numVisibleLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.HIGHLIGHTED) {
            this._numHighlightedLayerPortions++;
            this.model.numHighlightedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.XRAYED) {
            this._numXRayedLayerPortions++;
            this.model.numXRayedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.SELECTED) {
            this._numSelectedLayerPortions++;
            this.model.numSelectedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.CLIPPABLE) {
            this._numClippableLayerPortions++;
            this.model.numClippableLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.EDGES) {
            this._numEdgesLayerPortions++;
            this.model.numEdgesLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.PICKABLE) {
            this._numPickableLayerPortions++;
            this.model.numPickableLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.CULLED) {
            this._numCulledLayerPortions++;
            this.model.numCulledLayerPortions++;
        }
        if (meshTransparent) {
            this._numTransparentLayerPortions++;
            this.model.numTransparentLayerPortions++;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setVisible(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.VISIBLE) {
            this._numVisibleLayerPortions++;
            this.model.numVisibleLayerPortions++;
        } else {
            this._numVisibleLayerPortions--;
            this.model.numVisibleLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setHighlighted(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.HIGHLIGHTED) {
            this._numHighlightedLayerPortions++;
            this.model.numHighlightedLayerPortions++;
        } else {
            this._numHighlightedLayerPortions--;
            this.model.numHighlightedLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setXRayed(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.XRAYED) {
            this._numXRayedLayerPortions++;
            this.model.numXRayedLayerPortions++;
        } else {
            this._numXRayedLayerPortions--;
            this.model.numXRayedLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setSelected(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.SELECTED) {
            this._numSelectedLayerPortions++;
            this.model.numSelectedLayerPortions++;
        } else {
            this._numSelectedLayerPortions--;
            this.model.numSelectedLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setEdges(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.EDGES) {
            this._numEdgesLayerPortions++;
            this.model.numEdgesLayerPortions++;
        } else {
            this._numEdgesLayerPortions--;
            this.model.numEdgesLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setClippable(portionId, flags) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.CLIPPABLE) {
            this._numClippableLayerPortions++;
            this.model.numClippableLayerPortions++;
        } else {
            this._numClippableLayerPortions--;
            this.model.numClippableLayerPortions--;
        }
        this._setFlags(portionId, flags);
    }

    setCollidable(portionId, flags) {
        if (!this._finalized) {
            throw "Not finalized";
        }
    }

    setPickable(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.PICKABLE) {
            this._numPickableLayerPortions++;
            this.model.numPickableLayerPortions++;
        } else {
            this._numPickableLayerPortions--;
            this.model.numPickableLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setCulled(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.CULLED) {
            this._numCulledLayerPortions++;
            this.model.numCulledLayerPortions++;
        } else {
            this._numCulledLayerPortions--;
            this.model.numCulledLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setColor(portionId, color) { // RGBA color is normalized as ints
        if (!this._finalized) {
            throw "Not finalized";
        }
        tempUint8Vec4$1[0] = color[0];
        tempUint8Vec4$1[1] = color[1];
        tempUint8Vec4$1[2] = color[2];
        tempUint8Vec4$1[3] = color[3];
        this._state.colorsBuf.setData(tempUint8Vec4$1, portionId * 4, 4);
    }

    setTransparent(portionId, flags, transparent) {
        if (transparent) {
            this._numTransparentLayerPortions++;
            this.model.numTransparentLayerPortions++;
        } else {
            this._numTransparentLayerPortions--;
            this.model.numTransparentLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    /**
     * flags are 4bits values encoded on a 32bit base. color flag on the first 4 bits, silhouette flag on the next 4 bits and so on for edge, pick and clippable.
     */
    _setFlags(portionId, flags, meshTransparent) {

        if (!this._finalized) {
            throw "Not finalized";
        }

        const visible = !!(flags & ENTITY_FLAGS.VISIBLE);
        const xrayed = !!(flags & ENTITY_FLAGS.XRAYED);
        const highlighted = !!(flags & ENTITY_FLAGS.HIGHLIGHTED);
        const selected = !!(flags & ENTITY_FLAGS.SELECTED);
        const edges = !!(flags & ENTITY_FLAGS.EDGES);
        const pickable = !!(flags & ENTITY_FLAGS.PICKABLE);
        const culled = !!(flags & ENTITY_FLAGS.CULLED);

        let colorFlag;
        if (!visible || culled || xrayed
            || (highlighted && !this.model.scene.highlightMaterial.glowThrough)
            || (selected && !this.model.scene.selectedMaterial.glowThrough)) {
            colorFlag = RENDER_PASSES.NOT_RENDERED;
        } else {
            if (meshTransparent) {
                colorFlag = RENDER_PASSES.COLOR_TRANSPARENT;
            } else {
                colorFlag = RENDER_PASSES.COLOR_OPAQUE;
            }
        }

        let silhouetteFlag;
        if (!visible || culled) {
            silhouetteFlag = RENDER_PASSES.NOT_RENDERED;
        } else if (selected) {
            silhouetteFlag = RENDER_PASSES.SILHOUETTE_SELECTED;
        } else if (highlighted) {
            silhouetteFlag = RENDER_PASSES.SILHOUETTE_HIGHLIGHTED;
        } else if (xrayed) {
            silhouetteFlag = RENDER_PASSES.SILHOUETTE_XRAYED;
        } else {
            silhouetteFlag = RENDER_PASSES.NOT_RENDERED;
        }

        let edgeFlag = 0;
        if (!visible || culled) {
            edgeFlag = RENDER_PASSES.NOT_RENDERED;
        } else if (selected) {
            edgeFlag = RENDER_PASSES.EDGES_SELECTED;
        } else if (highlighted) {
            edgeFlag = RENDER_PASSES.EDGES_HIGHLIGHTED;
        } else if (xrayed) {
            edgeFlag = RENDER_PASSES.EDGES_XRAYED;
        } else if (edges) {
            if (meshTransparent) {
                edgeFlag = RENDER_PASSES.EDGES_COLOR_TRANSPARENT;
            } else {
                edgeFlag = RENDER_PASSES.EDGES_COLOR_OPAQUE;
            }
        } else {
            edgeFlag = RENDER_PASSES.NOT_RENDERED;
        }

        let pickFlag = (visible && !culled && pickable) ? RENDER_PASSES.PICK : RENDER_PASSES.NOT_RENDERED;

        const clippableFlag = !!(flags & ENTITY_FLAGS.CLIPPABLE) ? 1 : 0;

        let vertFlag = 0;
        vertFlag |= colorFlag;
        vertFlag |= silhouetteFlag << 4;
        vertFlag |= edgeFlag << 8;
        vertFlag |= pickFlag << 12;
        vertFlag |= clippableFlag << 16;

        tempFloat32$1[0] = vertFlag;

        this._state.flagsBuf.setData(tempFloat32$1, portionId);
    }

    setOffset(portionId, offset) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (!this.model.scene.entityOffsetsEnabled) {
            this.model.error("Entity#offset not enabled for this Viewer"); // See Viewer entityOffsetsEnabled
            return;
        }
        tempVec3fa$1[0] = offset[0];
        tempVec3fa$1[1] = offset[1];
        tempVec3fa$1[2] = offset[2];
        this._state.offsetsBuf.setData(tempVec3fa$1, portionId * 3, 3);
    }

    setMatrix(portionId, matrix) {

        ////////////////////////////////////////
        // TODO: Update portion matrix
        ////////////////////////////////////////

        if (!this._finalized) {
            throw "Not finalized";
        }
        const offset = portionId * 4;

        tempFloat32Vec4$1[0] = matrix[0];
        tempFloat32Vec4$1[1] = matrix[4];
        tempFloat32Vec4$1[2] = matrix[8];
        tempFloat32Vec4$1[3] = matrix[12];

        this._state.modelMatrixCol0Buf.setData(tempFloat32Vec4$1, offset);

        tempFloat32Vec4$1[0] = matrix[1];
        tempFloat32Vec4$1[1] = matrix[5];
        tempFloat32Vec4$1[2] = matrix[9];
        tempFloat32Vec4$1[3] = matrix[13];

        this._state.modelMatrixCol1Buf.setData(tempFloat32Vec4$1, offset);

        tempFloat32Vec4$1[0] = matrix[2];
        tempFloat32Vec4$1[1] = matrix[6];
        tempFloat32Vec4$1[2] = matrix[10];
        tempFloat32Vec4$1[3] = matrix[14];

        this._state.modelMatrixCol2Buf.setData(tempFloat32Vec4$1, offset);
    }

    // ---------------------- NORMAL RENDERING -----------------------------------

    drawColorOpaque(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }

        if (this._renderers.colorRenderer) {
            this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
        }
    }

    drawColorTransparent(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === 0 || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        if (this._renderers.colorRenderer) {
            this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT);
        }
    }

    // ---------------------- RENDERING SAO POST EFFECT TARGETS --------------

    drawDepth(renderFlags, frameCtx) {
    }

    drawNormals(renderFlags, frameCtx) {
    }

    // ---------------------- EMPHASIS RENDERING -----------------------------------

    drawSilhouetteXRayed(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) {
            return;
        }
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_XRAYED);
        }
    }

    drawSilhouetteHighlighted(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) {
            return;
        }
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_HIGHLIGHTED);
        }
    }

    drawSilhouetteSelected(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) {
            return;
        }
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_SELECTED);
        }
    }

    // ---------------------- EDGES RENDERING -----------------------------------

    drawEdgesColorOpaque(renderFlags, frameCtx) {
    }

    drawEdgesColorTransparent(renderFlags, frameCtx) {
    }

    drawEdgesXRayed(renderFlags, frameCtx) {
    }

    drawEdgesHighlighted(renderFlags, frameCtx) {
    }

    drawEdgesSelected(renderFlags, frameCtx) {
    }


    drawSnapInit(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        if (this._renderers.snapInitRenderer) {
            this._renderers.snapInitRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawSnap(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        if (this._renderers.snapRenderer) {
            this._renderers.snapRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    // ---------------------- OCCLUSION CULL RENDERING -----------------------------------

    drawOcclusion(renderFlags, frameCtx) {
    }

    // ---------------------- SHADOW BUFFER RENDERING -----------------------------------

    drawShadow(renderFlags, frameCtx) {
    }

    //---- PICKING ----------------------------------------------------------------------------------------------------

    drawPickMesh(renderFlags, frameCtx) {
    }

    drawPickDepths(renderFlags, frameCtx) {
    }

    drawPickNormals(renderFlags, frameCtx) {
    }


    destroy() {
        const state = this._state;
        if (state.positionsBuf) {
            state.positionsBuf.destroy();
            state.positionsBuf = null;
        }
        if (state.colorsBuf) {
            state.colorsBuf.destroy();
            state.colorsBuf = null;
        }
        if (state.flagsBuf) {
            state.flagsBuf.destroy();
            state.flagsBuf = null;
        }
        if (state.offsetsBuf) {
            state.offsetsBuf.destroy();
            state.offsetsBuf = null;
        }
        if (state.modelMatrixCol0Buf) {
            state.modelMatrixCol0Buf.destroy();
            state.modelMatrixCol0Buf = null;
        }
        if (state.modelMatrixCol1Buf) {
            state.modelMatrixCol1Buf.destroy();
            state.modelMatrixCol1Buf = null;
        }
        if (state.modelMatrixCol2Buf) {
            state.modelMatrixCol2Buf.destroy();
            state.modelMatrixCol2Buf = null;
        }
        state.destroy();
    }
}

/**
 * @private
 */


class VBOBatchingPointsRenderer extends VBORenderer {
    _draw(drawCfg) {
        const {gl} = this._scene.canvas;

        const {
            state,
            frameCtx,
            incrementDrawState,
        } = drawCfg;

        gl.drawArrays(gl.POINTS, 0, state.positionsBuf.numItems);

        if (incrementDrawState) {
            frameCtx.drawArrays++;
        }
    }
}

/**
 * @private
 */
class VBOBatchingPointsColorRenderer extends VBOBatchingPointsRenderer {
    _getHash() {
        return this._scene._sectionPlanesState.getHash() + this._scene.pointsMaterial.hash;
    }

    drawLayer(frameCtx, layer, renderPass) {
        super.drawLayer(frameCtx, layer, renderPass, { incrementDrawState: true });
    }

    _buildVertexShader() {

        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const pointsMaterial = scene.pointsMaterial;
        const src = [];
        src.push('#version 300 es');
        src.push("// Points batching color vertex shader");

        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        src.push("in vec4 color;");
        src.push("in float flags;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        this._addMatricesUniformBlockLines(src);

        src.push("uniform float pointSize;");
        if (pointsMaterial.perspectivePoints) {
            src.push("uniform float nearPlaneHeight;");
        }

        if (pointsMaterial.filterIntensity) {
            src.push("uniform vec2 intensityRange;");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec4 vColor;");

        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");

        if (pointsMaterial.filterIntensity) {
            src.push("float intensity = float(color.a) / 255.0;");
            src.push("if (intensity < intensityRange[0] || intensity > intensityRange[1]) {");
            src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
            src.push("} else {");
        }

        src.push("vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");

        src.push("vColor = vec4(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0, 1.0);");

        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
        }

        src.push("gl_Position = clipPos;");
        if (pointsMaterial.perspectivePoints) {
            src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;");
            src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);");
            src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);");
        } else {
            src.push("gl_PointSize = pointSize;");
        }
        src.push("}");
        if (pointsMaterial.filterIntensity) {
            src.push("}");
        }
        src.push("}");
        return src;
    }

    _buildFragmentShader() {

        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Points batching color fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (scene.pointsMaterial.roundPoints) {
            src.push("  vec2 cxy = 2.0 * gl_PointCoord - 1.0;");
            src.push("  float r = dot(cxy, cxy);");
            src.push("  if (r > 1.0) {");
            src.push("       discard;");
            src.push("  }");
        }
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > 0.0) { discard; }");
            src.push("}");
        }
        src.push("   outColor = vColor;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class VBOBatchingPointsSilhouetteRenderer extends VBOBatchingPointsRenderer {
    _getHash() {
        return this._scene._sectionPlanesState.getHash() + this._scene.pointsMaterial.hash;
    }

    drawLayer(frameCtx, pointsBatchingLayer, renderPass) {
        super.drawLayer(frameCtx, pointsBatchingLayer, renderPass, { colorUniform: true });
    }

    _buildVertexShader() {

        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const pointsMaterial = scene.pointsMaterial._state;
        const src = [];
        src.push ('#version 300 es');
        src.push("// Points batching silhouette vertex shader");
        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec4 color;");

        src.push("uniform float pointSize;");
        if (pointsMaterial.perspectivePoints) {
            src.push("uniform float nearPlaneHeight;");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }

        src.push("void main(void) {");

        // silhouetteFlag = NOT_RENDERED | SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | SILHOUETTE_XRAYED
        // renderPass = SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED

        src.push(`int silhouetteFlag = int(flags) >> 4 & 0xF;`);
        src.push(`if (silhouetteFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("} else {");

        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
           src.push("vFragDepth = 1.0 + clipPos.w;");
        }
        src.push("gl_Position = clipPos;");
        if (pointsMaterial.perspectivePoints) {
            src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;");
            src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);");
            src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);");
        } else {
            src.push("gl_PointSize = pointSize;");
        }
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        let i;
        let len;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// Points batching silhouette vertex shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("uniform vec4 color;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (scene.pointsMaterial.roundPoints) {
            src.push("  vec2 cxy = 2.0 * gl_PointCoord - 1.0;");
            src.push("  float r = dot(cxy, cxy);");
            src.push("  if (r > 1.0) {");
            src.push("       discard;");
            src.push("  }");
        }
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > 0.0) { discard; }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outColor = color;");
        src.push("}");
        return src;
    }
}

/**
 * @private
 */


class VBOBatchingPointsPickMeshRenderer extends VBOBatchingPointsRenderer {
    _getHash() {
        return this._scene._sectionPlanesState.getHash() + (this._scene.pointsMaterial.hash);
    }

    _buildVertexShader() {

        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const pointsMaterial = scene.pointsMaterial._state;
        const src = [];
        src.push ('#version 300 es');
        src.push("// Points batching pick mesh vertex shader");
        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");

        src.push("in vec4 pickColor;");

        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        this._addRemapClipPosLines(src);

        src.push("uniform float pointSize;");
        if (pointsMaterial.perspectivePoints) {
            src.push("uniform float nearPlaneHeight;");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }

        src.push("out vec4 vPickColor;");

        src.push("void main(void) {");

        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK

        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("  } else {");
        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("      vPickColor = vec4(float(pickColor.r) / 255.0, float(pickColor.g) / 255.0, float(pickColor.b) / 255.0, float(pickColor.a) / 255.0);");
        if (clipping) {
            src.push("      vWorldPosition = worldPosition;");
            src.push("      vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
           src.push("vFragDepth = 1.0 + clipPos.w;");
        }
        src.push("gl_Position = remapClipPos(clipPos);");
        if (pointsMaterial.perspectivePoints) {
            src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;");
            src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);");
            src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);");
        } else {
            src.push("gl_PointSize = pointSize;");
        }
        src.push("gl_PointSize += 10.0;");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// Points batching pick mesh vertex shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vPickColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (scene.pointsMaterial.roundPoints) {
            src.push("  vec2 cxy = 2.0 * gl_PointCoord - 1.0;");
            src.push("  float r = dot(cxy, cxy);");
            src.push("  if (r > 1.0) {");
            src.push("       discard;");
            src.push("  }");
        }
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("   outColor = vPickColor; ");
        src.push("}");
        return src;
    }
}

/**
 * @private
 */


class VBOBatchingPointsPickDepthRenderer extends VBOBatchingPointsRenderer {
    _getHash() {
        return this._scene._sectionPlanesState.getHash() + (this._scene.pointsMaterial.hash);
    }

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const pointsMaterial = scene.pointsMaterial._state;
        const src = [];
        src.push('#version 300 es');
        src.push("// Points batched pick depth vertex shader");
        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");

        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        this._addRemapClipPosLines(src);

        src.push("uniform float pointSize;");
        if (pointsMaterial.perspectivePoints) {
            src.push("uniform float nearPlaneHeight;");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec4 vViewPosition;");
        src.push("void main(void) {");

        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK

        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("      gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("  } else {");
        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("      vWorldPosition = worldPosition;");
            src.push("      vFlags = flags;");
        }
        src.push("vViewPosition = viewPosition;");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
        }
        src.push("gl_Position = remapClipPos(clipPos);");
        if (pointsMaterial.perspectivePoints) {
            src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;");
            src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);");
            src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);");
        } else {
            src.push("gl_PointSize = pointSize;");
        }
        src.push("gl_PointSize += 10.0;");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Points batched pick depth fragment shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }

        src.push("uniform float pickZNear;");
        src.push("uniform float pickZFar;");

        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vViewPosition;");
        src.push("vec4 packDepth(const in float depth) {");
        src.push("  const vec4 bitShift = vec4(256.0*256.0*256.0, 256.0*256.0, 256.0, 1.0);");
        src.push("  const vec4 bitMask  = vec4(0.0, 1.0/256.0, 1.0/256.0, 1.0/256.0);");
        src.push("  vec4 res = fract(depth * bitShift);");
        src.push("  res -= res.xxyz * bitMask;");
        src.push("  return res;");
        src.push("}");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (scene.pointsMaterial.roundPoints) {
            src.push("  vec2 cxy = 2.0 * gl_PointCoord - 1.0;");
            src.push("  float r = dot(cxy, cxy);");
            src.push("  if (r > 1.0) {");
            src.push("       discard;");
            src.push("  }");
        }
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("    float zNormalizedDepth = abs((pickZNear + vViewPosition.z) / (pickZFar - pickZNear));");
        src.push("    outColor = packDepth(zNormalizedDepth); ");  // Must be linear depth
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class VBOBatchingPointsOcclusionRenderer extends VBOBatchingPointsRenderer {
    _getHash() {
        return this._scene._sectionPlanesState.getHash() + (this._scene.pointsMaterial.hash);
    }

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const pointsMaterial = scene.pointsMaterial._state;
        const src = [];
        src.push('#version 300 es');
        src.push("// Points batching occlusion vertex shader");
        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform float pointSize;");
        if (pointsMaterial.perspectivePoints) {
            src.push("uniform float nearPlaneHeight;");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
        }
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE
        // Only opaque objects can be occluders

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("      gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("  } else {");
        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }

        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("      vWorldPosition = worldPosition;");
            src.push("      vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
        }
        src.push("  gl_Position = clipPos;");
        if (pointsMaterial.perspectivePoints) {
            src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;");
            src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);");
            src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);");
        } else {
            src.push("gl_PointSize = pointSize;");
        }
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Points batching occlusion fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (scene.pointsMaterial.roundPoints) {
            src.push("  vec2 cxy = 2.0 * gl_PointCoord - 1.0;");
            src.push("  float r = dot(cxy, cxy);");
            src.push("  if (r > 1.0) {");
            src.push("       discard;");
            src.push("  }");
        }
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("   outColor = vec4(0.0, 0.0, 1.0, 1.0); "); // Occluders are blue
        src.push("}");
        return src;
    }
}

const tempVec3a$u = math.vec3();
const tempVec3b$q = math.vec3();
const tempVec3c$l = math.vec3();
const tempVec3d$6 = math.vec3();
const tempMat4a$i = math.mat4();

/**
 * @private
 */
class VBOBatchingPointsSnapInitRenderer extends VBORenderer {
    drawLayer(frameCtx, batchingLayer, renderPass) {

        if (!this._program) {
            this._allocate();
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram();
        }

        const model = batchingLayer.model;
        const scene = model.scene;
        const camera = scene.camera;
        const gl = scene.canvas.gl;
        const state = batchingLayer._state;
        const origin = batchingLayer._state.origin;
        const {position, rotationMatrix} = model;
        const aabb = batchingLayer.aabb; // Per-layer AABB for best RTC accuracy
        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;

        if (this._vaoCache.has(batchingLayer)) {
            gl.bindVertexArray(this._vaoCache.get(batchingLayer));
        } else {
            this._vaoCache.set(batchingLayer, this._makeVAO(state));
        }

        const coordinateScaler = tempVec3a$u;
        coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT;
        coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT;
        coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT;

        frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]);
        frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]);
        frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]);

        let rtcViewMatrix;
        let rtcCameraEye;

        if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) {
            const rtcOrigin = tempVec3b$q;
            if (origin) {
                const rotatedOrigin = tempVec3c$l;
                math.transformPoint3(rotationMatrix, origin, rotatedOrigin);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$i);
            rtcCameraEye = tempVec3d$6;
            rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0];
            rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1];
            rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2];
            frameCtx.snapPickOrigin[0] = rtcOrigin[0];
            frameCtx.snapPickOrigin[1] = rtcOrigin[1];
            frameCtx.snapPickOrigin[2] = rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            rtcCameraEye = camera.eye;
            frameCtx.snapPickOrigin[0] = 0;
            frameCtx.snapPickOrigin[1] = 0;
            frameCtx.snapPickOrigin[2] = 0;
        }

        gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye);
        gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA);
        gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB);
        gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber);
        gl.uniform3fv(this._uCoordinateScaler, coordinateScaler);
        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible);
        gl.uniform1f(this._uPointSize, 1.0);

        let offset = 0;
        const mat4Size = 4 * 4;

        this._matricesUniformBlockBufferData.set(rotationMatrix, 0);
        this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size);

        gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer);
        gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW);

        gl.bindBufferBase(
            gl.UNIFORM_BUFFER,
            this._matricesUniformBlockBufferBindingPoint,
            this._matricesUniformBlockBuffer);

        {
            const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix?
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }

        this.setSectionPlanesStateUniforms(batchingLayer);
        //=============================================================
        // TODO: Use drawElements count and offset to draw only one entity
        //=============================================================

        gl.drawArrays(gl.POINTS, 0, state.positionsBuf.numItems);

        gl.bindVertexArray(null);
    }

    _allocate() {
        super._allocate();

        const program = this._program;

        {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
        this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc");
        this.uVectorA = program.getLocation("snapVectorA");
        this.uInverseVectorAB = program.getLocation("snapInvVectorAB");
        this._uLayerNumber = program.getLocation("layerNumber");
        this._uCoordinateScaler = program.getLocation("coordinateScaler");
        this._uPointSize = program.getLocation("pointSize");
    }

    _bindProgram() {
        this._program.bind();
    }

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// VBOBatchingPointsSnapInitRenderer vertex shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");
        src.push("uniform int renderPass;");
        src.push("in vec4 pickColor;");
        src.push("in vec3 position;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec3 uCameraEyeRtc;");
        src.push("uniform vec2 snapVectorA;");
        src.push("uniform vec2 snapInvVectorAB;");
        src.push("uniform float pointSize;");
        {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("out float isPerspective;");
        }
        src.push("bool isPerspectiveMatrix(mat4 m) {");
        src.push("    return (m[2][3] == - 1.0);");
        src.push("}");
        src.push("vec2 remapClipPos(vec2 clipPos) {");
        src.push("    float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;");
        src.push("    float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;");
        src.push("    return vec2(x, y);");
        src.push("}");
        src.push("flat out vec4 vPickColor;");
        src.push("out vec4 vWorldPosition;");
        if (clipping) {
            src.push("out float vFlags;");
        }
        src.push("out highp vec3 relativeToOriginPosition;");
        src.push("void main(void) {");
        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK
        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("      gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("  } else {");
        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("      relativeToOriginPosition = worldPosition.xyz;");
        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("      vWorldPosition = worldPosition;");
        if (clipping) {
            src.push("      vFlags = flags;");
        }
        src.push("vPickColor = pickColor;");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        src.push("float tmp = clipPos.w;");
        src.push("clipPos.xyzw /= tmp;");
        src.push("clipPos.xy = remapClipPos(clipPos.xy);");
        src.push("clipPos.xyzw *= tmp;");
        {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("gl_PointSize = pointSize;");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// VBOBatchingPointsSnapInitRenderer fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("uniform int layerNumber;");
        src.push("uniform vec3 coordinateScaler;");
        src.push("in vec4 vWorldPosition;");
        src.push("flat in vec4 vPickColor;");
        if (clipping) {
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in highp vec3 relativeToOriginPosition;");
        src.push("layout(location = 0) out highp ivec4 outCoords;");
        src.push("layout(location = 1) out highp ivec4 outNormal;");
        src.push("layout(location = 2) out lowp uvec4 outPickColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        {
            src.push("    float dx = dFdx(vFragDepth);");
            src.push("    float dy = dFdy(vFragDepth);");
            src.push("    float diff = sqrt(dx*dx+dy*dy);");
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth + diff ) * logDepthBufFC * 0.5;");
        }
        src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, -layerNumber);");

        // src.push("vec3 xTangent = dFdx( vWorldPosition.xyz );");
        // src.push("vec3 yTangent = dFdy( vWorldPosition.xyz );");
        // src.push("vec3 worldNormal = normalize( cross( xTangent, yTangent ) );");
         src.push(`outNormal = ivec4(1.0, 1.0, 1.0, 1.0);`);
        src.push("outPickColor = uvec4(vPickColor);");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

const tempVec3a$t = math.vec3();
const tempVec3b$p = math.vec3();
const tempVec3c$k = math.vec3();
const tempVec3d$5 = math.vec3();
const tempMat4a$h = math.mat4();

/**
 * @private
 */
class VBOBatchingPointsSnapRenderer extends VBORenderer{
    _getHash() {
        return this._scene._sectionPlanesState.getHash() + (this._scene.pointsMaterial.hash);
    }

    drawLayer(frameCtx, batchingLayer, renderPass) {

        if (!this._program) {
            this._allocate();
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram();
        }

        const model = batchingLayer.model;
        const scene = model.scene;
        const camera = scene.camera;
        const gl = scene.canvas.gl;
        const state = batchingLayer._state;
        const origin = batchingLayer._state.origin;
        const {position, rotationMatrix} = model;
        const aabb = batchingLayer.aabb; // Per-layer AABB for best RTC accuracy
        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;

        if (this._vaoCache.has(batchingLayer)) {
            gl.bindVertexArray(this._vaoCache.get(batchingLayer));
        } else {
            this._vaoCache.set(batchingLayer, this._makeVAO(state));
        }

        const coordinateScaler = tempVec3a$t;
        coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT;
        coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT;
        coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT;

        frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]);
        frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]);
        frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]);

        let rtcViewMatrix;
        let rtcCameraEye;

        if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) {
            const rtcOrigin = tempVec3b$p;
            if (origin) {
                const rotatedOrigin = tempVec3c$k;
                math.transformPoint3(rotationMatrix, origin, rotatedOrigin);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$h);
            rtcCameraEye = tempVec3d$5;
            rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0];
            rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1];
            rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2];
            frameCtx.snapPickOrigin[0] = rtcOrigin[0];
            frameCtx.snapPickOrigin[1] = rtcOrigin[1];
            frameCtx.snapPickOrigin[2] = rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            rtcCameraEye = camera.eye;
            frameCtx.snapPickOrigin[0] = 0;
            frameCtx.snapPickOrigin[1] = 0;
            frameCtx.snapPickOrigin[2] = 0;
        }

        gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye);
        gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA);
        gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB);
        gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber);
        gl.uniform3fv(this._uCoordinateScaler, coordinateScaler);
        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible);

        let offset = 0;
        const mat4Size = 4 * 4;

        this._matricesUniformBlockBufferData.set(rotationMatrix, 0);
        this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size);

        gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer);
        gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW);

        gl.bindBufferBase(
            gl.UNIFORM_BUFFER,
            this._matricesUniformBlockBufferBindingPoint,
            this._matricesUniformBlockBuffer);

        {
            const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix?
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }

        this.setSectionPlanesStateUniforms(batchingLayer);

        //=============================================================
        // TODO: Use drawElements count and offset to draw only one entity
        //=============================================================

        gl.drawArrays(gl.POINTS, 0, state.positionsBuf.numItems);

        gl.bindVertexArray(null);
    }

    _allocate() {
        super._allocate();

        const program = this._program;

        {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }

        this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); 
        this.uVectorA = program.getLocation("snapVectorA"); 
        this.uInverseVectorAB = program.getLocation("snapInvVectorAB"); 
        this._uLayerNumber = program.getLocation("layerNumber"); 
        this._uCoordinateScaler = program.getLocation("coordinateScaler"); 
    }

    _bindProgram() {
        this._program.bind();
    }

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        scene.pointsMaterial._state;
        const src = [];
        src.push ('#version 300 es');
        src.push("// VBOBatchingPointsSnapRenderer vertex shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");
        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec3 uCameraEyeRtc;"); 
        src.push("uniform vec2 snapVectorA;"); 
        src.push("uniform vec2 snapInvVectorAB;"); 
        {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }
        src.push("vec2 remapClipPos(vec2 clipPos) {");
        src.push("    float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;");
        src.push("    float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;");
        src.push("    return vec2(x, y);");
        src.push("}");
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out highp vec3 relativeToOriginPosition;");
        src.push("void main(void) {");

        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK

        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("      gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("  } else {");
        src.push("      vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("relativeToOriginPosition = worldPosition.xyz;");
        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("      vWorldPosition = worldPosition;");
            src.push("      vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        src.push("float tmp = clipPos.w;");
        src.push("clipPos.xyzw /= tmp;");
        src.push("clipPos.xy = remapClipPos(clipPos.xy);");
        src.push("clipPos.xyzw *= tmp;");
        {
           src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("gl_PointSize = 1.0;"); // Windows needs this?
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// VBOBatchingPointsSnapRenderer fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("uniform int layerNumber;"); 
        src.push("uniform vec3 coordinateScaler;"); 
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in highp vec3 relativeToOriginPosition;");
        src.push("out highp ivec4 outCoords;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, layerNumber);");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

/**
 * @private
 */
class VBOBatchingPointsRenderers {

    constructor(scene) {
        this._scene = scene;
    }

    _compile() {
        if (this._colorRenderer && (!this._colorRenderer.getValid())) {
            this._colorRenderer.destroy();
            this._colorRenderer = null;
        }
        if (this._silhouetteRenderer && (!this._silhouetteRenderer.getValid())) {
            this._silhouetteRenderer.destroy();
            this._silhouetteRenderer = null;
        }
        if (this._pickMeshRenderer && (!this._pickMeshRenderer.getValid())) {
            this._pickMeshRenderer.destroy();
            this._pickMeshRenderer = null;
        }
        if (this._pickDepthRenderer && (!this._pickDepthRenderer.getValid())) {
            this._pickDepthRenderer.destroy();
            this._pickDepthRenderer = null;
        }
        if (this._occlusionRenderer && this._occlusionRenderer.getValid() === false) {
            this._occlusionRenderer.destroy();
            this._occlusionRenderer = null;
        }
        if (this._snapInitRenderer && (!this._snapInitRenderer.getValid())) {
            this._snapInitRenderer.destroy();
            this._snapInitRenderer = null;
        }
        if (this._snapRenderer && (!this._snapRenderer.getValid())) {
            this._snapRenderer.destroy();
            this._snapRenderer = null;
        }
    }

    get colorRenderer() {
        if (!this._colorRenderer) {
            this._colorRenderer = new VBOBatchingPointsColorRenderer(this._scene);
        }
        return this._colorRenderer;
    }

    get silhouetteRenderer() {
        if (!this._silhouetteRenderer) {
            this._silhouetteRenderer = new VBOBatchingPointsSilhouetteRenderer(this._scene);
        }
        return this._silhouetteRenderer;
    }

    get pickMeshRenderer() {
        if (!this._pickMeshRenderer) {
            this._pickMeshRenderer = new VBOBatchingPointsPickMeshRenderer(this._scene);
        }
        return this._pickMeshRenderer;
    }

    get pickDepthRenderer() {
        if (!this._pickDepthRenderer) {
            this._pickDepthRenderer = new VBOBatchingPointsPickDepthRenderer(this._scene);
        }
        return this._pickDepthRenderer;
    }

    get occlusionRenderer() {
        if (!this._occlusionRenderer) {
            this._occlusionRenderer = new VBOBatchingPointsOcclusionRenderer(this._scene);
        }
        return this._occlusionRenderer;
    }

    get snapInitRenderer() {
        if (!this._snapInitRenderer) {
            this._snapInitRenderer = new VBOBatchingPointsSnapInitRenderer(this._scene, false);
        }
        return this._snapInitRenderer;
    }

    get snapRenderer() {
        if (!this._snapRenderer) {
            this._snapRenderer = new VBOBatchingPointsSnapRenderer(this._scene);
        }
        return this._snapRenderer;
    }

    _destroy() {
        if (this._colorRenderer) {
            this._colorRenderer.destroy();
        }
        if (this._silhouetteRenderer) {
            this._silhouetteRenderer.destroy();
        }
        if (this._pickMeshRenderer) {
            this._pickMeshRenderer.destroy();
        }
        if (this._pickDepthRenderer) {
            this._pickDepthRenderer.destroy();
        }
        if (this._occlusionRenderer) {
            this._occlusionRenderer.destroy();
        }
        if (this._snapInitRenderer) {
            this._snapInitRenderer.destroy();
        }
        if (this._snapRenderer) {
            this._snapRenderer.destroy();
        }
    }
}

const cachedRenderers$2 = {};

/**
 * @private
 */
function getRenderers$3(scene) {
    const sceneId = scene.id;
    let renderers = cachedRenderers$2[sceneId];
    if (!renderers) {
        renderers = new VBOBatchingPointsRenderers(scene);
        cachedRenderers$2[sceneId] = renderers;
        renderers._compile();
        scene.on("compile", () => {
            renderers._compile();
        });
        scene.on("destroyed", () => {
            delete cachedRenderers$2[sceneId];
            renderers._destroy();
        });
    }
    return renderers;
}

/**
 * @private
 */
class VBOBatchingPointsLayer {

    /**
     * @param model
     * @param cfg
     * @param cfg.layerIndex
     * @param cfg.positionsDecodeMatrix
     * @param cfg.maxGeometryBatchSize
     * @param cfg.origin
     * @param cfg.scratchMemory
     */
    constructor(cfg) {

     //   console.info("Creating VBOBatchingPointsLayer");

        /**
         * Owner model
         * @type {VBOSceneModel}
         */
        this.model = cfg.model;

        /**
         * State sorting key.
         * @type {string}
         */
        this.sortId = "PointsBatchingLayer";

        /**
         * Index of this PointsBatchingLayer in {@link VBOSceneModel#_layerList}.
         * @type {Number}
         */
        this.layerIndex = cfg.layerIndex;

        this._renderers = getRenderers$3(cfg.model.scene);

        const maxGeometryBatchSize = cfg.maxGeometryBatchSize || 5000000;

        const attribute = function() {
            const portions = [ ];

            return {
                append: function(data, times = 1, denormalizeScale = 1.0) {
                    portions.push({ data: data, times: times, denormalizeScale: denormalizeScale });
                },
                compileBuffer: function(type) {
                    let len = 0;
                    portions.forEach(p => { len += p.times * p.data.length; });
                    const buf = new type(len);

                    let begin = 0;
                    portions.forEach(p => {
                        const data = p.data;
                        const dScale = p.denormalizeScale;
                        const subBuf = buf.subarray(begin);

                        if (dScale === 1.0) {
                            subBuf.set(data, 0);
                        } else {
                            for (let i = 0; i < data.length; ++i) {
                                subBuf[i] = data[i] * dScale;
                            }
                        }

                        let soFar = data.length;
                        const allDataLen = p.times * data.length;
                        while (soFar < allDataLen) {
                            const toCopy = Math.min(soFar, allDataLen - soFar);
                            subBuf.set(subBuf.subarray(0, toCopy), soFar);
                            soFar += toCopy;
                        }

                        begin += soFar;
                    });

                    return buf;
                }
            };
        };

        this._buffer = {
            maxVerts:   maxGeometryBatchSize,
            positions:  attribute(),
            colors:     attribute(),
            pickColors: attribute(),
            vertsIndex: 0
        };

        this._scratchMemory = cfg.scratchMemory;

        this._state = new RenderState({
            positionsBuf: null,
            offsetsBuf: null,
            colorsBuf: null,
            flagsBuf: null,
            positionsDecodeMatrix: math.mat4(),
            origin: null
        });

        // These counts are used to avoid unnecessary render passes
        this._numPortions = 0;
        this._numVisibleLayerPortions = 0;
        this._numTransparentLayerPortions = 0;
        this._numXRayedLayerPortions = 0;
        this._numSelectedLayerPortions = 0;
        this._numHighlightedLayerPortions = 0;
        this._numClippableLayerPortions = 0;
        this._numPickableLayerPortions = 0;
        this._numCulledLayerPortions = 0;

        this._modelAABB = math.collapseAABB3(); // Model-space AABB
        this._portions = [];
        this._meshes = [];

        this._aabb = math.collapseAABB3();
        this.aabbDirty = true;

        this._finalized = false;

        if (cfg.positionsDecodeMatrix) {
            this._state.positionsDecodeMatrix.set(cfg.positionsDecodeMatrix);
            this._preCompressedPositionsExpected = true;
        } else {
            this._preCompressedPositionsExpected = false;
        }

        if (cfg.origin) {
            this._state.origin = math.vec3(cfg.origin);
        }
    }

    get aabb() {
        if (this.aabbDirty) {
            math.collapseAABB3(this._aabb);
            for (let i = 0, len = this._meshes.length; i < len; i++) {
                math.expandAABB3(this._aabb, this._meshes[i].aabb);
            }
            this.aabbDirty = false;
        }
        return this._aabb;
    }

    /**
     * Tests if there is room for another portion in this PointsBatchingLayer.
     *
     * @param lenPositions Number of positions we'd like to create in the portion.
     * @returns {Boolean} True if OK to create another portion.
     */
    canCreatePortion(lenPositions) {
        if (this._finalized) {
            throw "Already finalized";
        }
        return (this._buffer.vertsIndex + (lenPositions / 3)) <= this._buffer.maxVerts;
    }

    /**
     * Creates a new portion within this PointsBatchingLayer, returns the new portion ID.
     *
     * Gives the portion the specified geometry, color and matrix.
     *
     * @param mesh The SceneModelMesh that owns the portion
     * @param cfg.positions Flat float Local-space positions array.
     * @param cfg.positionsCompressed Flat quantized positions array - decompressed with PointsBatchingLayer positionsDecodeMatrix
     * @param [cfg.colorsCompressed] Quantized RGB colors [0..255,0..255,0..255,0..255]
     * @param [cfg.colors] Flat float colors array.
     * @param cfg.color Float RGB color [0..1,0..1,0..1]
     * @param [cfg.meshMatrix] Flat float 4x4 matrix
     * @param cfg.aabb Flat float AABB World-space AABB
     * @param cfg.pickColor Quantized pick color
     * @returns {number} Portion ID
     */
    createPortion(mesh, cfg) {

        if (this._finalized) {
            throw "Already finalized";
        }

        const buffer = this._buffer;

        const positions = this._preCompressedPositionsExpected ? cfg.positionsCompressed : cfg.positions;
        if (! positions) {
            throw ((this._preCompressedPositionsExpected ? "positionsCompressed" : "positions") + " expected");
        }

        buffer.positions.append(positions);

        const numVerts = positions.length / 3;

        const color = cfg.color;
        const colorsCompressed = cfg.colorsCompressed;
        const colors = cfg.colors;
        const pickColor = cfg.pickColor;

        if (colorsCompressed) {
            buffer.colors.append(colorsCompressed);
        } else if (colors) {
            buffer.colors.append(colors, 1, 255.0);
        } else if (color) {
            // Color is pre-quantized by VBOSceneModel
            buffer.colors.append([ color[0], color[1], color[2], 1.0 ], numVerts);
        }

        buffer.pickColors.append(pickColor.slice(0, 4), numVerts);

        math.expandAABB3(this._modelAABB, cfg.aabb);

        const portionId = this._portions.length / 2;

        this._portions.push(this._buffer.vertsIndex);
        this._portions.push(numVerts);
        this._buffer.vertsIndex += numVerts;

        this._numPortions++;
        this.model.numPortions++;
        this._meshes.push(mesh);
        return portionId;
    }

    /**
     * Builds batch VBOs from appended geometries.
     * No more portions can then be created.
     */
    finalize() {

        if (this._finalized) {
            return;
        }

        const state = this._state;
        const gl = this.model.scene.canvas.gl;
        const buffer = this._buffer;
        const maybeCreateGlBuffer = (srcData, size, usage) => (srcData.length > 0) ? new ArrayBuf(gl, gl.ARRAY_BUFFER, srcData, srcData.length, size, usage) : null;

        const positions = (this._preCompressedPositionsExpected
                           ? buffer.positions.compileBuffer(Uint16Array)
                           : (quantizePositions(buffer.positions.compileBuffer(Float32Array), this._modelAABB, state.positionsDecodeMatrix)));
        state.positionsBuf  = maybeCreateGlBuffer(positions, 3, gl.STATIC_DRAW);

        state.flagsBuf      = maybeCreateGlBuffer(new Float32Array(this._buffer.vertsIndex), 1, gl.DYNAMIC_DRAW); // Because we build flags arrays here, get their length from the positions array

        state.colorsBuf     = maybeCreateGlBuffer(buffer.colors.compileBuffer(Uint8Array), 4, gl.STATIC_DRAW);

        state.pickColorsBuf = maybeCreateGlBuffer(buffer.pickColors.compileBuffer(Uint8Array), 4, gl.STATIC_DRAW);

        state.offsetsBuf    = this.model.scene.entityOffsetsEnabled ? maybeCreateGlBuffer(new Float32Array(this._buffer.vertsIndex * 3), 3, gl.DYNAMIC_DRAW) : null;

        this._buffer = null;
        this._finalized = true;
    }

    initFlags(portionId, flags, meshTransparent) {
        if (flags & ENTITY_FLAGS.VISIBLE) {
            this._numVisibleLayerPortions++;
            this.model.numVisibleLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.HIGHLIGHTED) {
            this._numHighlightedLayerPortions++;
            this.model.numHighlightedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.XRAYED) {
            this._numXRayedLayerPortions++;
            this.model.numXRayedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.SELECTED) {
            this._numSelectedLayerPortions++;
            this.model.numSelectedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.CLIPPABLE) {
            this._numClippableLayerPortions++;
            this.model.numClippableLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.PICKABLE) {
            this._numPickableLayerPortions++;
            this.model.numPickableLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.CULLED) {
            this._numCulledLayerPortions++;
            this.model.numCulledLayerPortions++;
        }
        if (meshTransparent) {
            this._numTransparentLayerPortions++;
            this.model.numTransparentLayerPortions++;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setVisible(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.VISIBLE) {
            this._numVisibleLayerPortions++;
            this.model.numVisibleLayerPortions++;
        } else {
            this._numVisibleLayerPortions--;
            this.model.numVisibleLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setHighlighted(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.HIGHLIGHTED) {
            this._numHighlightedLayerPortions++;
            this.model.numHighlightedLayerPortions++;
        } else {
            this._numHighlightedLayerPortions--;
            this.model.numHighlightedLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setXRayed(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.XRAYED) {
            this._numXRayedLayerPortions++;
            this.model.numXRayedLayerPortions++;
        } else {
            this._numXRayedLayerPortions--;
            this.model.numXRayedLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setSelected(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.SELECTED) {
            this._numSelectedLayerPortions++;
            this.model.numSelectedLayerPortions++;
        } else {
            this._numSelectedLayerPortions--;
            this.model.numSelectedLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setEdges(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        // Not applicable to point clouds
    }

    setClippable(portionId, flags) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.CLIPPABLE) {
            this._numClippableLayerPortions++;
            this.model.numClippableLayerPortions++;
        } else {
            this._numClippableLayerPortions--;
            this.model.numClippableLayerPortions--;
        }
        this._setFlags(portionId, flags);
    }

    setCulled(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.CULLED) {
            this._numCulledLayerPortions++;
            this.model.numCulledLayerPortions++;
        } else {
            this._numCulledLayerPortions--;
            this.model.numCulledLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setCollidable(portionId, flags) {
        if (!this._finalized) {
            throw "Not finalized";
        }
    }

    setPickable(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.PICKABLE) {
            this._numPickableLayerPortions++;
            this.model.numPickableLayerPortions++;
        } else {
            this._numPickableLayerPortions--;
            this.model.numPickableLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setColor(portionId, color) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        const portionsIdx = portionId * 2;
        const vertexBase = this._portions[portionsIdx];
        const numVerts = this._portions[portionsIdx + 1];
        const firstColor = vertexBase * 4;
        const lenColor = numVerts * 4;
        const tempArray = this._scratchMemory.getUInt8Array(lenColor);
        const r = color[0];
        const g = color[1];
        const b = color[2];
        for (let i = 0; i < lenColor; i += 4) {
            tempArray[i + 0] = r;
            tempArray[i + 1] = g;
            tempArray[i + 2] = b;
        }
        this._state.colorsBuf.setData(tempArray, firstColor, lenColor);
    }

    setTransparent(portionId, flags, transparent) {
        if (transparent) {
            this._numTransparentLayerPortions++;
            this.model.numTransparentLayerPortions++;
        } else {
            this._numTransparentLayerPortions--;
            this.model.numTransparentLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    /**
     * flags are 4bits values encoded on a 32bit base. color flag on the first 4 bits, silhouette flag on the next 4 bits and so on for edge, pick and clippable.
     */
    _setFlags(portionId, flags, transparent) {

        if (!this._finalized) {
            throw "Not finalized";
        }

        const portionsIdx = portionId * 2;
        const vertexBase = this._portions[portionsIdx];
        const numVerts = this._portions[portionsIdx + 1];
        const firstFlag = vertexBase;
        const lenFlags = numVerts;
        const tempArray = this._scratchMemory.getFloat32Array(lenFlags);

        const visible = !!(flags & ENTITY_FLAGS.VISIBLE);
        const xrayed = !!(flags & ENTITY_FLAGS.XRAYED);
        const highlighted = !!(flags & ENTITY_FLAGS.HIGHLIGHTED);
        const selected = !!(flags & ENTITY_FLAGS.SELECTED);
        const pickable = !!(flags & ENTITY_FLAGS.PICKABLE);
        const culled = !!(flags & ENTITY_FLAGS.CULLED);

        let colorFlag;
        if (!visible || culled || xrayed
            || (highlighted && !this.model.scene.highlightMaterial.glowThrough)
            || (selected && !this.model.scene.selectedMaterial.glowThrough)) {
            colorFlag = RENDER_PASSES.NOT_RENDERED;
        } else {
            if (transparent) {
                colorFlag = RENDER_PASSES.COLOR_TRANSPARENT;
            } else {
                colorFlag = RENDER_PASSES.COLOR_OPAQUE;
            }
        }

        let silhouetteFlag;
        if (!visible || culled) {
            silhouetteFlag = RENDER_PASSES.NOT_RENDERED;
        } else if (selected) {
            silhouetteFlag = RENDER_PASSES.SILHOUETTE_SELECTED;
        } else if (highlighted) {
            silhouetteFlag = RENDER_PASSES.SILHOUETTE_HIGHLIGHTED;
        } else if (xrayed) {
            silhouetteFlag = RENDER_PASSES.SILHOUETTE_XRAYED;
        } else {
            silhouetteFlag = RENDER_PASSES.NOT_RENDERED;
        }

        let pickFlag = (visible && !culled && pickable) ? RENDER_PASSES.PICK : RENDER_PASSES.NOT_RENDERED;

        const clippableFlag = !!(flags & ENTITY_FLAGS.CLIPPABLE) ? 1 : 0;

        for (let i = 0; i < lenFlags; i++) {
            let vertFlag = 0;
            vertFlag |= colorFlag;
            vertFlag |= silhouetteFlag << 4;
            // no edges
            vertFlag |= pickFlag << 12;
            vertFlag |= clippableFlag << 16;

            tempArray[i] = vertFlag;
        }

        this._state.flagsBuf.setData(tempArray, firstFlag);
    }

    setOffset(portionId, offset) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (!this.model.scene.entityOffsetsEnabled) {
            this.model.error("Entity#offset not enabled for this Viewer"); // See Viewer entityOffsetsEnabled
            return;
        }
        const portionsIdx = portionId * 2;
        const vertexBase = this._portions[portionsIdx];
        const numVerts = this._portions[portionsIdx + 1];
        const firstOffset = vertexBase * 3;
        const lenOffsets = numVerts * 3;
        const tempArray = this._scratchMemory.getFloat32Array(lenOffsets);
        const x = offset[0];
        const y = offset[1];
        const z = offset[2];
        for (let i = 0; i < lenOffsets; i += 3) {
            tempArray[i + 0] = x;
            tempArray[i + 1] = y;
            tempArray[i + 2] = z;
        }
        this._state.offsetsBuf.setData(tempArray, firstOffset, lenOffsets);
    }

    //-- NORMAL RENDERING ----------------------------------------------------------------------------------------------

    drawColorOpaque(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        if (this._renderers.colorRenderer) {
            this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
        }
    }

    drawColorTransparent(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === 0 || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        if (this._renderers.colorRenderer) {
            this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT);
        }
    }

    // -- RENDERING SAO POST EFFECT TARGETS ----------------------------------------------------------------------------

    drawDepth(renderFlags, frameCtx) {
    }

    drawNormals(renderFlags, frameCtx) {
    }

    // -- EMPHASIS RENDERING -------------------------------------------------------------------------------------------

    drawSilhouetteXRayed(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) {
            return;
        }
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_XRAYED);
        }
    }

    drawSilhouetteHighlighted(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) {
            return;
        }
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_HIGHLIGHTED);
        }
    }

    drawSilhouetteSelected(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) {
            return;
        }
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_SELECTED);
        }
    }

    //-- EDGES RENDERING -----------------------------------------------------------------------------------------------

    drawEdgesColorOpaque(renderFlags, frameCtx) {
    }

    drawEdgesColorTransparent(renderFlags, frameCtx) {
    }

    drawEdgesHighlighted(renderFlags, frameCtx) {
    }

    drawEdgesSelected(renderFlags, frameCtx) {
    }

    drawEdgesXRayed(renderFlags, frameCtx) {
    }

    //---- PICKING ----------------------------------------------------------------------------------------------------

    drawPickMesh(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        if (this._renderers.pickMeshRenderer) {
            this._renderers.pickMeshRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawPickDepths(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        if (this._renderers.pickDepthRenderer) {
            this._renderers.pickDepthRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawPickNormals(renderFlags, frameCtx) {
    }

    drawSnapInit(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        if (this._renderers.snapInitRenderer) {
            this._renderers.snapInitRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawSnap(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        if (this._renderers.snapRenderer) {
            this._renderers.snapRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    //---- OCCLUSION TESTING -------------------------------------------------------------------------------------------

    drawOcclusion(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        if (this._renderers.occlusionRenderer) {
            this._renderers.occlusionRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
        }
    }

    //---- SHADOWS -----------------------------------------------------------------------------------------------------

    drawShadow(renderFlags, frameCtx) {
    }

    destroy() {
        const state = this._state;
        if (state.positionsBuf) {
            state.positionsBuf.destroy();
            state.positionsBuf = null;
        }
        if (state.offsetsBuf) {
            state.offsetsBuf.destroy();
            state.offsetsBuf = null;
        }
        if (state.colorsBuf) {
            state.colorsBuf.destroy();
            state.colorsBuf = null;
        }
        if (state.flagsBuf) {
            state.flagsBuf.destroy();
            state.flagsBuf = null;
        }
        if (state.pickColorsBuf) {
            state.pickColorsBuf.destroy();
            state.pickColorsBuf = null;
        }
        state.destroy();
    }
}

/**
 * @private
 */
class VBOInstancingPointsRenderer extends VBORenderer {
    constructor(scene, withSAO) {
        super(scene, withSAO, {instancing: true});
    }

    _draw(drawCfg) {
        const {gl} = this._scene.canvas;

        const {
            state,
            frameCtx,
            incrementDrawState,
        } = drawCfg;

        gl.drawArraysInstanced(gl.POINTS, 0, state.positionsBuf.numItems, state.numInstances);

        if (incrementDrawState) {
            frameCtx.drawArrays++;
        }
    }
}

/**
 * @private
 */
class VBOInstancingPointsColorRenderer extends VBOInstancingPointsRenderer {
    _getHash() {
        return this._scene._sectionPlanesState.getHash() + this._scene.pointsMaterial.hash;
    }

    drawLayer(frameCtx, layer, renderPass) {
        super.drawLayer(frameCtx, layer, renderPass, { incrementDrawState: true });
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const pointsMaterial = scene.pointsMaterial._state;
        const src = [];
        src.push('#version 300 es');
        src.push("// Points instancing color vertex shader");

        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        src.push("in vec4 color;");
        src.push("in float flags;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform float pointSize;");
        if (pointsMaterial.perspectivePoints) {
            src.push("uniform float nearPlaneHeight;");
        }

        if (pointsMaterial.filterIntensity) {
            src.push("uniform vec2 intensityRange;");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec4 vColor;");

        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");

        if (pointsMaterial.filterIntensity) {
            src.push("float intensity = float(color.a) / 255.0;");
            src.push("if (intensity < intensityRange[0] || intensity > intensityRange[1]) {");
            src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
            src.push("} else {");
        }

        src.push("vec4 worldPosition =  positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }

        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");

        src.push("vColor = vec4(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0, 1.0);");

        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }

        src.push("vec4 clipPos = projMatrix * viewPosition;");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
        }

        src.push("gl_Position = clipPos;");
        if (pointsMaterial.perspectivePoints) {
            src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;");
            src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);");
            src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);");
        } else {
            src.push("gl_PointSize = pointSize;");
        }
        src.push("}");
        if (pointsMaterial.filterIntensity) {
            src.push("}");
        }
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Points instancing color fragment shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (scene.pointsMaterial.roundPoints) {
            src.push("  vec2 cxy = 2.0 * gl_PointCoord - 1.0;");
            src.push("  float r = dot(cxy, cxy);");
            src.push("  if (r > 1.0) {");
            src.push("       discard;");
            src.push("  }");
        }
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        src.push("   outColor = vColor;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class VBOInstancingPointsSilhouetteRenderer extends VBOInstancingPointsRenderer {
    _getHash() {
        return this._scene._sectionPlanesState.getHash() + this._scene.pointsMaterial.hash;
    }

    drawLayer(frameCtx, instancingLayer, renderPass) {
        super.drawLayer(frameCtx, instancingLayer, renderPass, {colorUniform: true});
    }

    _buildVertexShader() {

        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const pointsMaterial = scene.pointsMaterial._state;
        const src = [];
        src.push('#version 300 es');
        src.push("// Points instancing silhouette vertex shader");

        src.push("uniform int renderPass;");

        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("in vec4 color;");

        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform float pointSize;");
        if (pointsMaterial.perspectivePoints) {
            src.push("uniform float nearPlaneHeight;");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
        }

        src.push("uniform vec4 silhouetteColor;");

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec4 vColor;");

        src.push("void main(void) {");

        // silhouetteFlag = NOT_RENDERED | SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED
        // renderPass = SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED

        src.push(`int silhouetteFlag = int(flags) >> 4 & 0xF;`);
        src.push(`if (silhouetteFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");

        src.push("vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");

        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
        }
        src.push("vColor = vec4(float(silhouetteColor.r) / 255.0, float(silhouetteColor.g) / 255.0, float(silhouetteColor.b) / 255.0, float(color.a) / 255.0);");
        src.push("gl_Position = clipPos;");
        if (pointsMaterial.perspectivePoints) {
            src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;");
            src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);");
            src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);");
        } else {
            src.push("gl_PointSize = pointSize;");
        }
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Points instancing silhouette fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (scene.pointsMaterial.roundPoints) {
            src.push("  vec2 cxy = 2.0 * gl_PointCoord - 1.0;");
            src.push("  float r = dot(cxy, cxy);");
            src.push("  if (r > 1.0) {");
            src.push("       discard;");
            src.push("  }");
        }
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outColor = vColor;");
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class VBOInstancingPointsPickMeshRenderer extends VBOInstancingPointsRenderer {
    _getHash() {
        return this._scene._sectionPlanesState.getHash() + this._scene.pointsMaterial.hash;
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const pointsMaterial = scene.pointsMaterial._state;
        const src = [];
        src.push ('#version 300 es');
        src.push("// Points instancing pick mesh vertex shader");
        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("in vec4 pickColor;");

        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        this._addRemapClipPosLines(src);

        src.push("uniform float pointSize;");
        if (pointsMaterial.perspectivePoints) {
            src.push("uniform float nearPlaneHeight;");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out vec4 vPickColor;");
        src.push("void main(void) {");

        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK

        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");


        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }

        src.push("  vec4 viewPosition  = viewMatrix * worldPosition; ");

        src.push("  vPickColor = vec4(float(pickColor.r) / 255.0, float(pickColor.g) / 255.0, float(pickColor.b) / 255.0, float(pickColor.a) / 255.0);");
        if (clipping) {
            src.push("  vWorldPosition = worldPosition;");
            src.push("  vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        src.push("gl_Position = remapClipPos(clipPos);");
        if (scene.logarithmicDepthBufferEnabled) {
           src.push("vFragDepth = 1.0 + clipPos.w;");
        }
        if (pointsMaterial.perspectivePoints) {
            src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;");
            src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);");
            src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);");
        } else {
            src.push("gl_PointSize = pointSize;");
        }
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// Points instancing pick mesh fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vPickColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");

        if (scene.pointsMaterial.roundPoints) {
            src.push("  vec2 cxy = 2.0 * gl_PointCoord - 1.0;");
            src.push("  float r = dot(cxy, cxy);");
            src.push("  if (r > 1.0) {");
            src.push("       discard;");
            src.push("  }");
        }

        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outColor = vPickColor; ");
        src.push("}");
        return src;
    }
}

/**
 * @private
 */


class VBOInstancingPointsPickDepthRenderer extends VBOInstancingPointsRenderer {
    _getHash() {
        return this._scene._sectionPlanesState.getHash() + this._scene.pointsMaterial.hash;
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const pointsMaterial = scene.pointsMaterial._state;
        const src = [];

        src.push('#version 300 es');
        src.push("// Points instancing pick depth vertex shader");

        src.push("uniform int renderPass;");

        src.push("in vec3 position;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        src.push("in float flags;");
        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        this._addRemapClipPosLines(src);

        src.push("uniform float pointSize;");
        if (pointsMaterial.perspectivePoints) {
            src.push("uniform float nearPlaneHeight;");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }

        src.push("out vec4 vViewPosition;");
        src.push("void main(void) {");

        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK

        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");
        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");

        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("  vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("  vWorldPosition = worldPosition;");
            src.push("  vFlags = flags;");
        }
        src.push("  vViewPosition = viewPosition;");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        src.push("gl_Position = remapClipPos(clipPos);");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
        }
        src.push("gl_Position = remapClipPos(clipPos);");
        if (pointsMaterial.perspectivePoints) {
            src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;");
            src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);");
            src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);");
        } else {
            src.push("gl_PointSize = pointSize;");
        }
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Points instancing pick depth fragment shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }

        src.push("uniform float pickZNear;");
        src.push("uniform float pickZFar;");

        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vViewPosition;");
        src.push("vec4 packDepth(const in float depth) {");
        src.push("  const vec4 bitShift = vec4(256.0*256.0*256.0, 256.0*256.0, 256.0, 1.0);");
        src.push("  const vec4 bitMask  = vec4(0.0, 1.0/256.0, 1.0/256.0, 1.0/256.0);");
        src.push("  vec4 res = fract(depth * bitShift);");
        src.push("  res -= res.xxyz * bitMask;");
        src.push("  return res;");
        src.push("}");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");

        if (scene.pointsMaterial.roundPoints) {
            src.push("  vec2 cxy = 2.0 * gl_PointCoord - 1.0;");
            src.push("  float r = dot(cxy, cxy);");
            src.push("  if (r > 1.0) {");
            src.push("       discard;");
            src.push("  }");
        }

        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("    float zNormalizedDepth = abs((pickZNear + vViewPosition.z) / (pickZFar - pickZNear));");
        src.push("    outColor = packDepth(zNormalizedDepth); "); // Must be linear depth
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class VBOInstancingPointsOcclusionRenderer extends VBOInstancingPointsRenderer {
    _getHash() {
        return this._scene._sectionPlanesState.getHash() + this._scene.pointsMaterial.hash;
    }

    _buildVertexShader() {

        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const pointsMaterial = scene.pointsMaterial._state;
        const src = [];

        src.push ('#version 300 es');
        src.push("// Points instancing occlusion vertex shader");

        src.push("uniform int renderPass;");
        src.push("in vec3 position;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        src.push("in vec4 color;");
        src.push("in float flags;");

        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform float pointSize;");
        if (pointsMaterial.perspectivePoints) {
            src.push("uniform float nearPlaneHeight;");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("      gl_Position = vec4(0.0, 0.0, 0.0, 0.0);");

        src.push("} else {");
        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }

        src.push("  vec4 viewPosition  = viewMatrix * worldPosition; ");

        if (clipping) {
            src.push("  vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }

        src.push("vec4 clipPos = projMatrix * viewPosition;");

        if (scene.logarithmicDepthBufferEnabled) {
           src.push("vFragDepth = 1.0 + clipPos.w;");
        }

        src.push("gl_Position = clipPos;");
        if (pointsMaterial.perspectivePoints) {
            src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;");
            src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);");
            src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);");
        } else {
            src.push("gl_PointSize = pointSize;");
        }
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// Points instancing occlusion vertex shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (scene.pointsMaterial.roundPoints) {
            src.push("  vec2 cxy = 2.0 * gl_PointCoord - 1.0;");
            src.push("  float r = dot(cxy, cxy);");
            src.push("  if (r > 1.0) {");
            src.push("       discard;");
            src.push("  }");
        }
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        src.push("   outColor = vec4(0.0, 0.0, 1.0, 1.0); "); // Occluders are blue
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("}");
        return src;
    }
}

/**
 * @private
 */
class VBOInstancingPointsDepthRenderer extends VBOInstancingPointsRenderer {
    _getHash() {
        return this._scene._sectionPlanesState.getHash() + this._scene.pointsMaterial.hash;
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const pointsMaterial = scene.pointsMaterial._state;
        const src = [];
        src.push('#version 300 es');
        src.push("// Points instancing depth vertex shader");
        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("in vec4 modelMatrixCol0;");
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform float pointSize;");
        if (pointsMaterial.perspectivePoints) {
            src.push("uniform float nearPlaneHeight;");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
        }
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("void main(void) {");

        // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE

        src.push(`int colorFlag = int(flags) & 0xF;`);
        src.push(`if (colorFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("} else {");
        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("  vec4 viewPosition  = viewMatrix * worldPosition; ");

        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
        }
        src.push("gl_Position = clipPos;");
        if (pointsMaterial.perspectivePoints) {
            src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;");
            src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);");
            src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);");
        } else {
            src.push("gl_PointSize = pointSize;");
        }
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        let i;
        let len;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Points instancing depth vertex shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }

        src.push("const float   packUpScale = 256. / 255.;");
        src.push("const float   unpackDownscale = 255. / 256.;");
        src.push("const vec3    packFactors = vec3( 256. * 256. * 256., 256. * 256.,  256. );");
        src.push("const vec4    unpackFactors = unpackDownscale / vec4( packFactors, 1. );");
        src.push("const float   shiftRight8 = 1.0 / 256.;");

        src.push("vec4 packDepthToRGBA( const in float v ) {");
        src.push("    vec4 r = vec4( fract( v * packFactors ), v );");
        src.push("    r.yzw -= r.xyz * shiftRight8;");
        src.push("    return r * packUpScale;");
        src.push("}");

        src.push("out vec4 outColor;");
        src.push("void main(void) {");

        if (scene.pointsMaterial.roundPoints) {
            src.push("  vec2 cxy = 2.0 * gl_PointCoord - 1.0;");
            src.push("  float r = dot(cxy, cxy);");
            src.push("  if (r > 1.0) {");
            src.push("       discard;");
            src.push("  }");
        }

        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        src.push("    outColor = packDepthToRGBA( gl_FragCoord.z); "); // Must be linear depth
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("}");
        return src;
    }
}

/**
 * Renders InstancingLayer fragment depths to a shadow map.
 *
 * @private
 */
class VBOInstancingPointsShadowRenderer extends VBOInstancingPointsRenderer {

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// Instancing geometry shadow drawing vertex shader");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in vec4 color;");
        src.push("in float flags;");
        src.push("in vec4 modelMatrixCol0;");
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");
        src.push("uniform mat4 shadowViewMatrix;");
        src.push("uniform mat4 shadowProjMatrix;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform float pointSize;");
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("void main(void) {");
        src.push("int colorFlag     = int(flags) & 0xF;");
        src.push("bool visible      = (colorFlag > 0);");
        src.push("bool transparent  = ((float(color.a) / 255.0) < 1.0);");
        src.push(`if (!visible || transparent) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("} else {");
        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("  vec4 viewPosition  = shadowViewMatrix * worldPosition; ");

        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags = flags;");
        }
        src.push("  gl_Position = shadowProjMatrix * viewPosition;");
        src.push("}");
        src.push("gl_PointSize = pointSize;");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// Instancing geometry depth drawing fragment shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec3 vViewNormal;");
        src.push("vec3 packNormalToRGB( const in vec3 normal ) {");
        src.push("    return normalize( normal ) * 0.5 + 0.5;");
        src.push("}");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        src.push("  vec2 cxy = 2.0 * gl_PointCoord - 1.0;");
        src.push("  float r = dot(cxy, cxy);");
        src.push("  if (r > 1.0) {");
        src.push("       discard;");
        src.push("  }");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("    outColor = vec4(packNormalToRGB(vViewNormal), 1.0); ");
        src.push("}");
        return src;
    }
}

const tempVec3a$s = math.vec3();
const tempVec3b$o = math.vec3();
const tempVec3c$j = math.vec3();
math.vec3();
const tempMat4a$g = math.mat4();

/**
 * @private
 */
class VBOInstancingPointsSnapInitRenderer extends VBORenderer {

    constructor(scene) {
        super(scene, false, { instancing: true });
    }

    drawLayer(frameCtx, instancingLayer, renderPass) {

        if (!this._program) {
            this._allocate();
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram();
        }

        const model = instancingLayer.model;
        const scene = model.scene;
        const gl = scene.canvas.gl;
        const camera = scene.camera;
        const state = instancingLayer._state;
        const origin = instancingLayer._state.origin;
        const {position, rotationMatrix} = model;
        const aabb = instancingLayer.aabb; // Per-layer AABB for best RTC accuracy
        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;

        if (this._vaoCache.has(instancingLayer)) {
            gl.bindVertexArray(this._vaoCache.get(instancingLayer));
        } else {
            this._vaoCache.set(instancingLayer, this._makeVAO(state));
        }

        const coordinateScaler = tempVec3a$s;
        coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT;
        coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT;
        coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT;

        frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]);
        frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]);
        frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]);

        let rtcViewMatrix;
        if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) {
            const rtcOrigin = tempVec3b$o;
            if (origin) {
                const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3c$j);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$g);
            frameCtx.snapPickOrigin[0] = rtcOrigin[0];
            frameCtx.snapPickOrigin[1] = rtcOrigin[1];
            frameCtx.snapPickOrigin[2] = rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            frameCtx.snapPickOrigin[0] = 0;
            frameCtx.snapPickOrigin[1] = 0;
            frameCtx.snapPickOrigin[2] = 0;
        }

        gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA);
        gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB);
        gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber);
        gl.uniform3fv(this._uCoordinateScaler, coordinateScaler);
        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible);

        let offset = 0;
        const mat4Size = 4 * 4;

        this._matricesUniformBlockBufferData.set(rotationMatrix, 0);
        this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size);

        gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer);
        gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW);

        gl.bindBufferBase(
            gl.UNIFORM_BUFFER,
            this._matricesUniformBlockBufferBindingPoint,
            this._matricesUniformBlockBuffer);


        {
            const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }

        this.setSectionPlanesStateUniforms(instancingLayer);

        gl.drawArraysInstanced(gl.POINTS, 0, state.positionsBuf.numItems, state.numInstances);

        gl.bindVertexArray(null);
    }

    _allocate() {
        super._allocate();

        const program = this._program;

        {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
        this.uVectorA = program.getLocation("snapVectorA");
        this.uInverseVectorAB = program.getLocation("snapInvVectorAB");
        this._uLayerNumber = program.getLocation("layerNumber");
        this._uCoordinateScaler = program.getLocation("coordinateScaler");
    }

    _bindProgram() {
        this._program.bind();

    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// SnapInstancingDepthBufInitRenderer vertex shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");
        src.push("uniform int renderPass;");
        src.push("in vec4 pickColor;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");
        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec2 snapVectorA;");
        src.push("uniform vec2 snapInvVectorAB;");
        {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }
        src.push("vec2 remapClipPos(vec2 clipPos) {");
        src.push("    float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;");
        src.push("    float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;");
        src.push("    return vec2(x, y);");
        src.push("}");
        src.push("flat out vec4 vPickColor;");
        src.push("out vec4 vWorldPosition;");
        if (clipping) {
            src.push("out float vFlags;");
        }
        src.push("out highp vec3 relativeToOriginPosition;");
        src.push("void main(void) {");
        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK
        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("} else {");
        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("relativeToOriginPosition = worldPosition.xyz;");
        src.push("  vec4 viewPosition  = viewMatrix * worldPosition; ");
        src.push("  vWorldPosition = worldPosition;");
        if (clipping) {
            src.push("  vFlags = flags;");
        }
        src.push("vPickColor = pickColor;");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        src.push("float tmp = clipPos.w;");
        src.push("clipPos.xyzw /= tmp;");
        src.push("clipPos.xy = remapClipPos(clipPos.xy);");
        src.push("clipPos.xyzw *= tmp;");
        {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// Points instancing pick depth fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("uniform int layerNumber;");
        src.push("uniform vec3 coordinateScaler;");
        src.push("in vec4 vWorldPosition;");
        src.push("flat in vec4 vPickColor;");
        if (clipping) {
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in highp vec3 relativeToOriginPosition;");
        src.push("layout(location = 0) out highp ivec4 outCoords;");
        src.push("layout(location = 1) out highp ivec4 outNormal;");
        src.push("layout(location = 2) out lowp uvec4 outPickColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        {
            src.push("    float dx = dFdx(vFragDepth);");
            src.push("    float dy = dFdy(vFragDepth);");
            src.push("    float diff = sqrt(dx*dx+dy*dy);");
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth + diff ) * logDepthBufFC * 0.5;");
        }
        src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, -layerNumber);");
        // src.push("vec3 xTangent = dFdx( vWorldPosition.xyz );");
        // src.push("vec3 yTangent = dFdy( vWorldPosition.xyz );");
        // src.push("vec3 worldNormal = normalize( cross( xTangent, yTangent ) );");
        src.push(`outNormal = ivec4(1.0, 1.0, 1.0, 1.0);`);
        src.push("outPickColor = uvec4(vPickColor);");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

const tempVec3a$r = math.vec3();
const tempVec3b$n = math.vec3();
const tempVec3c$i = math.vec3();
math.vec3();
const tempMat4a$f = math.mat4();

/**
 * @private
 */
class VBOInstancingPointsSnapRenderer extends VBORenderer {

    constructor(scene) {
        super(scene, false, { instancing: true });
    }

    drawLayer(frameCtx, instancingLayer, renderPass) {

        if (!this._program) {
            this._allocate(instancingLayer);
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram();
        }

        const model = instancingLayer.model;
        const scene = model.scene;
        const camera = scene.camera;
        const gl = scene.canvas.gl;
        const state = instancingLayer._state;
        const origin = instancingLayer._state.origin;
        const {position, rotationMatrix} = model;
        const aabb = instancingLayer.aabb; // Per-layer AABB for best RTC accuracy
        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;

        if (this._vaoCache.has(instancingLayer)) {
            gl.bindVertexArray(this._vaoCache.get(instancingLayer));
        } else {
            this._vaoCache.set(instancingLayer, this._makeVAO(state));
        }

        const coordinateScaler = tempVec3a$r;
        coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT;
        coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT;
        coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT;

        frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]);
        frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]);
        frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]);

        let rtcViewMatrix;

        if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) {
            const rtcOrigin = tempVec3b$n;
            if (origin) {
                const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3c$i);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$f);
            frameCtx.snapPickOrigin[0] = rtcOrigin[0];
            frameCtx.snapPickOrigin[1] = rtcOrigin[1];
            frameCtx.snapPickOrigin[2] = rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            frameCtx.snapPickOrigin[0] = 0;
            frameCtx.snapPickOrigin[1] = 0;
            frameCtx.snapPickOrigin[2] = 0;
        }

        gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA);
        gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB);
        gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber);
        gl.uniform3fv(this._uCoordinateScaler, coordinateScaler);
        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible);

        let offset = 0;
        const mat4Size = 4 * 4;

        this._matricesUniformBlockBufferData.set(rotationMatrix, 0);
        this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size);
        this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size);

        gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer);
        gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW);

        gl.bindBufferBase(
            gl.UNIFORM_BUFFER,
            this._matricesUniformBlockBufferBindingPoint,
            this._matricesUniformBlockBuffer);

        {
            const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }

        this.setSectionPlanesStateUniforms(instancingLayer);

        gl.drawArraysInstanced(gl.POINTS, 0, state.positionsBuf.numItems, state.numInstances);

        gl.bindVertexArray(null);
    }

    _allocate() {
        super._allocate();

        const program = this._program;

        {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
        this.uVectorA = program.getLocation("snapVectorA");
        this.uInverseVectorAB = program.getLocation("snapInvVectorAB"); 
        this._uLayerNumber = program.getLocation("layerNumber"); 
        this._uCoordinateScaler = program.getLocation("coordinateScaler"); 
    }

    _bindProgram() {
        this._program.bind();

    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// SnapInstancingDepthRenderer vertex shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");
        src.push("uniform int renderPass;");
        src.push("in vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }
        src.push("in float flags;");
        src.push("in vec4 modelMatrixCol0;"); // Modeling matrix
        src.push("in vec4 modelMatrixCol1;");
        src.push("in vec4 modelMatrixCol2;");
        src.push("uniform bool pickInvisible;");

        this._addMatricesUniformBlockLines(src);

        src.push("uniform vec2 snapVectorA;"); 
        src.push("uniform vec2 snapInvVectorAB;"); 
        {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }
        src.push("vec2 remapClipPos(vec2 clipPos) {");
        src.push("    float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;");
        src.push("    float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;");
        src.push("    return vec2(x, y);");
        src.push("}");
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out float vFlags;");
        }
        src.push("out highp vec3 relativeToOriginPosition;");
        src.push("void main(void) {");
        // pickFlag = NOT_RENDERED | PICK
        // renderPass = PICK
        src.push(`int pickFlag = int(flags) >> 12 & 0xF;`);
        src.push(`if (pickFlag != renderPass) {`);
        src.push("   gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex
        src.push("} else {");
        src.push("  vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); ");
        src.push("  worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("relativeToOriginPosition = worldPosition.xyz;");
        src.push("  vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("  vWorldPosition = worldPosition;");
            src.push("  vFlags = flags;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        src.push("float tmp = clipPos.w;");
        src.push("clipPos.xyzw /= tmp;");
        src.push("clipPos.xy = remapClipPos(clipPos.xy);");
        src.push("clipPos.xyzw *= tmp;");
        {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("gl_PointSize = 1.0;"); // Windows needs this?
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push ('#version 300 es');
        src.push("// SnapInstancingDepthRenderer fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("uniform int layerNumber;"); 
        src.push("uniform vec3 coordinateScaler;"); 
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in float vFlags;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in highp vec3 relativeToOriginPosition;");
        src.push("out highp ivec4 outCoords;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (int(vFlags) >> 16 & 0xF) == 1;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("if (dist > 0.0) { discard; }");
            src.push("}");
        }
        {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, layerNumber);");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

/**
 * @private
 */
 class VBOInstancingPointsRenderers {

    constructor(scene) {
        this._scene = scene;
    }

    _compile() {
        if (this._colorRenderer && (!this._colorRenderer.getValid())) {
            this._colorRenderer.destroy();
            this._colorRenderer = null;
        }
        if (this._depthRenderer && (!this._depthRenderer.getValid())) {
            this._depthRenderer.destroy();
            this._depthRenderer = null;
        }
        if (this._silhouetteRenderer && (!this._silhouetteRenderer.getValid())) {
            this._silhouetteRenderer.destroy();
            this._silhouetteRenderer = null;
        }
        if (this._pickMeshRenderer && (!this._pickMeshRenderer.getValid())) {
            this._pickMeshRenderer.destroy();
            this._pickMeshRenderer = null;
        }
        if (this._pickDepthRenderer && (!this._pickDepthRenderer.getValid())) {
            this._pickDepthRenderer.destroy();
            this._pickDepthRenderer = null;
        }
        if (this._occlusionRenderer && this._occlusionRenderer.getValid() === false) {
            this._occlusionRenderer.destroy();
            this._occlusionRenderer = null;
        }
        if (this._shadowRenderer && (!this._shadowRenderer.getValid())) {
            this._shadowRenderer.destroy();
            this._shadowRenderer = null;
        }
        if (this._snapInitRenderer && (!this._snapInitRenderer.getValid())) {
            this._snapInitRenderer.destroy();
            this._snapInitRenderer = null;
        }
        if (this._snapRenderer && (!this._snapRenderer.getValid())) {
            this._snapRenderer.destroy();
            this._snapRenderer = null;
        }
    }

    get colorRenderer() {
        if (!this._colorRenderer) {
            this._colorRenderer = new VBOInstancingPointsColorRenderer(this._scene, false);
        }
        return this._colorRenderer;
    }

    get silhouetteRenderer() {
        if (!this._silhouetteRenderer) {
            this._silhouetteRenderer = new VBOInstancingPointsSilhouetteRenderer(this._scene);
        }
        return this._silhouetteRenderer;
    }

    get depthRenderer() {
        if (!this._depthRenderer) {
            this._depthRenderer = new VBOInstancingPointsDepthRenderer(this._scene);
        }
        return this._depthRenderer;
    }

    get pickMeshRenderer() {
        if (!this._pickMeshRenderer) {
            this._pickMeshRenderer = new VBOInstancingPointsPickMeshRenderer(this._scene);
        }
        return this._pickMeshRenderer;
    }

    get pickDepthRenderer() {
        if (!this._pickDepthRenderer) {
            this._pickDepthRenderer = new VBOInstancingPointsPickDepthRenderer(this._scene);
        }
        return this._pickDepthRenderer;
    }

    get occlusionRenderer() {
        if (!this._occlusionRenderer) {
            this._occlusionRenderer = new VBOInstancingPointsOcclusionRenderer(this._scene);
        }
        return this._occlusionRenderer;
    }

    get shadowRenderer() {
        if (!this._shadowRenderer) {
            this._shadowRenderer = new VBOInstancingPointsShadowRenderer(this._scene);
        }
        return this._shadowRenderer;
    }

    get snapInitRenderer() {
        if (!this._snapInitRenderer) {
            this._snapInitRenderer = new VBOInstancingPointsSnapInitRenderer(this._scene, false);
        }
        return this._snapInitRenderer;
    }

    get snapRenderer() {
        if (!this._snapRenderer) {
            this._snapRenderer = new VBOInstancingPointsSnapRenderer(this._scene);
        }
        return this._snapRenderer;
    }

    _destroy() {
        if (this._colorRenderer) {
            this._colorRenderer.destroy();
        }
        if (this._depthRenderer) {
            this._depthRenderer.destroy();
        }
        if (this._silhouetteRenderer) {
            this._silhouetteRenderer.destroy();
        }
        if (this._pickMeshRenderer) {
            this._pickMeshRenderer.destroy();
        }
        if (this._pickDepthRenderer) {
            this._pickDepthRenderer.destroy();
        }
        if (this._occlusionRenderer) {
            this._occlusionRenderer.destroy();
        }
        if (this._shadowRenderer) {
            this._shadowRenderer.destroy();
        }
        if (this._snapInitRenderer) {
            this._snapInitRenderer.destroy();
        }
        if (this._snapRenderer) {
            this._snapRenderer.destroy();
        }
    }
}

const cachedRenderers$1 = {};

/**
 * @private
 */
function getRenderers$2(scene) {
    const sceneId = scene.id;
    let instancingRenderers = cachedRenderers$1[sceneId];
    if (!instancingRenderers) {
        instancingRenderers = new VBOInstancingPointsRenderers(scene);
        cachedRenderers$1[sceneId] = instancingRenderers;
        instancingRenderers._compile();
        scene.on("compile", () => {
            instancingRenderers._compile();
        });
        scene.on("destroyed", () => {
            delete cachedRenderers$1[sceneId];
            instancingRenderers._destroy();
        });
    }
    return instancingRenderers;
}

const tempUint8Vec4 = new Uint8Array(4);
const tempFloat32 = new Float32Array(1);
const tempVec3fa = new Float32Array(3);

const tempFloat32Vec4 = new Float32Array(4);

/**
 * @private
 */
class VBOInstancingPointsLayer {

    /**
     * @param cfg
     * @param cfg.layerIndex
     * @param cfg.model
     * @param cfg.geometry
     * @param cfg.material
     * @param cfg.origin
     */
    constructor(cfg) {

      //  console.info("VBOInstancingPointsLayer");

        /**
         * Owner model
         * @type {VBOSceneModel}
         */
        this.model = cfg.model;

        /**
         * Shared material
         * @type {VBOSceneModelGeometry}
         */
        this.material = cfg.material;

        /**
         * State sorting key.
         * @type {string}
         */
        this.sortId = "PointsInstancingLayer";

        /**
         * Index of this InstancingLayer in VBOSceneModel#_layerList
         * @type {Number}
         */
        this.layerIndex = cfg.layerIndex;

        this._renderers = getRenderers$2(cfg.model.scene);
        this._aabb = math.collapseAABB3();

        this._state = new RenderState({
            obb: math.OBB3(),
            numInstances: 0,
            origin: cfg.origin ? math.vec3(cfg.origin) : null,
            geometry: cfg.geometry,
            positionsDecodeMatrix: cfg.geometry.positionsDecodeMatrix, // So we can null the geometry for GC
            colorsBuf: null,
            flagsBuf: null,
            offsetsBuf: null,
            modelMatrixCol0Buf: null,
            modelMatrixCol1Buf: null,
            modelMatrixCol2Buf: null,
            pickColorsBuf: null
        });

        // These counts are used to avoid unnecessary render passes
        this._numPortions = 0;
        this._numVisibleLayerPortions = 0;
        this._numTransparentLayerPortions = 0;
        this._numXRayedLayerPortions = 0;
        this._numHighlightedLayerPortions = 0;
        this._numSelectedLayerPortions = 0;
        this._numClippableLayerPortions = 0;
        this._numEdgesLayerPortions = 0;
        this._numPickableLayerPortions = 0;
        this._numCulledLayerPortions = 0;

        /** @private */
        this.numIndices = cfg.geometry.numIndices;

        // Per-instance arrays
        this._pickColors = [];
        this._offsets = [];

        // Modeling matrix per instance, array for each column
        this._modelMatrixCol0 = [];
        this._modelMatrixCol1 = [];
        this._modelMatrixCol2 = [];

        this._portions = [];
        this._meshes = [];
        this._aabb = math.collapseAABB3();
        this.aabbDirty = true;

        this._finalized = false;

        /**
         * The type of primitives in this layer.
         */
        this.primitive = cfg.geometry.primitive;
    }

    get aabb() {
        if (this.aabbDirty) {
            math.collapseAABB3(this._aabb);
            for (let i = 0, len = this._meshes.length; i < len; i++) {
                math.expandAABB3(this._aabb, this._meshes[i].aabb);
            }
            this.aabbDirty = false;
        }
        return this._aabb;
    }

    /**
     * Creates a new portion within this InstancingLayer, returns the new portion ID.
     *
     * The portion will instance this InstancingLayer's geometry.
     *
     * Gives the portion the specified color and matrix.
     *
     * @param mesh The SceneModelMesh that owns the portion
     * @param cfg Portion params
     * @param cfg.meshMatrix Flat float 4x4 matrix.
     * @param [cfg.worldMatrix] Flat float 4x4 matrix.
     * @param cfg.pickColor Quantized pick color
     * @returns {number} Portion ID.
     */
    createPortion(mesh, cfg) {

        const meshMatrix = cfg.meshMatrix;
        const pickColor = cfg.pickColor;

        if (this._finalized) {
            throw "Already finalized";
        }

        if (this.model.scene.entityOffsetsEnabled) {
            this._offsets.push(0);
            this._offsets.push(0);
            this._offsets.push(0);
        }

        this._modelMatrixCol0.push(meshMatrix[0]);
        this._modelMatrixCol0.push(meshMatrix[4]);
        this._modelMatrixCol0.push(meshMatrix[8]);
        this._modelMatrixCol0.push(meshMatrix[12]);

        this._modelMatrixCol1.push(meshMatrix[1]);
        this._modelMatrixCol1.push(meshMatrix[5]);
        this._modelMatrixCol1.push(meshMatrix[9]);
        this._modelMatrixCol1.push(meshMatrix[13]);

        this._modelMatrixCol2.push(meshMatrix[2]);
        this._modelMatrixCol2.push(meshMatrix[6]);
        this._modelMatrixCol2.push(meshMatrix[10]);
        this._modelMatrixCol2.push(meshMatrix[14]);

        // Per-instance pick colors

        this._pickColors.push(pickColor[0]);
        this._pickColors.push(pickColor[1]);
        this._pickColors.push(pickColor[2]);
        this._pickColors.push(pickColor[3]);

        this._state.numInstances++;

        const portionId = this._portions.length;
        this._portions.push({});

        this._numPortions++;
        this.model.numPortions++;
        this._meshes.push(mesh);
        return portionId;
    }

    finalize() {
        if (this._finalized) {
            throw "Already finalized";
        }
        const gl = this.model.scene.canvas.gl;
        const flagsLength = this._pickColors.length / 4;
        const state = this._state;
        const geometry = state.geometry;
        if (flagsLength > 0) {
            // Because we only build flags arrays here, 
            // get their length from the colors array
            let notNormalized = false;
            state.flagsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(flagsLength), flagsLength, 1, gl.DYNAMIC_DRAW, notNormalized);
        }
        if (this.model.scene.entityOffsetsEnabled) {
            if (this._offsets.length > 0) {
                const notNormalized = false;
                state.offsetsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._offsets), this._offsets.length, 3, gl.DYNAMIC_DRAW, notNormalized);
                this._offsets = []; // Release memory
            }
        }
        if (geometry.positionsCompressed && geometry.positionsCompressed.length > 0) {
            const normalized = false;
            state.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, geometry.positionsCompressed, geometry.positionsCompressed.length, 3, gl.STATIC_DRAW, normalized);
            state.positionsDecodeMatrix = math.mat4(geometry.positionsDecodeMatrix);
        }
        if (geometry.colorsCompressed && geometry.colorsCompressed.length > 0) {
            const colorsCompressed = new Uint8Array(geometry.colorsCompressed);
            const notNormalized = false;
            state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, colorsCompressed, colorsCompressed.length, 4, gl.STATIC_DRAW, notNormalized);
        }
        if (this._modelMatrixCol0.length > 0) {
            const normalized = false;
            state.modelMatrixCol0Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelMatrixCol0), this._modelMatrixCol0.length, 4, gl.STATIC_DRAW, normalized);
            state.modelMatrixCol1Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelMatrixCol1), this._modelMatrixCol1.length, 4, gl.STATIC_DRAW, normalized);
            state.modelMatrixCol2Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelMatrixCol2), this._modelMatrixCol2.length, 4, gl.STATIC_DRAW, normalized);
            this._modelMatrixCol0 = [];
            this._modelMatrixCol1 = [];
            this._modelMatrixCol2 = [];
        }
        if (this._pickColors.length > 0) {
            const normalized = false;
            state.pickColorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Uint8Array(this._pickColors), this._pickColors.length, 4, gl.STATIC_DRAW, normalized);
            this._pickColors = []; // Release memory
        }
        state.geometry = null;
        this._finalized = true;
    }

    // The following setters are called by VBOSceneModelMesh, in turn called by VBOSceneModelNode, only after the layer is finalized.
    // It's important that these are called after finalize() in order to maintain integrity of counts like _numVisibleLayerPortions etc.

    initFlags(portionId, flags, meshTransparent) {
        if (flags & ENTITY_FLAGS.VISIBLE) {
            this._numVisibleLayerPortions++;
            this.model.numVisibleLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.HIGHLIGHTED) {
            this._numHighlightedLayerPortions++;
            this.model.numHighlightedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.XRAYED) {
            this._numXRayedLayerPortions++;
            this.model.numXRayedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.SELECTED) {
            this._numSelectedLayerPortions++;
            this.model.numSelectedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.CLIPPABLE) {
            this._numClippableLayerPortions++;
            this.model.numClippableLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.EDGES) {
            this._numEdgesLayerPortions++;
            this.model.numEdgesLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.PICKABLE) {
            this._numPickableLayerPortions++;
            this.model.numPickableLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.CULLED) {
            this._numCulledLayerPortions++;
            this.model.numCulledLayerPortions++;
        }
        if (meshTransparent) {
            this._numTransparentLayerPortions++;
            this.model.numTransparentLayerPortions++;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setVisible(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.VISIBLE) {
            this._numVisibleLayerPortions++;
            this.model.numVisibleLayerPortions++;
        } else {
            this._numVisibleLayerPortions--;
            this.model.numVisibleLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setHighlighted(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.HIGHLIGHTED) {
            this._numHighlightedLayerPortions++;
            this.model.numHighlightedLayerPortions++;
        } else {
            this._numHighlightedLayerPortions--;
            this.model.numHighlightedLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setXRayed(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.XRAYED) {
            this._numXRayedLayerPortions++;
            this.model.numXRayedLayerPortions++;
        } else {
            this._numXRayedLayerPortions--;
            this.model.numXRayedLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setSelected(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.SELECTED) {
            this._numSelectedLayerPortions++;
            this.model.numSelectedLayerPortions++;
        } else {
            this._numSelectedLayerPortions--;
            this.model.numSelectedLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setEdges(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.EDGES) {
            this._numEdgesLayerPortions++;
            this.model.numEdgesLayerPortions++;
        } else {
            this._numEdgesLayerPortions--;
            this.model.numEdgesLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setClippable(portionId, flags) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.CLIPPABLE) {
            this._numClippableLayerPortions++;
            this.model.numClippableLayerPortions++;
        } else {
            this._numClippableLayerPortions--;
            this.model.numClippableLayerPortions--;
        }
        this._setFlags(portionId, flags);
    }

    setCollidable(portionId, flags) {
        if (!this._finalized) {
            throw "Not finalized";
        }
    }

    setPickable(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.PICKABLE) {
            this._numPickableLayerPortions++;
            this.model.numPickableLayerPortions++;
        } else {
            this._numPickableLayerPortions--;
            this.model.numPickableLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setCulled(portionId, flags, meshTransparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.CULLED) {
            this._numCulledLayerPortions++;
            this.model.numCulledLayerPortions++;
        } else {
            this._numCulledLayerPortions--;
            this.model.numCulledLayerPortions--;
        }
        this._setFlags(portionId, flags, meshTransparent);
    }

    setColor(portionId, color) { // RGBA color is normalized as ints
        if (!this._finalized) {
            throw "Not finalized";
        }
        tempUint8Vec4[0] = color[0];
        tempUint8Vec4[1] = color[1];
        tempUint8Vec4[2] = color[2];
        this._state.colorsBuf.setData(tempUint8Vec4, portionId * 3);
    }

    setTransparent(portionId, flags, transparent) {
        if (transparent) {
            this._numTransparentLayerPortions++;
            this.model.numTransparentLayerPortions++;
        } else {
            this._numTransparentLayerPortions--;
            this.model.numTransparentLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    // setMatrix(portionId, matrix) {

    ////////////////////////////////////////
    // TODO: Update portion matrix
    ////////////////////////////////////////
    //
    //     if (!this._finalized) {
    //         throw "Not finalized";
    //     }
    //
    //     var offset = portionId * 4;
    //
    //     tempFloat32Vec4[0] = matrix[0];
    //     tempFloat32Vec4[1] = matrix[4];
    //     tempFloat32Vec4[2] = matrix[8];
    //     tempFloat32Vec4[3] = matrix[12];
    //
    //     this._state.modelMatrixCol0Buf.setData(tempFloat32Vec4, offset);
    //
    //     tempFloat32Vec4[0] = matrix[1];
    //     tempFloat32Vec4[1] = matrix[5];
    //     tempFloat32Vec4[2] = matrix[9];
    //     tempFloat32Vec4[3] = matrix[13];
    //
    //     this._state.modelMatrixCol1Buf.setData(tempFloat32Vec4, offset);
    //
    //     tempFloat32Vec4[0] = matrix[2];
    //     tempFloat32Vec4[1] = matrix[6];
    //     tempFloat32Vec4[2] = matrix[10];
    //     tempFloat32Vec4[3] = matrix[14];
    //
    //     this._state.modelMatrixCol2Buf.setData(tempFloat32Vec4, offset);
    // }

    /**
     * flags are 4bits values encoded on a 32bit base. color flag on the first 4 bits, silhouette flag on the next 4 bits and so on for edge, pick and clippable.
     */
    _setFlags(portionId, flags, meshTransparent) {

        if (!this._finalized) {
            throw "Not finalized";
        }

        const visible = !!(flags & ENTITY_FLAGS.VISIBLE);
        const xrayed = !!(flags & ENTITY_FLAGS.XRAYED);
        const highlighted = !!(flags & ENTITY_FLAGS.HIGHLIGHTED);
        const selected = !!(flags & ENTITY_FLAGS.SELECTED);
        const edges = !!(flags & ENTITY_FLAGS.EDGES);
        const pickable = !!(flags & ENTITY_FLAGS.PICKABLE);
        const culled = !!(flags & ENTITY_FLAGS.CULLED);

        let colorFlag;
        if (!visible || culled || xrayed
            || (highlighted && !this.model.scene.highlightMaterial.glowThrough)
            || (selected && !this.model.scene.selectedMaterial.glowThrough)) {
            colorFlag = RENDER_PASSES.NOT_RENDERED;
        } else {
            if (meshTransparent) {
                colorFlag = RENDER_PASSES.COLOR_TRANSPARENT;
            } else {
                colorFlag = RENDER_PASSES.COLOR_OPAQUE;
            }
        }

        let silhouetteFlag;
        if (!visible || culled) {
            silhouetteFlag = RENDER_PASSES.NOT_RENDERED;
        } else if (selected) {
            silhouetteFlag = RENDER_PASSES.SILHOUETTE_SELECTED;
        } else if (highlighted) {
            silhouetteFlag = RENDER_PASSES.SILHOUETTE_HIGHLIGHTED;
        } else if (xrayed) {
            silhouetteFlag = RENDER_PASSES.SILHOUETTE_XRAYED;
        } else {
            silhouetteFlag = RENDER_PASSES.NOT_RENDERED;
        }

        let edgeFlag = 0;
        if (!visible || culled) {
            edgeFlag = RENDER_PASSES.NOT_RENDERED;
        } else if (selected) {
            edgeFlag = RENDER_PASSES.EDGES_SELECTED;
        } else if (highlighted) {
            edgeFlag = RENDER_PASSES.EDGES_HIGHLIGHTED;
        } else if (xrayed) {
            edgeFlag = RENDER_PASSES.EDGES_XRAYED;
        } else if (edges) {
            if (meshTransparent) {
                edgeFlag = RENDER_PASSES.EDGES_COLOR_TRANSPARENT;
            } else {
                edgeFlag = RENDER_PASSES.EDGES_COLOR_OPAQUE;
            }
        } else {
            edgeFlag = RENDER_PASSES.NOT_RENDERED;
        }

        let pickFlag = (visible && !culled && pickable) ? RENDER_PASSES.PICK : RENDER_PASSES.NOT_RENDERED;

        const clippableFlag = !!(flags & ENTITY_FLAGS.CLIPPABLE) ? 255 : 0;

        let vertFlag = 0;
        vertFlag |= colorFlag;
        vertFlag |= silhouetteFlag << 4;
        vertFlag |= edgeFlag << 8;
        vertFlag |= pickFlag << 12;
        vertFlag |= clippableFlag << 16;

        tempFloat32[0] = vertFlag;

        this._state.flagsBuf.setData(tempFloat32, portionId);
    }

    setOffset(portionId, offset) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (!this.model.scene.entityOffsetsEnabled) {
            this.model.error("Entity#offset not enabled for this Viewer"); // See Viewer entityOffsetsEnabled
            return;
        }
        tempVec3fa[0] = offset[0];
        tempVec3fa[1] = offset[1];
        tempVec3fa[2] = offset[2];
        this._state.offsetsBuf.setData(tempVec3fa, portionId * 3);
    }

    setMatrix(portionId, matrix) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        const offset = portionId * 4;

        tempFloat32Vec4[0] = matrix[0];
        tempFloat32Vec4[1] = matrix[4];
        tempFloat32Vec4[2] = matrix[8];
        tempFloat32Vec4[3] = matrix[12];

        this._state.modelMatrixCol0Buf.setData(tempFloat32Vec4, offset);

        tempFloat32Vec4[0] = matrix[1];
        tempFloat32Vec4[1] = matrix[5];
        tempFloat32Vec4[2] = matrix[9];
        tempFloat32Vec4[3] = matrix[13];

        this._state.modelMatrixCol1Buf.setData(tempFloat32Vec4, offset);

        tempFloat32Vec4[0] = matrix[2];
        tempFloat32Vec4[1] = matrix[6];
        tempFloat32Vec4[2] = matrix[10];
        tempFloat32Vec4[3] = matrix[14];

        this._state.modelMatrixCol2Buf.setData(tempFloat32Vec4, offset);
    }

    // ---------------------- NORMAL RENDERING -----------------------------------

    drawColorOpaque(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        if (this._renderers.colorRenderer) {
            this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
        }
    }

    drawColorTransparent(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === 0 || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        if (this._renderers.colorRenderer) {
            this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT);
        }
    }

    // -- RENDERING SAO POST EFFECT TARGETS ----------------------------------------------------------------------------

    drawDepth(renderFlags, frameCtx) {
    }

    drawNormals(renderFlags, frameCtx) {
    }

    // ---------------------- EMPHASIS RENDERING -----------------------------------

    drawSilhouetteXRayed(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) {
            return;
        }
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_XRAYED);
        }
    }

    drawSilhouetteHighlighted(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) {
            return;
        }
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_HIGHLIGHTED);
        }
    }

    drawSilhouetteSelected(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) {
            return;
        }
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_SELECTED);
        }
    }

    //-- EDGES RENDERING -----------------------------------------------------------------------------------------------

    drawEdgesColorOpaque(renderFlags, frameCtx) {
    }

    drawEdgesColorTransparent(renderFlags, frameCtx) {
    }

    drawEdgesHighlighted(renderFlags, frameCtx) {
    }

    drawEdgesSelected(renderFlags, frameCtx) {
    }

    drawEdgesXRayed(renderFlags, frameCtx) {
    }

    // ---------------------- OCCLUSION CULL RENDERING -----------------------------------

    drawOcclusion(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        if (this._renderers.occlusionRenderer) {
            // Only opaque, filled objects can be occluders
            this._renderers.occlusionRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
        }
    }

    // ---------------------- SHADOW BUFFER RENDERING -----------------------------------

    drawShadow(renderFlags, frameCtx) {
    }

    //---- PICKING ----------------------------------------------------------------------------------------------------

    drawPickMesh(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        if (this._renderers.pickMeshRenderer) {
            this._renderers.pickMeshRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawPickDepths(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        if (this._renderers.pickDepthRenderer) {
            this._renderers.pickDepthRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawPickNormals(renderFlags, frameCtx) {
    }

    drawSnapInit(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        if (this._renderers.snapInitRenderer) {
            this._renderers.snapInitRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawSnap(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        if (this._renderers.snapRenderer) {
            this._renderers.snapRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    destroy() {
        const state = this._state;
        if (state.colorsBuf) {
            state.colorsBuf.destroy();
            state.colorsBuf = null;
        }
        if (state.flagsBuf) {
            state.flagsBuf.destroy();
            state.flagsBuf = null;
        }
        if (state.offsetsBuf) {
            state.offsetsBuf.destroy();
            state.offsetsBuf = null;
        }
        if (state.modelMatrixCol0Buf) {
            state.modelMatrixCol0Buf.destroy();
            state.modelMatrixCol0Buf = null;
        }
        if (state.modelMatrixCol1Buf) {
            state.modelMatrixCol1Buf.destroy();
            state.modelMatrixCol1Buf = null;
        }
        if (state.modelMatrixCol2Buf) {
            state.modelMatrixCol2Buf.destroy();
            state.modelMatrixCol2Buf = null;
        }
        if (state.pickColorsBuf) {
            state.pickColorsBuf.destroy();
            state.pickColorsBuf = null;
        }
        state.destroy();
    }
}

const tempVec3a$q = math.vec3();
const tempVec3b$m = math.vec3();
const tempMat4a$e = math.mat4();

/**
 * @private
 */
class DTXLinesColorRenderer {

    constructor(scene) {
        this._scene = scene;
        this._hash = this._getHash();
        this._allocate();
    }

    getValid() {
        return this._hash === this._getHash();
    };

    _getHash() {
        return this._scene._sectionPlanesState.getHash();
    }

    drawLayer(frameCtx, dataTextureLayer, renderPass) {

        const scene = this._scene;
        const camera = scene.camera;
        const model = dataTextureLayer.model;
        const gl = scene.canvas.gl;
        const state = dataTextureLayer._state;
        const textureState = state.textureState;
        const origin = dataTextureLayer._state.origin;
        const {position, rotationMatrix} = model;
        const viewMatrix = camera.viewMatrix;

        if (!this._program) {
            this._allocate();
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram(frameCtx, state);
        }

        textureState.bindCommonTextures(
            this._program,
            this.uPerObjectDecodeMatrix,
            this._uPerVertexPosition,
            this.uPerObjectColorAndFlags,
            this._uPerObjectMatrix
        );

        let rtcViewMatrix;

        const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0);
        const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0);
        if (gotOrigin || gotPosition) {
            const rtcOrigin = tempVec3a$q;
            if (gotOrigin) {
                const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3b$m);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$e);
        } else {
            rtcViewMatrix = viewMatrix;
        }

        gl.uniformMatrix4fv(this._uSceneModelMatrix, false, rotationMatrix);
        gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix);
        gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix);
        gl.uniform1i(this._uRenderPass, renderPass);

        if (scene.logarithmicDepthBufferEnabled) {
            const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }

        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes;
            const renderFlags = model.renderFlags;
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$q, model.matrix);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }

        if (state.numIndices8Bits > 0) {
            textureState.bindLineIndicesTextures(
                this._program,
                this._uPerLineObject,
                this._uPerLineIndices,
                8 // 8 bits indices
            );
            gl.drawArrays(gl.LINES, 0, state.numIndices8Bits);
        }

        if (state.numIndices16Bits > 0) {
            textureState.bindLineIndicesTextures(
                this._program,
                this._uPerLineObject,
                this._uPerLineIndices,
                16 // 16 bits indices
            );
            gl.drawArrays(gl.LINES, 0, state.numIndices16Bits);
        }

        if (state.numIndices32Bits > 0) {
            textureState.bindLineIndicesTextures(
                this._program,
                this._uPerLineObject,
                this._uPerLineIndices,
                32 // 32 bits indices
            );
            gl.drawArrays(gl.LINES, 0, state.numIndices32Bits);
        }

        frameCtx.drawElements++;
    }

    _allocate() {
        const scene = this._scene;
        const gl = scene.canvas.gl;
        this._program = new Program(gl, this._buildShader());
        if (this._program.errors) {
            this.errors = this._program.errors;
            console.error(this.errors);
            return;
        }
        const program = this._program;
        this._uRenderPass = program.getLocation("renderPass");
        this._uSceneModelMatrix = program.getLocation("sceneModelMatrix");
        this._uViewMatrix = program.getLocation("viewMatrix");
        this._uProjMatrix = program.getLocation("projMatrix");
        this._uSectionPlanes = [];
        for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            this._uSectionPlanes.push({
                active: program.getLocation("sectionPlaneActive" + i),
                pos: program.getLocation("sectionPlanePos" + i),
                dir: program.getLocation("sectionPlaneDir" + i)
            });
        }
        if (scene.logarithmicDepthBufferEnabled) {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
        this.uPerObjectDecodeMatrix = "uPerObjectDecodeMatrix";
        this.uPerObjectColorAndFlags = "uPerObjectColorAndFlags";
        this._uPerVertexPosition = "uPerVertexPosition";
        this._uPerLineIndices = "uPerLineIndices";
        this._uPerLineObject = "uPerLineObject";
        this._uPerObjectMatrix= "uPerObjectMatrix";
        this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc");
    }

    _bindProgram(frameCtx) {
        const scene = this._scene;
        const gl = scene.canvas.gl;
        const program = this._program;
        const project = scene.camera.project;
        program.bind();
        if (scene.logarithmicDepthBufferEnabled) {
            const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }
    }

    _buildShader() {
        return {
            vertex: this._buildVertexShader(),
            fragment: this._buildFragmentShader()
        };
    }

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// LinesDataTextureColorRenderer");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");

        src.push("uniform int renderPass;");

        if (scene.entityOffsetsEnabled) ;

        src.push("uniform mat4 sceneModelMatrix;");
        src.push("uniform mat4 viewMatrix;");
        src.push("uniform mat4 projMatrix;");

        src.push("uniform highp sampler2D uPerObjectDecodeMatrix;");
        src.push("uniform highp sampler2D uPerObjectMatrix;");
        src.push("uniform lowp usampler2D uPerObjectColorAndFlags;");
        src.push("uniform mediump usampler2D uPerVertexPosition;");
        src.push("uniform highp usampler2D uPerLineIndices;");
        src.push("uniform mediump usampler2D uPerLineObject;");

        //  src.push("uniform vec4 color;");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("flat out uint vFlags2;");
        }
        src.push("out vec4 vColor;");
        src.push("void main(void) {");

        src.push("  int lineIndex = gl_VertexID / 2;");

        src.push("  int h_packed_object_id_index = (lineIndex >> 3) & 4095;");
        src.push("  int v_packed_object_id_index = (lineIndex >> 3) >> 12;");

        src.push("  int objectIndex = int(texelFetch(uPerLineObject, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);");

        src.push("  ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);");

        src.push("  uvec4 flags = texelFetch (uPerObjectColorAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);");
        src.push("  uvec4 flags2 = texelFetch (uPerObjectColorAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);");

        // flags.x = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE

        src.push(`  if (int(flags.x) != renderPass) {`);
        src.push("      gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("      return;"); // Cull vertex
        src.push("  } else {");

        src.push("      ivec4 packedVertexBase = ivec4(texelFetch (uPerObjectColorAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));");
        src.push("      ivec4 packedLineIndexBaseOffset = ivec4(texelFetch (uPerObjectColorAndFlags, ivec2(objectIndexCoords.x*8+6, objectIndexCoords.y), 0));");
        src.push("      int lineIndexBaseOffset = (packedLineIndexBaseOffset.r << 24) + (packedLineIndexBaseOffset.g << 16) + (packedLineIndexBaseOffset.b << 8) + packedLineIndexBaseOffset.a;");
        src.push("      int h_index = (lineIndex - lineIndexBaseOffset) & 4095;");
        src.push("      int v_index = (lineIndex - lineIndexBaseOffset) >> 12;");
        src.push("      ivec3 vertexIndices = ivec3(texelFetch(uPerLineIndices, ivec2(h_index, v_index), 0));");
        src.push("      ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;");

        src.push("      int indexPositionH = uniqueVertexIndexes[gl_VertexID % 2] & 4095;");
        src.push("      int indexPositionV = uniqueVertexIndexes[gl_VertexID % 2] >> 12;");

        src.push("      mat4 objectInstanceMatrix = mat4 (texelFetch (uPerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uPerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uPerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uPerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");
        src.push("      mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uPerObjectDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uPerObjectDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uPerObjectDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uPerObjectDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");

        src.push("      uvec4 flags = texelFetch (uPerObjectColorAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);");
        src.push("      uvec4 flags2 = texelFetch (uPerObjectColorAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);");

        src.push("      vec3 position = vec3(texelFetch(uPerVertexPosition, ivec2(indexPositionH, indexPositionV), 0));");

        src.push("      uvec4 color = texelFetch (uPerObjectColorAndFlags, ivec2(objectIndexCoords.x*8+0, objectIndexCoords.y), 0);");
        src.push(`      if (color.a == 0u) {`);
        src.push("          gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("          return;");
        src.push("      };");
        src.push("      vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); ");
        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("      vWorldPosition = worldPosition;");
            src.push("      vFlags2 = flags2.r;");
        }
        src.push("      vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("      vFragDepth = 1.0 + clipPos.w;");
            src.push("      isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("      gl_Position = clipPos;");
        src.push("      vec4 rgb = vec4(color.rgba);");
        src.push("      vColor = vec4(float(rgb.r*0.5) / 255.0, float(rgb.g*0.5) / 255.0, float(rgb.b*0.5) / 255.0, float(rgb.a) / 255.0);");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// LinesDataTextureColorRenderer fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("flat in uint vFlags2;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = vFlags2 > 0u;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > 0.0) { ");
            src.push("      discard;");
            src.push("  }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
             src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("   outColor            = vColor;");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

/**
 * @private
 */
class DTXLinesRenderers {

    constructor(scene) {
        this._scene = scene;
    }

    _compile() {
        if (this._colorRenderer && (!this._colorRenderer.getValid())) {
            this._colorRenderer.destroy();
            this._colorRenderer = null;
        }
       
    }

    eagerCreateRenders() {

        // Pre-initialize certain renderers that would otherwise be lazy-initialised
        // on user interaction, such as picking or emphasis, so that there is no delay
        // when user first begins interacting with the viewer.
       
    }
    
    get colorRenderer() {
        if (!this._colorRenderer) {
            this._colorRenderer = new DTXLinesColorRenderer(this._scene, false);
        }
        return this._colorRenderer;
    }
    
    _destroy() {
        if (this._colorRenderer) {
            this._colorRenderer.destroy();
        }
    }
}

const cachedRenderers = {};

/**
 * @private
 */
function getRenderers$1(scene) {
    const sceneId = scene.id;
    let dataTextureRenderers = cachedRenderers[sceneId];
    if (!dataTextureRenderers) {
        dataTextureRenderers = new DTXLinesRenderers(scene);
        cachedRenderers[sceneId] = dataTextureRenderers;
        dataTextureRenderers._compile();
        dataTextureRenderers.eagerCreateRenders();
        scene.on("compile", () => {
            dataTextureRenderers._compile();
            dataTextureRenderers.eagerCreateRenders();
        });
        scene.on("destroyed", () => {
            delete cachedRenderers[sceneId];
            dataTextureRenderers._destroy();
        });
    }
    return dataTextureRenderers;
}

/**
 * @private
 */
class DTXLinesBuffer {

    constructor() {
        this.positionsCompressed = [];
        this.lenPositionsCompressed = 0;
        this.indices8Bits = [];
        this.lenIndices8Bits = 0;
        this.indices16Bits = [];
        this.lenIndices16Bits = 0;
        this.indices32Bits = [];
        this.lenIndices32Bits = 0;
        this.perObjectColors = [];
        this.perObjectPickColors = [];
        this.perObjectSolid = [];
        this.perObjectOffsets = [];
        this.perObjectPositionsDecodeMatrices = [];
        this.perObjectInstancePositioningMatrices = [];
        this.perObjectVertexBases = [];
        this.perObjectIndexBaseOffsets = [];
        this.perLineNumberPortionId8Bits = [];
        this.perLineNumberPortionId16Bits = [];
        this.perLineNumberPortionId32Bits = [];
    }
}

/**
 * @private
 */
class DTXLinesState {

    constructor() {
        this.texturePerObjectColorsAndFlags = null;
        this.texturePerObjectOffsets = null;
        this.texturePerObjectInstanceMatrices = null;
        this.texturePerObjectPositionsDecodeMatrix = null;
        this.texturePerVertexIdCoordinates = null;
        this.texturePerLineIdPortionIds8Bits = null;
        this.texturePerLineIdPortionIds16Bits = null;
        this.texturePerLineIdPortionIds32Bits = null;
        this.texturePerLineIdIndices8Bits = null;
        this.texturePerLineIdIndices16Bits = null;
        this.texturePerLineIdIndices32Bits = null;
        this.textureModelMatrices = null;
    }

    finalize() {
        this.indicesPerBitnessTextures = {
            8: this.texturePerLineIdIndices8Bits,
            16: this.texturePerLineIdIndices16Bits,
            32: this.texturePerLineIdIndices32Bits,
        };
        this.indicesPortionIdsPerBitnessTextures = {
            8: this.texturePerLineIdPortionIds8Bits,
            16: this.texturePerLineIdPortionIds16Bits,
            32: this.texturePerLineIdPortionIds32Bits,
        };
    }

    bindCommonTextures(glProgram, objectDecodeMatricesShaderName, vertexTextureShaderName, objectAttributesTextureShaderName, objectMatricesShaderName) {
        this.texturePerObjectPositionsDecodeMatrix.bindTexture(glProgram, objectDecodeMatricesShaderName, 1);
        this.texturePerVertexIdCoordinates.bindTexture(glProgram, vertexTextureShaderName, 2);
        this.texturePerObjectColorsAndFlags.bindTexture(glProgram, objectAttributesTextureShaderName, 3);
        this.texturePerObjectInstanceMatrices.bindTexture(glProgram, objectMatricesShaderName, 4);
    }

    bindLineIndicesTextures(glProgram, portionIdsShaderName, lineIndicesShaderName, textureBitness) {
        this.indicesPortionIdsPerBitnessTextures[textureBitness].bindTexture(glProgram, portionIdsShaderName, 5);
        this.indicesPerBitnessTextures[textureBitness].bindTexture(glProgram, lineIndicesShaderName, 6);
    }
}

/**
 * @private
 */
class BindableDataTexture {

    constructor(gl, texture, textureWidth, textureHeight, textureData = null) {
        this._gl = gl;
        this._texture = texture;
        this._textureWidth = textureWidth;
        this._textureHeight = textureHeight;
        this._textureData = textureData;
    }

    bindTexture(glProgram, shaderName, glTextureUnit) {
        return glProgram.bindTexture(shaderName, this, glTextureUnit);
    }

    bind(unit) {
        this._gl.activeTexture(this._gl["TEXTURE" + unit]);
        this._gl.bindTexture(this._gl.TEXTURE_2D, this._texture);
        return true;
    }

    unbind(unit) {
        // This `unbind` method is ignored at the moment to allow avoiding
        // to rebind same texture already bound to a texture unit.

        // this._gl.activeTexture(this.state.gl["TEXTURE" + unit]);
        // this._gl.bindTexture(this.state.gl.TEXTURE_2D, null);
    }
}

const dataTextureRamStats$1 = {
    sizeDataColorsAndFlags: 0,
    sizeDataPositionDecodeMatrices: 0,
    sizeDataTextureOffsets: 0,
    sizeDataTexturePositions: 0,
    sizeDataTextureIndices: 0,
    sizeDataTexturePortionIds: 0,
    numberOfGeometries: 0,
    numberOfPortions: 0,
    numberOfLayers: 0,
    numberOfTextures: 0,
    totalLines: 0,
    totalLines8Bits: 0,
    totalLines16Bits: 0,
    totalLines32Bits: 0,
    cannotCreatePortion: {
        because10BitsObjectId: 0,
        becauseTextureSize: 0,
    },
    overheadSizeAlignementIndices: 0,
    overheadSizeAlignementEdgeIndices: 0,
};

window.printDataTextureRamStats = function () {

    console.log(JSON.stringify(dataTextureRamStats$1, null, 4));

    let totalRamSize = 0;

    Object.keys(dataTextureRamStats$1).forEach(key => {
        if (key.startsWith("size")) {
            totalRamSize += dataTextureRamStats$1[key];
        }
    });

    console.log(`Total size ${totalRamSize} bytes (${(totalRamSize / 1000 / 1000).toFixed(2)} MB)`);
    console.log(`Avg bytes / triangle: ${(totalRamSize / dataTextureRamStats$1.totalLines).toFixed(2)}`);

    let percentualRamStats = {};

    Object.keys(dataTextureRamStats$1).forEach(key => {
        if (key.startsWith("size")) {
            percentualRamStats[key] =
                `${(dataTextureRamStats$1[key] / totalRamSize * 100).toFixed(2)} % of total`;
        }
    });

    console.log(JSON.stringify({percentualRamUsage: percentualRamStats}, null, 4));
};

/**
 * @private
 */
class DTXLinesTextureFactory {
    /**
     * Enables the currently binded ````WebGLTexture```` to be used as a data texture.
     *
     * @param {WebGL2RenderingContext} gl
     *
     * @private
     */
    disableBindedTextureFiltering(gl) {
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    }

    /**
     * This will generate an RGBA texture for:
     * - colors
     * - pickColors
     * - flags
     * - flags2
     * - vertex bases
     * - vertex base offsets
     *
     * The texture will have:
     * - 4 RGBA columns per row: for each object (pick) color and flags(2)
     * - N rows where N is the number of objects
     *
     * @param {WebGL2RenderingContext} gl
     * @param {ArrayLike<ArrayLike<int>>} colors Array of colors for all objects in the layer
     * @param {ArrayLike<ArrayLike<int>>} pickColors Array of pickColors for all objects in the layer
     * @param {ArrayLike<int>} vertexBases Array of position-index-bases foteh all objects in the layer
     * @param {ArrayLike<int>} indexBaseOffsets For lines: array of offests between the (gl_VertexID / 2) and the position where the indices start in the texture layer
     *
     * @returns {BindableDataTexture}
     */
    generateTextureForColorsAndFlags(gl, colors, pickColors, vertexBases, indexBaseOffsets) {
        const numPortions = colors.length;

        // The number of rows in the texture is the number of
        // objects in the layer.

        this.numPortions = numPortions;

        const textureWidth = 512 * 8;
        const textureHeight = Math.ceil(numPortions / (textureWidth / 8));

        if (textureHeight === 0) {
            throw "texture height===0";
        }

        // 8 columns per texture row:
        // - col0: (RGBA) object color RGBA
        // - col1: (packed Uint32 as RGBA) object pick color
        // - col2: (packed 4 bytes as RGBA) object flags
        // - col3: (packed 4 bytes as RGBA) object flags2
        // - col4: (packed Uint32 bytes as RGBA) vertex base
        // - col5: (packed Uint32 bytes as RGBA) index base offset


       // // - col6: (packed Uint32 bytes as RGBA) edge index base offset
       // // - col7: (packed 4 bytes as RGBA) is-solid flag for objects

        const texArray = new Uint8Array(4 * textureWidth * textureHeight);

        dataTextureRamStats$1.sizeDataColorsAndFlags += texArray.byteLength;
        dataTextureRamStats$1.numberOfTextures++;

        for (let i = 0; i < numPortions; i++) {
            // object color
            texArray.set(colors [i], i * 32 + 0);
            texArray.set(pickColors [i], i * 32 + 4); // object pick color
            texArray.set([0, 0, 0, 0], i * 32 + 8);     // object flags
            texArray.set([0, 0, 0, 0], i * 32 + 12);        // object flags2

            // vertex base
            texArray.set([
                    (vertexBases[i] >> 24) & 255,
                    (vertexBases[i] >> 16) & 255,
                    (vertexBases[i] >> 8) & 255,
                    (vertexBases[i]) & 255,
                ],
                i * 32 + 16
            );

            // lines index base offset
            texArray.set(
                [
                    (indexBaseOffsets[i] >> 24) & 255,
                    (indexBaseOffsets[i] >> 16) & 255,
                    (indexBaseOffsets[i] >> 8) & 255,
                    (indexBaseOffsets[i]) & 255,
                ],
                i * 32 + 20
            );

            // // edge index base offset
            // texArray.set(
            //     [
            //         (edgeIndexBaseOffsets[i] >> 24) & 255,
            //         (edgeIndexBaseOffsets[i] >> 16) & 255,
            //         (edgeIndexBaseOffsets[i] >> 8) & 255,
            //         (edgeIndexBaseOffsets[i]) & 255,
            //     ],
            //     i * 32 + 24
            // );
            //
            // // is-solid flag
            // texArray.set([solid[i] ? 1 : 0, 0, 0, 0], i * 32 + 28);
        }
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8UI, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGBA_INTEGER, gl.UNSIGNED_BYTE, texArray, 0);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight, texArray);
    }

    /**
     * This will generate a texture for all object offsets.
     *
     * @param {WebGL2RenderingContext} gl
     * @param {int[]} offsets Array of int[3], one XYZ offset array for each object
     *
     * @returns {BindableDataTexture}
     */
    generateTextureForObjectOffsets(gl, numOffsets) {
        const textureWidth = 512;
        const textureHeight = Math.ceil(numOffsets / textureWidth);
        if (textureHeight === 0) {
            throw "texture height===0";
        }
        const texArray = new Float32Array(3 * textureWidth * textureHeight).fill(0);
        dataTextureRamStats$1.sizeDataTextureOffsets += texArray.byteLength;
        dataTextureRamStats$1.numberOfTextures++;
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB32F, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB, gl.FLOAT, texArray, 0);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight, texArray);
    }

    /**
     * This will generate a texture for all positions decode matrices in the layer.
     *
     * The texture will have:
     * - 4 RGBA columns per row (each column will contain 4 packed half-float (16 bits) components).
     *   Thus, each row will contain 16 packed half-floats corresponding to a complete positions decode matrix)
     * - N rows where N is the number of objects
     *
     * @param {WebGL2RenderingContext} gl
    * @param {ArrayLike<Matrix4x4>} instanceMatrices Array of geometry instancing matrices for all objects in the layer. Null if the objects are not instanced.
     *
     * @returns {BindableDataTexture}
     */
    generateTextureForInstancingMatrices(gl, instanceMatrices) {
        const numMatrices = instanceMatrices.length;
        if (numMatrices === 0) {
            throw "num instance matrices===0";
        }
        // in one row we can fit 512 matrices
        const textureWidth = 512 * 4;
        const textureHeight = Math.ceil(numMatrices / (textureWidth / 4));
        const texArray = new Float32Array(4 * textureWidth * textureHeight);
       // dataTextureRamStats.sizeDataPositionDecodeMatrices += texArray.byteLength;
        dataTextureRamStats$1.numberOfTextures++;
        for (let i = 0; i < instanceMatrices.length; i++) {            // 4x4 values
            texArray.set(instanceMatrices[i], i * 16);
        }
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGBA, gl.FLOAT, texArray, 0);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight, texArray);
    }

    /**
     * This will generate a texture for all positions decode matrices in the layer.
     *
     * The texture will have:
     * - 4 RGBA columns per row (each column will contain 4 packed half-float (16 bits) components).
     *   Thus, each row will contain 16 packed half-floats corresponding to a complete positions decode matrix)
     * - N rows where N is the number of objects
     *
     * @param {WebGL2RenderingContext} gl
     * @param {ArrayLike<Matrix4x4>} positionDecodeMatrices Array of positions decode matrices for all objects in the layer
     *
     * @returns {BindableDataTexture}
     */
    generateTextureForPositionsDecodeMatrices(gl, positionDecodeMatrices) {
        const numMatrices = positionDecodeMatrices.length;
        if (numMatrices === 0) {
            throw "num decode+entity matrices===0";
        }
        // in one row we can fit 512 matrices
        const textureWidth = 512 * 4;
        const textureHeight = Math.ceil(numMatrices / (textureWidth / 4));
        const texArray = new Float32Array(4 * textureWidth * textureHeight);
        dataTextureRamStats$1.sizeDataPositionDecodeMatrices += texArray.byteLength;
        dataTextureRamStats$1.numberOfTextures++;
        for (let i = 0; i < positionDecodeMatrices.length; i++) {            // 4x4 values
            texArray.set(positionDecodeMatrices[i], i * 16);
        }
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGBA, gl.FLOAT, texArray, 0);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight);
    }

    /**
     * @param {WebGL2RenderingContext} gl
     * @param indicesArrays
     * @param lenIndices
     *
     * @returns {BindableDataTexture}
     */
    generateTextureFor8BitIndices(gl, indicesArrays, lenIndices) {
        if (lenIndices === 0) {
            return {texture: null, textureHeight: 0,};
        }
        const textureWidth = 4096;
        const textureHeight = Math.ceil(lenIndices / 3 / textureWidth);
        if (textureHeight === 0) {
            throw "texture height===0";
        }
        const texArraySize = textureWidth * textureHeight * 3;
        const texArray = new Uint8Array(texArraySize);
        dataTextureRamStats$1.sizeDataTextureIndices += texArray.byteLength;
        dataTextureRamStats$1.numberOfTextures++;
        for (let i = 0, j = 0, len = indicesArrays.length; i < len; i++) {
            const pc = indicesArrays[i];
            texArray.set(pc, j);
            j += pc.length;
        }
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB8UI, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB_INTEGER, gl.UNSIGNED_BYTE, texArray, 0);
        this.disableBindedTextureFiltering(gl);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight);
    }

    /**
     * @param {WebGL2RenderingContext} gl
     * @param indicesArrays
     * @param lenIndices
     *
     * @returns {BindableDataTexture}
     */
    generateTextureFor16BitIndices(gl, indicesArrays, lenIndices) {
        if (lenIndices === 0) {
            return {texture: null, textureHeight: 0,};
        }
        const textureWidth = 4096;
        const textureHeight = Math.ceil(lenIndices / 3 / textureWidth);
        if (textureHeight === 0) {
            throw "texture height===0";
        }
        const texArraySize = textureWidth * textureHeight * 3;
        const texArray = new Uint16Array(texArraySize);
        dataTextureRamStats$1.sizeDataTextureIndices += texArray.byteLength;
        dataTextureRamStats$1.numberOfTextures++;
        for (let i = 0, j = 0, len = indicesArrays.length; i < len; i++) {
            const pc = indicesArrays[i];
            texArray.set(pc, j);
            j += pc.length;
        }
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB16UI, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB_INTEGER, gl.UNSIGNED_SHORT, texArray, 0);
        this.disableBindedTextureFiltering(gl);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight);
    }

    /**
     * @param {WebGL2RenderingContext} gl
     * @param indicesArrays
     * @param lenIndices
     *
     * @returns {BindableDataTexture}
     */
    generateTextureFor32BitIndices(gl, indicesArrays, lenIndices) {
        if (lenIndices === 0) {
            return {texture: null, textureHeight: 0,};
        }
        const textureWidth = 4096;
        const textureHeight = Math.ceil(lenIndices / 3 / textureWidth);
        if (textureHeight === 0) {
            throw "texture height===0";
        }
        const texArraySize = textureWidth * textureHeight * 3;
        const texArray = new Uint32Array(texArraySize);
        dataTextureRamStats$1.sizeDataTextureIndices += texArray.byteLength;
        dataTextureRamStats$1.numberOfTextures++;
        for (let i = 0, j = 0, len = indicesArrays.length; i < len; i++) {
            const pc = indicesArrays[i];
            texArray.set(pc, j);
            j += pc.length;
        }
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB32UI, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB_INTEGER, gl.UNSIGNED_INT, texArray, 0);
        this.disableBindedTextureFiltering(gl);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight);
    }

    /**
     * @param {WebGL2RenderingContext} gl
     * @param {ArrayLike<int>} positionsArrays Arrays of  quantized positions in the layer
     * @param lenPositions
     *
     * This will generate a texture for positions in the layer.
     *
     * The texture will have:
     * - 1024 columns, where each pixel will be a 16-bit-per-component RGB texture, corresponding to the XYZ of the position
     * - a number of rows R where R*1024 is just >= than the number of vertices (positions / 3)
     *
     * @returns {BindableDataTexture}
     */
    generateTextureForPositions(gl, positionsArrays, lenPositions) {
        const numVertices = lenPositions / 3;
        const textureWidth = 4096;
        const textureHeight = Math.ceil(numVertices / textureWidth);
        if (textureHeight === 0) {
            throw "texture height===0";
        }
        const texArraySize = textureWidth * textureHeight * 3;
        const texArray = new Uint16Array(texArraySize);
        dataTextureRamStats$1.sizeDataTexturePositions += texArray.byteLength;
        dataTextureRamStats$1.numberOfTextures++;
        for (let i = 0, j = 0, len = positionsArrays.length; i < len; i++) {
            const pc = positionsArrays[i];
            texArray.set(pc, j);
            j += pc.length;
        }
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB16UI, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB_INTEGER, gl.UNSIGNED_SHORT, texArray, 0);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight);
    }

    /**
     * @param {WebGL2RenderingContext} gl
     * @param {ArrayLike<int>} portionIdsArray
     *
     * @returns {BindableDataTexture}
     */
    generateTextureForPackedPortionIds(gl, portionIdsArray) {
        if (portionIdsArray.length === 0) {
            return {texture: null, textureHeight: 0};
        }
        const lenArray = portionIdsArray.length;
        const textureWidth = 4096;
        const textureHeight = Math.ceil(lenArray / textureWidth);
        if (textureHeight === 0) {
            throw "texture height===0";
        }
        const texArraySize = textureWidth * textureHeight;
        const texArray = new Uint16Array(texArraySize);
        texArray.set(portionIdsArray, 0);
        dataTextureRamStats$1.sizeDataTexturePortionIds += texArray.byteLength;
        dataTextureRamStats$1.numberOfTextures++;
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.R16UI, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RED_INTEGER, gl.UNSIGNED_SHORT, texArray, 0);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight);
    }
}

const configs$1 = new Configs();

const MAX_NUMBER_OF_OBJECTS_IN_LAYER$1 = (1 << 16);
const MAX_DATA_TEXTURE_HEIGHT$1 = configs$1.maxDataTextureHeight;
const INDICES_EDGE_INDICES_ALIGNEMENT_SIZE$1 = 8;
const MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE$1 = 10;

const tempMat4a$d = new Float32Array(16);
const tempUint8Array4$1 = new Uint8Array(4);
const tempFloat32Array3$1 = new Float32Array(3);

let numLayers$1 = 0;

const DEFAULT_MATRIX$2 = math.identityMat4();

/**
 * @private
 */
class DTXLinesLayer {

    constructor(model, cfg) {

      //  console.info("Creating DTXLinesLayer");

        dataTextureRamStats$1.numberOfLayers++;

        this._layerNumber = numLayers$1++;
        this.sortId = `TriDTX-${this._layerNumber}`; // State sorting key.
        this.layerIndex = cfg.layerIndex; // Index of this DTXLinesLayer in {@link SceneModel#_layerList}.
        this._renderers = getRenderers$1(model.scene);
        this.model = model;
        this._buffer = new DTXLinesBuffer();
        this._dataTextureState = new DTXLinesState();
        this._dataTextureGenerator = new DTXLinesTextureFactory();

        this._state = new RenderState({
            origin: math.vec3(cfg.origin),
            textureState: this._dataTextureState,
            numIndices8Bits: 0,
            numIndices16Bits: 0,
            numIndices32Bits: 0,
            numVertices: 0,
        });

        this._numPortions = 0;        // These counts are used to avoid unnecessary render passes
        this._numVisibleLayerPortions = 0;
        this._numTransparentLayerPortions = 0;
        this._numXRayedLayerPortions = 0;
        this._numSelectedLayerPortions = 0;
        this._numHighlightedLayerPortions = 0;
        this._numClippableLayerPortions = 0;
        this._numPickableLayerPortions = 0;
        this._numCulledLayerPortions = 0;

        this._subPortions = [];
        this._portionToSubPortionsMap = [];
        this._bucketGeometries = {};
        this._meshes = [];
        this._aabb = math.collapseAABB3();
        this.aabbDirty = true;
        this._numUpdatesInFrame = 0;

        /**
         * The type of primitives in this layer.
         */
        this.primitive = cfg.primitive;

        this._finalized = false;
    }

    get aabb() {
        if (this.aabbDirty) {
            math.collapseAABB3(this._aabb);
            for (let i = 0, len = this._meshes.length; i < len; i++) {
                math.expandAABB3(this._aabb, this._meshes[i].aabb);
            }
            this.aabbDirty = false;
        }
        return this._aabb;
    }

    canCreatePortion(portionCfg) {
        if (this._finalized) {
            throw "Already finalized";
        }
        const numNewPortions = portionCfg.buckets.length;
        if ((this._numPortions + numNewPortions) > MAX_NUMBER_OF_OBJECTS_IN_LAYER$1) {
            dataTextureRamStats$1.cannotCreatePortion.because10BitsObjectId++;
        }
        let retVal = (this._numPortions + numNewPortions) <= MAX_NUMBER_OF_OBJECTS_IN_LAYER$1;
        const bucketIndex = 0; // TODO: Is this a bug?
        const bucketGeometryId = portionCfg.geometryId !== undefined && portionCfg.geometryId !== null
            ? `${portionCfg.geometryId}#${bucketIndex}`
            : `${portionCfg.id}#${bucketIndex}`;
        const alreadyHasPortionGeometry = this._bucketGeometries[bucketGeometryId];
        if (!alreadyHasPortionGeometry) {
            const maxIndicesOfAnyBits = Math.max(this._state.numIndices8Bits, this._state.numIndices16Bits, this._state.numIndices32Bits,);
            let numVertices = 0;
            let numIndices = 0;
            portionCfg.buckets.forEach(bucket => {
                numVertices += bucket.positionsCompressed.length / 3;
                numIndices += bucket.indices.length / 2;
            });
            if ((this._state.numVertices + numVertices) > MAX_DATA_TEXTURE_HEIGHT$1 * 4096 ||
                (maxIndicesOfAnyBits + numIndices) > MAX_DATA_TEXTURE_HEIGHT$1 * 4096) {
                dataTextureRamStats$1.cannotCreatePortion.becauseTextureSize++;
            }
            retVal &&=
                (this._state.numVertices + numVertices) <= MAX_DATA_TEXTURE_HEIGHT$1 * 4096 &&
                (maxIndicesOfAnyBits + numIndices) <= MAX_DATA_TEXTURE_HEIGHT$1 * 4096;
        }
        return retVal;
    }

    createPortion(mesh, portionCfg) {
        if (this._finalized) {
            throw "Already finalized";
        }
        const subPortionIds = [];
        //   const portionAABB = portionCfg.worldAABB;
        portionCfg.buckets.forEach((bucket, bucketIndex) => {
            const bucketGeometryId = portionCfg.geometryId !== undefined && portionCfg.geometryId !== null
                ? `${portionCfg.geometryId}#${bucketIndex}`
                : `${portionCfg.id}#${bucketIndex}`;
            let bucketGeometry = this._bucketGeometries[bucketGeometryId];
            if (!bucketGeometry) {
                bucketGeometry = this._createBucketGeometry(portionCfg, bucket);
                this._bucketGeometries[bucketGeometryId] = bucketGeometry;
            }
            //  const subPortionAABB = math.collapseAABB3(tempAABB3b);
            const subPortionId = this._createSubPortion(portionCfg, bucketGeometry, bucket);
            //math.expandAABB3(portionAABB, subPortionAABB);
            subPortionIds.push(subPortionId);
        });
        const portionId = this._portionToSubPortionsMap.length;
        this._portionToSubPortionsMap.push(subPortionIds);
        this.model.numPortions++;
        this._meshes.push(mesh);
        return portionId;
    }

    _createBucketGeometry(portionCfg, bucket) {
        if (bucket.indices) {
            const alignedIndicesLen = Math.ceil((bucket.indices.length / 2) / INDICES_EDGE_INDICES_ALIGNEMENT_SIZE$1) * INDICES_EDGE_INDICES_ALIGNEMENT_SIZE$1 * 2;
            dataTextureRamStats$1.overheadSizeAlignementIndices += 2 * (alignedIndicesLen - bucket.indices.length);
            const alignedIndices = new Uint32Array(alignedIndicesLen);
            alignedIndices.fill(0);
            alignedIndices.set(bucket.indices);
            bucket.indices = alignedIndices;
        }
        const positionsCompressed = bucket.positionsCompressed;
        const indices = bucket.indices;
        const buffer = this._buffer;
        buffer.positionsCompressed.push(positionsCompressed);
        const vertexBase = buffer.lenPositionsCompressed / 3;
        const numVertices = positionsCompressed.length / 3;
        buffer.lenPositionsCompressed += positionsCompressed.length;
        let indicesBase;
        let numLines = 0;
        if (indices) {
            numLines = indices.length / 2;
            let indicesBuffer;
            if (numVertices <= (1 << 8)) {
                indicesBuffer = buffer.indices8Bits;
                indicesBase = buffer.lenIndices8Bits / 2;
                buffer.lenIndices8Bits += indices.length;
            } else if (numVertices <= (1 << 16)) {
                indicesBuffer = buffer.indices16Bits;
                indicesBase = buffer.lenIndices16Bits / 2;
                buffer.lenIndices16Bits += indices.length;
            } else {
                indicesBuffer = buffer.indices32Bits;
                indicesBase = buffer.lenIndices32Bits / 2;
                buffer.lenIndices32Bits += indices.length;
            }
            indicesBuffer.push(indices);
        }
        this._state.numVertices += numVertices;
        dataTextureRamStats$1.numberOfGeometries++;
        const bucketGeometry = {
            vertexBase,
            numVertices,
            numLines,
            indicesBase
        };
        return bucketGeometry;
    }

    _createSubPortion(portionCfg, bucketGeometry) {
        const color = portionCfg.color;
        const colors = portionCfg.colors;
        const opacity = portionCfg.opacity;
        const meshMatrix = portionCfg.meshMatrix;
        const pickColor = portionCfg.pickColor;
        const buffer = this._buffer;
        const state = this._state;
        buffer.perObjectPositionsDecodeMatrices.push(portionCfg.positionsDecodeMatrix);
        buffer.perObjectInstancePositioningMatrices.push(meshMatrix || DEFAULT_MATRIX$2);
        buffer.perObjectSolid.push(!!portionCfg.solid);
        if (colors) {
            buffer.perObjectColors.push([colors[0] * 255, colors[1] * 255, colors[2] * 255, 255]);
        } else if (color) { // Color is pre-quantized by SceneModel
            buffer.perObjectColors.push([color[0], color[1], color[2], opacity]);
        }
        buffer.perObjectPickColors.push(pickColor);
        buffer.perObjectVertexBases.push(bucketGeometry.vertexBase);
        {
            let currentNumIndices;
            if (bucketGeometry.numVertices <= (1 << 8)) {
                currentNumIndices = state.numIndices8Bits;
            } else if (bucketGeometry.numVertices <= (1 << 16)) {
                currentNumIndices = state.numIndices16Bits;
            } else {
                currentNumIndices = state.numIndices32Bits;
            }
            buffer.perObjectIndexBaseOffsets.push(currentNumIndices / 2 - bucketGeometry.indicesBase);
        }
        const subPortionId = this._subPortions.length;
        if (bucketGeometry.numLines > 0) {
            let numIndices = bucketGeometry.numLines * 2;
            let indicesPortionIdBuffer;
            if (bucketGeometry.numVertices <= (1 << 8)) {
                indicesPortionIdBuffer = buffer.perLineNumberPortionId8Bits;
                state.numIndices8Bits += numIndices;
                dataTextureRamStats$1.totalLines8Bits += bucketGeometry.numLines;
            } else if (bucketGeometry.numVertices <= (1 << 16)) {
                indicesPortionIdBuffer = buffer.perLineNumberPortionId16Bits;
                state.numIndices16Bits += numIndices;
                dataTextureRamStats$1.totalLines16Bits += bucketGeometry.numLines;
            } else {
                indicesPortionIdBuffer = buffer.perLineNumberPortionId32Bits;
                state.numIndices32Bits += numIndices;
                dataTextureRamStats$1.totalLines32Bits += bucketGeometry.numLines;
            }
            dataTextureRamStats$1.totalLines += bucketGeometry.numLines;
            for (let i = 0; i < bucketGeometry.numLines; i += INDICES_EDGE_INDICES_ALIGNEMENT_SIZE$1) {
                indicesPortionIdBuffer.push(subPortionId);
            }
        }
        this._subPortions.push({
            // vertsBase: vertsIndex,
            numVertices: bucketGeometry.numLines
        });
        this._numPortions++;
        dataTextureRamStats$1.numberOfPortions++;
        return subPortionId;
    }

    /**
     * Builds data textures from the appended geometries and loads them into the GPU.
     *
     * No more portions can then be created.
     */
    finalize() {
        if (this._finalized) {
            return;
        }
        const state = this._state;
        const textureState = this._dataTextureState;
        const gl = this.model.scene.canvas.gl;
        const buffer = this._buffer;
        state.gl = gl;
        textureState.texturePerObjectColorsAndFlags = this._dataTextureGenerator.generateTextureForColorsAndFlags(
            gl,
            buffer.perObjectColors,
            buffer.perObjectPickColors,
            buffer.perObjectVertexBases,
            buffer.perObjectIndexBaseOffsets,
            buffer.perObjectSolid);
        textureState.texturePerObjectInstanceMatrices = this._dataTextureGenerator.generateTextureForInstancingMatrices(gl, buffer.perObjectInstancePositioningMatrices);
        textureState.texturePerObjectPositionsDecodeMatrix = this._dataTextureGenerator.generateTextureForPositionsDecodeMatrices(gl, buffer.perObjectPositionsDecodeMatrices);
        textureState.texturePerVertexIdCoordinates = this._dataTextureGenerator.generateTextureForPositions(gl, buffer.positionsCompressed, buffer.lenPositionsCompressed);
        textureState.texturePerLineIdPortionIds8Bits = this._dataTextureGenerator.generateTextureForPackedPortionIds(gl, buffer.perLineNumberPortionId8Bits);
        textureState.texturePerLineIdPortionIds16Bits = this._dataTextureGenerator.generateTextureForPackedPortionIds(gl, buffer.perLineNumberPortionId16Bits);
        textureState.texturePerLineIdPortionIds32Bits = this._dataTextureGenerator.generateTextureForPackedPortionIds(gl, buffer.perLineNumberPortionId32Bits);
        if (buffer.lenIndices8Bits > 0) {
            textureState.texturePerLineIdIndices8Bits = this._dataTextureGenerator.generateTextureFor8BitIndices(gl, buffer.indices8Bits, buffer.lenIndices8Bits);
        }
        if (buffer.lenIndices16Bits > 0) {
            textureState.texturePerLineIdIndices16Bits = this._dataTextureGenerator.generateTextureFor16BitIndices(gl, buffer.indices16Bits, buffer.lenIndices16Bits);
        }
        if (buffer.lenIndices32Bits > 0) {
            textureState.texturePerLineIdIndices32Bits = this._dataTextureGenerator.generateTextureFor32BitIndices(gl, buffer.indices32Bits, buffer.lenIndices32Bits);
        }
        textureState.finalize();
        // Free up memory
        this._buffer = null;
        this._bucketGeometries = {};
        this._finalized = true;
        this._deferredSetFlagsDirty = false; //
        this._onSceneRendering = this.model.scene.on("rendering", () => {
            if (this._deferredSetFlagsDirty) {
                this._uploadDeferredFlags();
            }
            this._numUpdatesInFrame = 0;
        });
    }

    initFlags(portionId, flags, meshTransparent) {
        if (flags & ENTITY_FLAGS.VISIBLE) {
            this._numVisibleLayerPortions++;
            this.model.numVisibleLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.HIGHLIGHTED) {
            this._numHighlightedLayerPortions++;
            this.model.numHighlightedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.XRAYED) {
            this._numXRayedLayerPortions++;
            this.model.numXRayedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.SELECTED) {
            this._numSelectedLayerPortions++;
            this.model.numSelectedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.CLIPPABLE) {
            this._numClippableLayerPortions++;
            this.model.numClippableLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.PICKABLE) {
            this._numPickableLayerPortions++;
            this.model.numPickableLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.CULLED) {
            this._numCulledLayerPortions++;
            this.model.numCulledLayerPortions++;
        }
        if (meshTransparent) {
            this._numTransparentLayerPortions++;
            this.model.numTransparentLayerPortions++;
        }
        const deferred = true;
        this._setFlags(portionId, flags, meshTransparent, deferred);
        this._setFlags2(portionId, flags, deferred);
    }

    flushInitFlags() {
        this._setDeferredFlags();
        this._setDeferredFlags2();
    }

    setVisible(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.VISIBLE) {
            this._numVisibleLayerPortions++;
            this.model.numVisibleLayerPortions++;
        } else {
            this._numVisibleLayerPortions--;
            this.model.numVisibleLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setHighlighted(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.HIGHLIGHTED) {
            this._numHighlightedLayerPortions++;
            this.model.numHighlightedLayerPortions++;
        } else {
            this._numHighlightedLayerPortions--;
            this.model.numHighlightedLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setXRayed(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.XRAYED) {
            this._numXRayedLayerPortions++;
            this.model.numXRayedLayerPortions++;
        } else {
            this._numXRayedLayerPortions--;
            this.model.numXRayedLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setSelected(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.SELECTED) {
            this._numSelectedLayerPortions++;
            this.model.numSelectedLayerPortions++;
        } else {
            this._numSelectedLayerPortions--;
            this.model.numSelectedLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setEdges(portionId, flags, transparent) {
    }

    setClippable(portionId, flags) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.CLIPPABLE) {
            this._numClippableLayerPortions++;
            this.model.numClippableLayerPortions++;
        } else {
            this._numClippableLayerPortions--;
            this.model.numClippableLayerPortions--;
        }
        this._setFlags2(portionId, flags);
    }

    /**
     * This will _start_ a "set-flags transaction".
     *
     * After invoking this method, calling setFlags/setFlags2 will not update
     * the colors+flags texture but only store the new flags/flag2 in the
     * colors+flags texture data array.
     *
     * After invoking this method, and when all desired setFlags/setFlags2 have
     * been called on needed portions of the layer, invoke `_uploadDeferredFlags`
     * to actually upload the data array into the texture.
     *
     * In massive "set-flags" scenarios like VFC or LOD mechanisms, the combination of
     * `_beginDeferredFlags` + `_uploadDeferredFlags`brings a speed-up of
     * up to 80x when e.g. objects are massively (un)culled 🚀.
     */
    _beginDeferredFlags() {
        this._deferredSetFlagsActive = true;
    }

    /**
     * This will _commit_ a "set-flags transaction".
     *
     * Invoking this method will update the colors+flags texture data with new
     * flags/flags2 set since the previous invocation of `_beginDeferredFlags`.
     */
    _uploadDeferredFlags() {
        this._deferredSetFlagsActive = false;
        if (!this._deferredSetFlagsDirty) {
            return;
        }
        this._deferredSetFlagsDirty = false;
        const gl = this.model.scene.canvas.gl;
        const textureState = this._dataTextureState;
        gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectColorsAndFlags._texture);
        gl.texSubImage2D(
            gl.TEXTURE_2D,
            0, // level
            0, // xoffset
            0, // yoffset
            textureState.texturePerObjectColorsAndFlags._textureWidth, // width
            textureState.texturePerObjectColorsAndFlags._textureHeight, // width
            gl.RGBA_INTEGER,
            gl.UNSIGNED_BYTE,
            textureState.texturePerObjectColorsAndFlags._textureData
        );
    }

    setCulled(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.CULLED) {
            this._numCulledLayerPortions += this._portionToSubPortionsMap[portionId].length;
            this.model.numCulledLayerPortions++;
        } else {
            this._numCulledLayerPortions -= this._portionToSubPortionsMap[portionId].length;
            this.model.numCulledLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setCollidable(portionId, flags) {
        if (!this._finalized) {
            throw "Not finalized";
        }
    }

    setPickable(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.PICKABLE) {
            this._numPickableLayerPortions++;
            this.model.numPickableLayerPortions++;
        } else {
            this._numPickableLayerPortions--;
            this.model.numPickableLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setColor(portionId, color) {
        const subPortionIds = this._portionToSubPortionsMap[portionId];
        for (let i = 0, len = subPortionIds.length; i < len; i++) {
            this._subPortionSetColor(subPortionIds[i], color);
        }
    }

    _subPortionSetColor(subPortionId, color) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        // Color
        const textureState = this._dataTextureState;
        const gl = this.model.scene.canvas.gl;
        tempUint8Array4$1 [0] = color[0];
        tempUint8Array4$1 [1] = color[1];
        tempUint8Array4$1 [2] = color[2];
        tempUint8Array4$1 [3] = color[3];
        // object colors
        textureState.texturePerObjectColorsAndFlags._textureData.set(tempUint8Array4$1, subPortionId * 32);
        if (this._deferredSetFlagsActive) {
            this._deferredSetFlagsDirty = true;
            return;
        }
        if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE$1) {
            this._beginDeferredFlags(); // Subsequent flags updates now deferred
        }
        gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectColorsAndFlags._texture);
        gl.texSubImage2D(
            gl.TEXTURE_2D,
            0, // level
            (subPortionId % 512) * 8, // xoffset
            Math.floor(subPortionId / 512), // yoffset
            1, // width
            1, //height
            gl.RGBA_INTEGER,
            gl.UNSIGNED_BYTE,
            tempUint8Array4$1
        );
        // gl.bindTexture (gl.TEXTURE_2D, null);
    }

    setTransparent(portionId, flags, transparent) {
        if (transparent) {
            this._numTransparentLayerPortions++;
            this.model.numTransparentLayerPortions++;
        } else {
            this._numTransparentLayerPortions--;
            this.model.numTransparentLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    _setFlags(portionId, flags, transparent, deferred = false) {
        const subPortionIds = this._portionToSubPortionsMap[portionId];
        for (let i = 0, len = subPortionIds.length; i < len; i++) {
            this._subPortionSetFlags(subPortionIds[i], flags, transparent, deferred);
        }
    }

    _subPortionSetFlags(subPortionId, flags, transparent, deferred = false) {
        if (!this._finalized) {
            throw "Not finalized";
        }

        const visible = !!(flags & ENTITY_FLAGS.VISIBLE);
        const xrayed = !!(flags & ENTITY_FLAGS.XRAYED);
        const highlighted = !!(flags & ENTITY_FLAGS.HIGHLIGHTED);
        const selected = !!(flags & ENTITY_FLAGS.SELECTED);
        const pickable = !!(flags & ENTITY_FLAGS.PICKABLE);
        const culled = !!(flags & ENTITY_FLAGS.CULLED);

        // Color

        let f0;
        if (!visible || culled || xrayed) { // Highlight & select are layered on top of color - not mutually exclusive
            f0 = RENDER_PASSES.NOT_RENDERED;
        } else {
            if (transparent) {
                f0 = RENDER_PASSES.COLOR_TRANSPARENT;
            } else {
                f0 = RENDER_PASSES.COLOR_OPAQUE;
            }
        }

        // Silhouette

        let f1;
        if (!visible || culled) {
            f1 = RENDER_PASSES.NOT_RENDERED;
        } else if (selected) {
            f1 = RENDER_PASSES.SILHOUETTE_SELECTED;
        } else if (highlighted) {
            f1 = RENDER_PASSES.SILHOUETTE_HIGHLIGHTED;
        } else if (xrayed) {
            f1 = RENDER_PASSES.SILHOUETTE_XRAYED;
        } else {
            f1 = RENDER_PASSES.NOT_RENDERED;
        }

        // // Edges
        //
        // let f2 = 0;
        // if (!visible || culled) {
        //     f2 = RENDER_PASSES.NOT_RENDERED;
        // } else if (selected) {
        //     f2 = RENDER_PASSES.EDGES_SELECTED;
        // } else if (highlighted) {
        //     f2 = RENDER_PASSES.EDGES_HIGHLIGHTED;
        // } else if (xrayed) {
        //     f2 = RENDER_PASSES.EDGES_XRAYED;
        // } else if (edges) {
        //     if (transparent) {
        //         f2 = RENDER_PASSES.EDGES_COLOR_TRANSPARENT;
        //     } else {
        //         f2 = RENDER_PASSES.EDGES_COLOR_OPAQUE;
        //     }
        // } else {
        //     f2 = RENDER_PASSES.NOT_RENDERED;
        // }

        // Pick

        let f3 = (visible && (!culled) && pickable) ? RENDER_PASSES.PICK : RENDER_PASSES.NOT_RENDERED;
        const textureState = this._dataTextureState;
        const gl = this.model.scene.canvas.gl;
        tempUint8Array4$1 [0] = f0;
        tempUint8Array4$1 [1] = f1;
       // tempUint8Array4 [2] = f2;
        tempUint8Array4$1 [3] = f3;
        // object flags
        textureState.texturePerObjectColorsAndFlags._textureData.set(tempUint8Array4$1, subPortionId * 32 + 8);
        if (this._deferredSetFlagsActive || deferred) {
            this._deferredSetFlagsDirty = true;
            return;
        }
        if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE$1) {
            this._beginDeferredFlags(); // Subsequent flags updates now deferred
        }
        gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectColorsAndFlags._texture);
        gl.texSubImage2D(
            gl.TEXTURE_2D,
            0, // level
            (subPortionId % 512) * 8 + 2, // xoffset
            Math.floor(subPortionId / 512), // yoffset
            1, // width
            1, //height
            gl.RGBA_INTEGER,
            gl.UNSIGNED_BYTE,
            tempUint8Array4$1
        );
        // gl.bindTexture (gl.TEXTURE_2D, null);
    }

    _setDeferredFlags() {
    }

    _setFlags2(portionId, flags, deferred = false) {
        const subPortionIds = this._portionToSubPortionsMap[portionId];
        for (let i = 0, len = subPortionIds.length; i < len; i++) {
            this._subPortionSetFlags2(subPortionIds[i], flags, deferred);
        }
    }

    _subPortionSetFlags2(subPortionId, flags, deferred = false) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        const clippable = !!(flags & ENTITY_FLAGS.CLIPPABLE) ? 255 : 0;
        const textureState = this._dataTextureState;
        const gl = this.model.scene.canvas.gl;
        tempUint8Array4$1 [0] = clippable;
        tempUint8Array4$1 [1] = 0;
        tempUint8Array4$1 [2] = 1;
        tempUint8Array4$1 [3] = 2;
        // object flags2
        textureState.texturePerObjectColorsAndFlags._textureData.set(tempUint8Array4$1, subPortionId * 32 + 12);
        if (this._deferredSetFlagsActive || deferred) {
            // console.log("_subPortionSetFlags2 set flags defer");
            this._deferredSetFlagsDirty = true;
            return;
        }
        if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE$1) {
            this._beginDeferredFlags(); // Subsequent flags updates now deferred
        }
        gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectColorsAndFlags._texture);
        gl.texSubImage2D(
            gl.TEXTURE_2D,
            0, // level
            (subPortionId % 512) * 8 + 3, // xoffset
            Math.floor(subPortionId / 512), // yoffset
            1, // width
            1, //height
            gl.RGBA_INTEGER,
            gl.UNSIGNED_BYTE,
            tempUint8Array4$1
        );
        // gl.bindTexture (gl.TEXTURE_2D, null);
    }

    _setDeferredFlags2() {
    }

    setOffset(portionId, offset) {
        const subPortionIds = this._portionToSubPortionsMap[portionId];
        for (let i = 0, len = subPortionIds.length; i < len; i++) {
            this._subPortionSetOffset(subPortionIds[i], offset);
        }
    }

    _subPortionSetOffset(subPortionId, offset) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        // if (!this.model.scene.entityOffsetsEnabled) {
        //     this.model.error("Entity#offset not enabled for this Viewer"); // See Viewer entityOffsetsEnabled
        //     return;
        // }
        const textureState = this._dataTextureState;
        const gl = this.model.scene.canvas.gl;
        tempFloat32Array3$1 [0] = offset[0];
        tempFloat32Array3$1 [1] = offset[1];
        tempFloat32Array3$1 [2] = offset[2];
        // object offset
        textureState.texturePerObjectOffsets._textureData.set(tempFloat32Array3$1, subPortionId * 3);
        if (this._deferredSetFlagsActive) {
            this._deferredSetFlagsDirty = true;
            return;
        }
        if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE$1) {
            this._beginDeferredFlags(); // Subsequent flags updates now deferred
        }
        gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectOffsets._texture);
        gl.texSubImage2D(
            gl.TEXTURE_2D,
            0, // level
            0, // x offset
            subPortionId, // yoffset
            1, // width
            1, // height
            gl.RGB,
            gl.FLOAT,
            tempFloat32Array3$1
        );
        // gl.bindTexture (gl.TEXTURE_2D, null);
    }

    setMatrix(portionId, matrix) {
        const subPortionIds = this._portionToSubPortionsMap[portionId];
        for (let i = 0, len = subPortionIds.length; i < len; i++) {
            this._subPortionSetMatrix(subPortionIds[i], matrix);
        }
    }

    _subPortionSetMatrix(subPortionId, matrix) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        // if (!this.model.scene.entityMatrixsEnabled) {
        //     this.model.error("Entity#matrix not enabled for this Viewer"); // See Viewer entityMatrixsEnabled
        //     return;
        // }
        const textureState = this._dataTextureState;
        const gl = this.model.scene.canvas.gl;
        tempMat4a$d.set(matrix);
        textureState.texturePerObjectInstanceMatrices._textureData.set(tempMat4a$d, subPortionId * 16);
        if (this._deferredSetFlagsActive) {
            this._deferredSetFlagsDirty = true;
            return;
        }
        if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE$1) {
            this._beginDeferredFlags(); // Subsequent flags updates now deferred
        }
        gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectInstanceMatrices._texture);
        gl.texSubImage2D(
            gl.TEXTURE_2D,
            0, // level
            (subPortionId % 512) * 4, // xoffset
            Math.floor(subPortionId / 512), // yoffset
            // 1,
            4, // width
            1, // height
            gl.RGBA,
            gl.FLOAT,
            tempMat4a$d
        );
        // gl.bindTexture (gl.TEXTURE_2D, null);
    }

    // ---------------------- COLOR RENDERING -----------------------------------

    drawColorOpaque(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        if (this._renderers.colorRenderer) {
            this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
        }
    }

    drawColorTransparent(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === 0 || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        if (this._renderers.colorRenderer) {
            this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT);
        }
    }

    drawDepth(renderFlags, frameCtx) {
    }

    drawNormals(renderFlags, frameCtx) {
    }

    drawSilhouetteXRayed(renderFlags, frameCtx) {
        // if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) {
        //     return;
        // }
        // if (this._renderers.silhouetteRenderer) {
        //     this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_XRAYED);
        // }
    }

    drawSilhouetteHighlighted(renderFlags, frameCtx) {
        // if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) {
        //     return;
        // }
        // if (this._renderers.silhouetteRenderer) {
        //     this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_HIGHLIGHTED);
        // }
    }

    drawSilhouetteSelected(renderFlags, frameCtx) {
        // if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) {
        //     return;
        // }
        // if (this._renderers.silhouetteRenderer) {
        //     this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_SELECTED);
        // }
    }

    drawEdgesColorOpaque(renderFlags, frameCtx) {
    }

    drawEdgesColorTransparent(renderFlags, frameCtx) {
    }

    drawEdgesHighlighted(renderFlags, frameCtx) {
    }

    drawEdgesSelected(renderFlags, frameCtx) {
    }

    drawEdgesXRayed(renderFlags, frameCtx) {
    }

    drawOcclusion(renderFlags, frameCtx) {
    }

    drawShadow(renderFlags, frameCtx) {
    }

    setPickMatrices(pickViewMatrix, pickProjMatrix) {
    }

    drawPickMesh(renderFlags, frameCtx) {
    }

    drawPickDepths(renderFlags, frameCtx) {
    }

    drawSnapInit(renderFlags, frameCtx) {
    }

    drawSnap(renderFlags, frameCtx) {
    }

    drawPickNormals(renderFlags, frameCtx) {
    }

    destroy() {
        if (this._destroyed) {
            return;
        }
        const state = this._state;
        this.model.scene.off(this._onSceneRendering);
        state.destroy();
        this._destroyed = true;
    }
}

const tempVec3a$p = math.vec3();
const tempVec3b$l = math.vec3();
const tempVec3c$h = math.vec3();
math.vec3();

const tempVec4a$b = math.vec4();

const tempMat4a$c = math.mat4();

/**
 * @private
 */
class DTXTrianglesColorRenderer {

    constructor(scene, withSAO) {
        this._scene = scene;
        this._withSAO = withSAO;
        this._hash = this._getHash();
        this._allocate();
    }

    getValid() {
        return this._hash === this._getHash();
    };

    _getHash() {
        const scene = this._scene;
        return [scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";");
    }

    drawLayer(frameCtx, dataTextureLayer, renderPass) {

        const scene = this._scene;
        const camera = scene.camera;
        const model = dataTextureLayer.model;
        const gl = scene.canvas.gl;
        const state = dataTextureLayer._state;
        const textureState = state.textureState;
        const origin = dataTextureLayer._state.origin;
        const {position, rotationMatrix} = model;

        if (!this._program) {
            this._allocate();
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram(frameCtx, state);
        }

        textureState.bindCommonTextures(
            this._program,
            this.uTexturePerObjectPositionsDecodeMatrix,
            this._uTexturePerVertexIdCoordinates,
            this.uTexturePerObjectColorsAndFlags,
            this._uTexturePerObjectMatrix
        );

        let rtcViewMatrix;
        let rtcCameraEye;

        const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0);
        const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0);
        if (gotOrigin || gotPosition) {
            const rtcOrigin = tempVec3a$p;
            if (gotOrigin) {
                const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3b$l);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(camera.viewMatrix, rtcOrigin, tempMat4a$c);
            rtcCameraEye = tempVec3c$h;
            rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0];
            rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1];
            rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2];
        } else {
            rtcViewMatrix = camera.viewMatrix;
            rtcCameraEye = camera.eye;
        }

        gl.uniformMatrix4fv(this._uSceneModelMatrix, false, rotationMatrix);
        gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix);
        gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix);
        gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye);
        gl.uniform1i(this._uRenderPass, renderPass);

        if (scene.logarithmicDepthBufferEnabled) {
            const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }

        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes;
            const renderFlags = model.renderFlags;
            if (scene.crossSections) {
                gl.uniform4fv(this._uSliceColor, scene.crossSections.sliceColor);
                gl.uniform1f(this._uSliceThickness, scene.crossSections.sliceThickness);
            }
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$p, model.matrix);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }

        if (state.numIndices8Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                8 // 8 bits indices
            );
            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices8Bits);
        }

        if (state.numIndices16Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                16 // 16 bits indices
            );
            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices16Bits);
        }

        if (state.numIndices32Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                32 // 32 bits indices
            );
            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices32Bits);
        }

        frameCtx.drawElements++;
    }

    _allocate() {

        const scene = this._scene;
        const gl = scene.canvas.gl;
        const lightsState = scene._lightsState;

        this._program = new Program(gl, this._buildShader());

        if (this._program.errors) {
            this.errors = this._program.errors;
            console.error(this.errors);
            return;
        }

        const program = this._program;

        this._uRenderPass = program.getLocation("renderPass");

        this._uLightAmbient = program.getLocation("lightAmbient");
        this._uLightColor = [];
        this._uLightDir = [];
        this._uLightPos = [];
        this._uLightAttenuation = [];

        const lights = lightsState.lights;
        let light;

        for (let i = 0, len = lights.length; i < len; i++) {
            light = lights[i];
            switch (light.type) {
                case "dir":
                    this._uLightColor[i] = program.getLocation("lightColor" + i);
                    this._uLightPos[i] = null;
                    this._uLightDir[i] = program.getLocation("lightDir" + i);
                    break;
                case "point":
                    this._uLightColor[i] = program.getLocation("lightColor" + i);
                    this._uLightPos[i] = program.getLocation("lightPos" + i);
                    this._uLightDir[i] = null;
                    this._uLightAttenuation[i] = program.getLocation("lightAttenuation" + i);
                    break;
                case "spot":
                    this._uLightColor[i] = program.getLocation("lightColor" + i);
                    this._uLightPos[i] = program.getLocation("lightPos" + i);
                    this._uLightDir[i] = program.getLocation("lightDir" + i);
                    this._uLightAttenuation[i] = program.getLocation("lightAttenuation" + i);
                    break;
            }
        }

        this._uSceneModelMatrix = program.getLocation("sceneModelMatrix");
        this._uViewMatrix = program.getLocation("viewMatrix");
        this._uProjMatrix = program.getLocation("projMatrix");

        this._uSectionPlanes = [];

        for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            this._uSectionPlanes.push({
                active: program.getLocation("sectionPlaneActive" + i),
                pos: program.getLocation("sectionPlanePos" + i),
                dir: program.getLocation("sectionPlaneDir" + i)
            });
        }

        if (this._withSAO) {
            this._uOcclusionTexture = "uOcclusionTexture";
            this._uSAOParams = program.getLocation("uSAOParams");
        }

        if (scene.logarithmicDepthBufferEnabled) {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }

        this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix";
        this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags";
        this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates";
        this._uTexturePerPolygonIdNormals = "uTexturePerPolygonIdNormals";
        this._uTexturePerPolygonIdIndices = "uTexturePerPolygonIdIndices";
        this._uTexturePerPolygonIdPortionIds = "uTexturePerPolygonIdPortionIds";
        this._uTexturePerObjectMatrix= "uTexturePerObjectMatrix";
        this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc");

        if (scene.crossSections) {
            this._uSliceColor = program.getLocation("sliceColor");
            this._uSliceThickness = program.getLocation("sliceThickness");
        }
    }

    _bindProgram(frameCtx) {

        const scene = this._scene;
        const gl = scene.canvas.gl;
        const program = this._program;
        const lights = scene._lightsState.lights;
        const project = scene.camera.project;

        program.bind();

        if (this._uLightAmbient) {
            gl.uniform4fv(this._uLightAmbient, scene._lightsState.getAmbientColorAndIntensity());
        }

        for (let i = 0, len = lights.length; i < len; i++) {
            const light = lights[i];

            if (this._uLightColor[i]) {
                gl.uniform4f(this._uLightColor[i], light.color[0], light.color[1], light.color[2], light.intensity);
            }
            if (this._uLightPos[i]) {
                gl.uniform3fv(this._uLightPos[i], light.pos);
                if (this._uLightAttenuation[i]) {
                    gl.uniform1f(this._uLightAttenuation[i], light.attenuation);
                }
            }
            if (this._uLightDir[i]) {
                gl.uniform3fv(this._uLightDir[i], light.dir);
            }
        }

        if (this._withSAO) {
            const sao = scene.sao;
            const saoEnabled = sao.possible;
            if (saoEnabled) {
                const viewportWidth = gl.drawingBufferWidth;
                const viewportHeight = gl.drawingBufferHeight;
                tempVec4a$b[0] = viewportWidth;
                tempVec4a$b[1] = viewportHeight;
                tempVec4a$b[2] = sao.blendCutoff;
                tempVec4a$b[3] = sao.blendFactor;
                gl.uniform4fv(this._uSAOParams, tempVec4a$b);
                this._program.bindTexture(this._uOcclusionTexture, frameCtx.occlusionTexture, 10);
            }
        }

        if (scene.logarithmicDepthBufferEnabled) {
            const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }
    }

    _buildShader() {
        return {
            vertex: this._buildVertexShader(),
            fragment: this._buildFragmentShader()
        };
    }

    _buildVertexShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const lightsState = scene._lightsState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        let light;
        const src = [];
        src.push("#version 300 es");
        src.push("// TrianglesDataTextureColorRenderer vertex shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");

        src.push("uniform int renderPass;");

        src.push("uniform mat4 sceneModelMatrix;");
        src.push("uniform mat4 viewMatrix;");
        src.push("uniform mat4 projMatrix;");

        src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;");
        src.push("uniform highp sampler2D uTexturePerObjectMatrix;");
        src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;");
        src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;");
        src.push("uniform highp usampler2D uTexturePerPolygonIdIndices;");
        src.push("uniform mediump usampler2D uTexturePerPolygonIdPortionIds;");

        src.push("uniform vec3 uCameraEyeRtc;");

        src.push("vec3 positions[3];");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("out float isPerspective;");
        }

        src.push("bool isPerspectiveMatrix(mat4 m) {");
        src.push("    return (m[2][3] == - 1.0);");
        src.push("}");

        src.push("uniform vec4 lightAmbient;");

        for (let i = 0, len = lightsState.lights.length; i < len; i++) {
            light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            src.push("uniform vec4 lightColor" + i + ";");
            if (light.type === "dir") {
                src.push("uniform vec3 lightDir" + i + ";");
            }
            if (light.type === "point") {
                src.push("uniform vec3 lightPos" + i + ";");
            }
            if (light.type === "spot") {
                src.push("uniform vec3 lightPos" + i + ";");
                src.push("uniform vec3 lightDir" + i + ";");
            }
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("flat out uint vFlags2;");
        }

        src.push("out vec4 vColor;");

        src.push("void main(void) {");

        // constants
        src.push("int polygonIndex = gl_VertexID / 3;");

        // get packed object-id
        src.push("int h_packed_object_id_index = (polygonIndex >> 3) & 4095;");
        src.push("int v_packed_object_id_index = (polygonIndex >> 3) >> 12;");

        src.push("int objectIndex = int(texelFetch(uTexturePerPolygonIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);");
        src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);");

        // get flags & flags2
        src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);");
        src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);");

        // flags.x = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE

        src.push(`if (int(flags.x) != renderPass) {`);
        src.push("   gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("   return;"); // Cull vertex
        src.push("} else {");

        src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));");
        src.push("ivec4 packedIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+5, objectIndexCoords.y), 0));");

        src.push("int indexBaseOffset = (packedIndexBaseOffset.r << 24) + (packedIndexBaseOffset.g << 16) + (packedIndexBaseOffset.b << 8) + packedIndexBaseOffset.a;");

        src.push("int h_index = (polygonIndex - indexBaseOffset) & 4095;");
        src.push("int v_index = (polygonIndex - indexBaseOffset) >> 12;");

        src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdIndices, ivec2(h_index, v_index), 0));");
        src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;");

        src.push("ivec3 indexPositionH = uniqueVertexIndexes & 4095;");
        src.push("ivec3 indexPositionV = uniqueVertexIndexes >> 12;");

        src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");
        src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");

        src.push("uint solid = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+7, objectIndexCoords.y), 0).r;");

        src.push("positions[0] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.r, indexPositionV.r), 0));");
        src.push("positions[1] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.g, indexPositionV.g), 0));");
        src.push("positions[2] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.b, indexPositionV.b), 0));");

        src.push("uvec4 color = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+0, objectIndexCoords.y), 0);");
        src.push(`if (color.a == 0u) {`);
        src.push("   gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("   return;");
        src.push("};");

        src.push("vec3 normal = normalize(cross(positions[2] - positions[0], positions[1] - positions[0]));");
        src.push("vec3 position;");
        src.push("position = positions[gl_VertexID % 3];");
        src.push("vec3 viewNormal = -normalize((transpose(inverse(viewMatrix*objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);");
        // when the geometry is not solid, if needed, flip the triangle winding

        src.push("if (solid != 1u) {");
        src.push("if (isPerspectiveMatrix(projMatrix)) {");
        src.push("vec3 uCameraEyeRtcInQuantizedSpace = (inverse(sceneModelMatrix * objectDecodeAndInstanceMatrix) * vec4(uCameraEyeRtc, 1)).xyz;");
        // src.push("vColor = vec4(vec3(1, -1, 0)*dot(normalize(position.xyz - uCameraEyeRtcInQuantizedSpace), normal), 1);")
        src.push("if (dot(position.xyz - uCameraEyeRtcInQuantizedSpace, normal) < 0.0) {");
        src.push("position = positions[2 - (gl_VertexID % 3)];");
        src.push("viewNormal = -viewNormal;");
        src.push("}");
        src.push("} else {");
        // src.push("vColor = vec4(vec3(1, -1, 0)*viewNormal.z, 1);")

        src.push("if (viewNormal.z < 0.0) {");
        src.push("position = positions[2 - (gl_VertexID % 3)];");
        src.push("viewNormal = -viewNormal;");
        src.push("}");
        src.push("}");
        src.push("}");

        src.push("vec4 worldPosition = sceneModelMatrix * ((objectDecodeAndInstanceMatrix * vec4(position, 1.0))); ");

        src.push("vec4 viewPosition = viewMatrix * worldPosition; ");

        src.push("vec3 reflectedColor = vec3(0.0, 0.0, 0.0);");
        src.push("vec3 viewLightDir = vec3(0.0, 0.0, -1.0);");

        src.push("float lambertian = 1.0;");
        for (let i = 0, len = lightsState.lights.length; i < len; i++) {
            light = lightsState.lights[i];
            if (light.type === "ambient") {
                continue;
            }
            if (light.type === "dir") {
                if (light.space === "view") {
                    src.push("viewLightDir = normalize(lightDir" + i + ");");
                } else {
                    src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "point") {
                if (light.space === "view") {
                    src.push("viewLightDir = -normalize(lightPos" + i + " - viewPosition.xyz);");
                } else {
                    src.push("viewLightDir = -normalize((viewMatrix * vec4(lightPos" + i + ", 0.0)).xyz);");
                }
            } else if (light.type === "spot") {
                if (light.space === "view") {
                    src.push("viewLightDir = normalize(lightDir" + i + ");");
                } else {
                    src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);");
                }
            } else {
                continue;
            }
            src.push("lambertian = max(dot(-viewNormal, viewLightDir), 0.0);");
            src.push("reflectedColor += lambertian * (lightColor" + i + ".rgb * lightColor" + i + ".a);");
        }

        src.push("vec3 rgb = vec3(color.rgb) / 255.0;");
        src.push("vColor =  vec4((lightAmbient.rgb * lightAmbient.a * rgb) + (reflectedColor * rgb), float(color.a) / 255.0);");

        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags2 = flags2.r;");
        }
        src.push("gl_Position = clipPos;");
        src.push("}");

        src.push("}");

        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// TrianglesDataTextureColorRenderer fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }

        if (this._withSAO) {
            src.push("uniform sampler2D uOcclusionTexture;");
            src.push("uniform vec4      uSAOParams;");

            src.push("const float       packUpscale = 256. / 255.;");
            src.push("const float       unpackDownScale = 255. / 256.;");
            src.push("const vec3        packFactors = vec3( 256. * 256. * 256., 256. * 256.,  256. );");
            src.push("const vec4        unPackFactors = unpackDownScale / vec4( packFactors, 1. );");

            src.push("float unpackRGBToFloat( const in vec4 v ) {");
            src.push("    return dot( v, unPackFactors );");
            src.push("}");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("flat in uint vFlags2;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
            src.push("uniform float sliceThickness;");
            src.push("uniform vec4 sliceColor;");
        }
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        src.push("  vec4 newColor;");
        src.push("  newColor = vColor;");
        if (clipping) {
            src.push("  bool clippable = vFlags2 > 0u;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > sliceThickness) { ");
            src.push("      discard;");
            src.push("  }");
            src.push("  if (dist > 0.0) { ");
            src.push("      newColor = sliceColor;");
            src.push("  }");
            src.push("}");
        }

        if (scene.logarithmicDepthBufferEnabled) {
             src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
            //src.push("    gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        } else {
            src.push("    float dx = dFdx(gl_FragCoord.z);");
            src.push("    float dy = dFdy(gl_FragCoord.z);");
            src.push("    float diff = sqrt(dx*dx+dy*dy);");
            src.push("    gl_FragDepth = gl_FragCoord.z + diff;");
        }

        if (this._withSAO) {
            // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top
            // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject
            src.push("   float viewportWidth     = uSAOParams[0];");
            src.push("   float viewportHeight    = uSAOParams[1];");
            src.push("   float blendCutoff       = uSAOParams[2];");
            src.push("   float blendFactor       = uSAOParams[3];");
            src.push("   vec2 uv                 = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);");
            src.push("   float ambient           = smoothstep(blendCutoff, 1.0, unpackRGBToFloat(texture(uOcclusionTexture, uv))) * blendFactor;");
            src.push("   outColor            = vec4(newColor.rgb * ambient, 1.0);");
        } else {
            src.push("   outColor            = newColor;");
        }

        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

const defaultColor$1 = new Float32Array([1, 1, 1]);

const tempVec3a$o = math.vec3();
const tempVec3b$k = math.vec3();
const tempVec3c$g = math.vec3();
math.vec3();
const tempMat4a$b = math.mat4();

/**
 * @private
 */
class DTXTrianglesSilhouetteRenderer {

    constructor(scene, primitiveType) {
        this._scene = scene;
        this._hash = this._getHash();
        this._allocate();
    }

    getValid() {
        return this._hash === this._getHash();
    };

    _getHash() {
        return this._scene._sectionPlanesState.getHash();
    }

    drawLayer(frameCtx, dataTextureLayer, renderPass) {

        const scene = this._scene;
        const camera = scene.camera;
        const model = dataTextureLayer.model;
        const gl = scene.canvas.gl;
        const state = dataTextureLayer._state;
        const textureState = state.textureState;
        const origin = dataTextureLayer._state.origin;
        const {position, rotationMatrix} = model;
        const viewMatrix = camera.viewMatrix;

        if (!this._program) {
            this._allocate();
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram(frameCtx, state);
        }


        textureState.bindCommonTextures(
            this._program,
            this.uTexturePerObjectPositionsDecodeMatrix,
            this._uTexturePerVertexIdCoordinates,
            this.uTexturePerObjectColorsAndFlags,
            this._uTexturePerObjectMatrix
        );

        let rtcViewMatrix;
        let rtcCameraEye;

        if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) {
            const rtcOrigin = tempVec3a$o;
            if (origin) {
                const rotatedOrigin = tempVec3b$k;
                math.transformPoint3(rotationMatrix, origin, rotatedOrigin);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$b);
            rtcCameraEye = tempVec3c$g;
            rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0];
            rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1];
            rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            rtcCameraEye = camera.eye;
        }

        gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye);
        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniformMatrix4fv(this._uWorldMatrix, false, rotationMatrix);
        gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix);
        gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix);

        if (renderPass === RENDER_PASSES.SILHOUETTE_XRAYED) {
            const material = scene.xrayMaterial._state;
            const fillColor = material.fillColor;
            const fillAlpha = material.fillAlpha;
            gl.uniform4f(this._uColor, fillColor[0], fillColor[1], fillColor[2], fillAlpha);

        } else if (renderPass === RENDER_PASSES.SILHOUETTE_HIGHLIGHTED) {
            const material = scene.highlightMaterial._state;
            const fillColor = material.fillColor;
            const fillAlpha = material.fillAlpha;
            gl.uniform4f(this._uColor, fillColor[0], fillColor[1], fillColor[2], fillAlpha);

        } else if (renderPass === RENDER_PASSES.SILHOUETTE_SELECTED) {
            const material = scene.selectedMaterial._state;
            const fillColor = material.fillColor;
            const fillAlpha = material.fillAlpha;
            gl.uniform4f(this._uColor, fillColor[0], fillColor[1], fillColor[2], fillAlpha);

        } else {
            gl.uniform4fv(this._uColor, defaultColor$1);
        }
        if (scene.logarithmicDepthBufferEnabled) {
            const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }
        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes;
            const renderFlags = model.renderFlags;
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$o, model.matrix);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }

        if (state.numIndices8Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                8 // 8 bits indices
            );

            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices8Bits);
        }

        if (state.numIndices16Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                16 // 16 bits indices
            );

            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices16Bits);
        }

        if (state.numIndices32Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                32 // 32 bits indices
            );

            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices32Bits);
        }

        frameCtx.drawElements++;
    }

    _allocate() {
        const scene = this._scene;
        const gl = scene.canvas.gl;
        this._program = new Program(gl, this._buildShader());
        if (this._program.errors) {
            this.errors = this._program.errors;
            return;
        }
        const program = this._program;
        this._uRenderPass = program.getLocation("renderPass");
        this._uColor = program.getLocation("color");
        this._uWorldMatrix = program.getLocation("sceneModelMatrix");
        this._uViewMatrix = program.getLocation("viewMatrix");
        this._uProjMatrix = program.getLocation("projMatrix");
        this._uSectionPlanes = [];
        for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            this._uSectionPlanes.push({
                active: program.getLocation("sectionPlaneActive" + i),
                pos: program.getLocation("sectionPlanePos" + i),
                dir: program.getLocation("sectionPlaneDir" + i)
            });
        }
        if (scene.logarithmicDepthBufferEnabled) {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
        this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix";
        this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags";
        this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates";
        this._uTexturePerPolygonIdNormals = "uTexturePerPolygonIdNormals";
        this._uTexturePerPolygonIdIndices = "uTexturePerPolygonIdIndices";
        this._uTexturePerPolygonIdPortionIds = "uTexturePerPolygonIdPortionIds";
        this._uTexturePerObjectMatrix= "uTexturePerObjectMatrix";
        this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc");
    }

    _bindProgram(frameCtx) {

        const scene = this._scene;
        const gl = scene.canvas.gl;
        const project = scene.camera.project;

        this._program.bind();


        if (scene.logarithmicDepthBufferEnabled) {
            const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }
    }

    _buildShader() {
        return {
            vertex: this._buildVertexShader(),
            fragment: this._buildFragmentShader()
        };
    }

    _buildVertexShader() {

        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;

        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles dataTexture silhouette vertex shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");

        src.push("uniform int renderPass;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        src.push("uniform mat4 sceneModelMatrix;");
        src.push("uniform mat4 viewMatrix;");
        src.push("uniform mat4 projMatrix;");

        // src.push("uniform sampler2D uOcclusionTexture;"); 

        src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;");
        src.push("uniform highp sampler2D uTexturePerObjectMatrix;");
        src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;");
        src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;");
        src.push("uniform highp usampler2D uTexturePerPolygonIdIndices;");
        src.push("uniform mediump usampler2D uTexturePerPolygonIdPortionIds;");
        src.push("uniform vec3 uCameraEyeRtc;");

        src.push("vec3 positions[3];");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("out float isPerspective;");
        }

        src.push("bool isPerspectiveMatrix(mat4 m) {");
        src.push("    return (m[2][3] == - 1.0);");
        src.push("}");

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("flat out uint vFlags2;");
        }

        src.push("void main(void) {");

        // constants
        src.push("int polygonIndex = gl_VertexID / 3;");

        // get packed object-id
        src.push("int h_packed_object_id_index = (polygonIndex >> 3) & 4095;");
        src.push("int v_packed_object_id_index = (polygonIndex >> 3) >> 12;");

        src.push("int objectIndex = int(texelFetch(uTexturePerPolygonIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);");
        src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);");

        // get flags & flags2
        src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);");
        src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);");

        // flags.y = NOT_RENDERED | SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | SILHOUETTE_XRAYED
        // renderPass = SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED

        src.push(`if (int(flags.y) != renderPass) {`);
        src.push("   gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("   return;"); // Cull vertex
        src.push("} else {");

        // get vertex base
        src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));");

        src.push("ivec4 packedIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+5, objectIndexCoords.y), 0));");

        src.push("int indexBaseOffset = (packedIndexBaseOffset.r << 24) + (packedIndexBaseOffset.g << 16) + (packedIndexBaseOffset.b << 8) + packedIndexBaseOffset.a;");

        src.push("int h_index = (polygonIndex - indexBaseOffset) & 4095;");
        src.push("int v_index = (polygonIndex - indexBaseOffset) >> 12;");

        src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdIndices, ivec2(h_index, v_index), 0));");
        src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;");

        src.push("ivec3 indexPositionH = uniqueVertexIndexes & 4095;");
        src.push("ivec3 indexPositionV = uniqueVertexIndexes >> 12;");

        src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");

        src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");

        src.push("uint solid = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+7, objectIndexCoords.y), 0).r;");

        // get position
        src.push("positions[0] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.r, indexPositionV.r), 0));");
        src.push("positions[1] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.g, indexPositionV.g), 0));");
        src.push("positions[2] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.b, indexPositionV.b), 0));");

        // get normal
        src.push("vec3 normal = normalize(cross(positions[2] - positions[0], positions[1] - positions[0]));");

        src.push("vec3 position;");
        src.push("position = positions[gl_VertexID % 3];");

        src.push("vec3 viewNormal = -normalize((transpose(inverse(viewMatrix*objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);");

        // when the geometry is not solid, if needed, flip the triangle winding
        src.push("if (solid != 1u) {");
        src.push("if (isPerspectiveMatrix(projMatrix)) {");
        src.push("vec3 uCameraEyeRtcInQuantizedSpace = (inverse(sceneModelMatrix * objectDecodeAndInstanceMatrix) * vec4(uCameraEyeRtc, 1)).xyz;");
        src.push("if (dot(position.xyz - uCameraEyeRtcInQuantizedSpace, normal) < 0.0) {");
        src.push("position = positions[2 - (gl_VertexID % 3)];");
        src.push("viewNormal = -viewNormal;");
        src.push("}");
        src.push("} else {");
        src.push("if (viewNormal.z < 0.0) {");
        src.push("position = positions[2 - (gl_VertexID % 3)];");
        src.push("viewNormal = -viewNormal;");
        src.push("}");
        src.push("}");
        src.push("}");

        src.push("vec4 worldPosition = sceneModelMatrix *  (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); ");
        src.push("vec4 viewPosition = viewMatrix * worldPosition; ");

        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags2 = flags2.r;");
        }
        src.push("gl_Position = clipPos;");
        src.push("}");

        src.push("}");

        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Triangles dataTexture draw fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("flat in uint vFlags2;");
            for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
            src.push("uniform float sliceThickness;");
            src.push("uniform vec4 sliceColor;");
        }
        src.push("uniform vec4 color;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        src.push("  vec4 newColor;");
        src.push("  newColor = color;");
        if (clipping) {
            src.push("  bool clippable = vFlags2 > 0u;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > sliceThickness) { ");
            src.push("      discard;");
            src.push("  }");
            src.push("  if (dist > 0.0) { ");
            src.push("      newColor = sliceColor;");
            src.push("  }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("    outColor = newColor;");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

const defaultColor = new Float32Array([0, 0, 0, 1]);

const tempVec3a$n = math.vec3();
const tempVec3b$j = math.vec3();
math.vec3();
const tempMat4a$a = math.mat4();

/**
 * @private
 */
class DTXTrianglesEdgesRenderer {

    constructor(scene) {
        this._scene = scene;
        this._hash = this._getHash();
        this._allocate();
    }

    getValid() {
        return this._hash === this._getHash();
    };

    _getHash() {
        return this._scene._sectionPlanesState.getHash();
    }

    drawLayer(frameCtx, dataTextureLayer, renderPass) {

        const model = dataTextureLayer.model;
        const scene = model.scene;
        const camera = scene.camera;
        const gl = scene.canvas.gl;
        const state = dataTextureLayer._state;
        const textureState = state.textureState;
        const origin = dataTextureLayer._state.origin;
        const {position, rotationMatrix} = model;
        const viewMatrix = camera.viewMatrix;

        if (!this._program) {
            this._allocate(dataTextureLayer);
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram();
        }

        textureState.bindCommonTextures(
            this._program,
            this.uTexturePerObjectPositionsDecodeMatrix,
            this._uTexturePerVertexIdCoordinates,
            this.uTexturePerObjectColorsAndFlags,
            this._uTexturePerObjectMatrix
        );

        let rtcViewMatrix;

        const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0);
        const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0);
        if (gotOrigin || gotPosition) {
            const rtcOrigin = tempVec3a$n;
            if (gotOrigin) {
                const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3b$j);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$a);
        } else {
            rtcViewMatrix = viewMatrix;
        }

        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniformMatrix4fv(this._uSceneModelMatrix, false, rotationMatrix);
        gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix);
        gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix);

        if (renderPass === RENDER_PASSES.EDGES_XRAYED) {
            const material = scene.xrayMaterial._state;
            const edgeColor = material.edgeColor;
            const edgeAlpha = material.edgeAlpha;
            gl.uniform4f(this._uColor, edgeColor[0], edgeColor[1], edgeColor[2], edgeAlpha);

        } else if (renderPass === RENDER_PASSES.EDGES_HIGHLIGHTED) {
            const material = scene.highlightMaterial._state;
            const edgeColor = material.edgeColor;
            const edgeAlpha = material.edgeAlpha;
            gl.uniform4f(this._uColor, edgeColor[0], edgeColor[1], edgeColor[2], edgeAlpha);

        } else if (renderPass === RENDER_PASSES.EDGES_SELECTED) {
            const material = scene.selectedMaterial._state;
            const edgeColor = material.edgeColor;
            const edgeAlpha = material.edgeAlpha;
            gl.uniform4f(this._uColor, edgeColor[0], edgeColor[1], edgeColor[2], edgeAlpha);

        } else {
            gl.uniform4fv(this._uColor, defaultColor);
        }

        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes;
            const renderFlags = model.renderFlags;
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$n, model.matrix);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }

        if (state.numEdgeIndices8Bits > 0) {
            textureState.bindEdgeIndicesTextures(
                this._program,
                this._uTexturePerEdgeIdPortionIds,
                this._uTexturePerPolygonIdEdgeIndices,
                8 // 8 bits edge indices
            );

            gl.drawArrays(gl.LINES, 0, state.numEdgeIndices8Bits);
        }

        if (state.numEdgeIndices16Bits > 0) {
            textureState.bindEdgeIndicesTextures(
                this._program,
                this._uTexturePerEdgeIdPortionIds,
                this._uTexturePerPolygonIdEdgeIndices,
                16 // 16 bits edge indices
            );

            gl.drawArrays(gl.LINES, 0, state.numEdgeIndices16Bits);
        }

        if (state.numEdgeIndices32Bits > 0) {
            textureState.bindEdgeIndicesTextures(
                this._program,
                this._uTexturePerEdgeIdPortionIds,
                this._uTexturePerPolygonIdEdgeIndices,
                32 // 32 bits edge indices
            );

            gl.drawArrays(gl.LINES, 0, state.numEdgeIndices32Bits);
        }

        frameCtx.drawElements++;
    }

    _allocate() {
        const scene = this._scene;
        const gl = scene.canvas.gl;
        this._program = new Program(gl, this._buildShader());
        if (this._program.errors) {
            this.errors = this._program.errors;
            return;
        }
        const program = this._program;
        this._uRenderPass = program.getLocation("renderPass");
        this._uColor = program.getLocation("color");
        this._uSceneModelMatrix = program.getLocation("sceneModelMatrix");
        this._uWorldMatrix = program.getLocation("worldMatrix");
        this._uViewMatrix = program.getLocation("viewMatrix");
        this._uProjMatrix = program.getLocation("projMatrix");
        this._uSectionPlanes = [];
        for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            this._uSectionPlanes.push({
                active: program.getLocation("sectionPlaneActive" + i),
                pos: program.getLocation("sectionPlanePos" + i),
                dir: program.getLocation("sectionPlaneDir" + i)
            });
        }
        if (scene.logarithmicDepthBufferEnabled) {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
        this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix";
        this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags";
        this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates";
        this._uTexturePerPolygonIdEdgeIndices = "uTexturePerPolygonIdEdgeIndices";
        this._uTexturePerEdgeIdPortionIds = "uTexturePerEdgeIdPortionIds";
        this._uTexturePerObjectMatrix= "uTexturePerObjectMatrix";
    }

    _bindProgram() {
        const scene = this._scene;
        const gl = scene.canvas.gl;
        const program = this._program;
        const project = scene.camera.project;
        program.bind();
        if (scene.logarithmicDepthBufferEnabled) {
            const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }
    }

    _buildShader() {
        return {
            vertex: this._buildVertexShader(),
            fragment: this._buildFragmentShader()
        };
    }

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// DTXTrianglesEdgesRenderer vertex shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");

        src.push("uniform int renderPass;");

        // if (scene.entityOffsetsEnabled) {
        //     src.push("in vec3 offset;");
        // }

        src.push("uniform mat4 sceneModelMatrix;");
        src.push("uniform mat4 viewMatrix;");
        src.push("uniform mat4 projMatrix;");

        src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;");
        src.push("uniform highp sampler2D uTexturePerObjectMatrix;");
        src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;");
        src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;");
        src.push("uniform highp usampler2D uTexturePerPolygonIdEdgeIndices;");
        src.push("uniform mediump usampler2D uTexturePerEdgeIdPortionIds;");

        src.push("uniform vec4 color;");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("flat out uint vFlags2;");
        }
        src.push("out vec4 vColor;");

        src.push("void main(void) {");

        // constants
        src.push("int edgeIndex = gl_VertexID / 2;");

        // get packed object-id
        src.push("int h_packed_object_id_index = (edgeIndex >> 3) & 4095;");
        src.push("int v_packed_object_id_index = (edgeIndex >> 3) >> 12;");

        src.push("int objectIndex = int(texelFetch(uTexturePerEdgeIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);");
        src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);");

        // get flags & flags2
        src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);");
        src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);");

        // flags.z = NOT_RENDERED | EDGES_COLOR_OPAQUE | EDGES_COLOR_TRANSPARENT | EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED
        // renderPass = EDGES_COLOR_OPAQUE | EDGES_COLOR_TRANSPARENT | EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED

        src.push(`if (int(flags.z) != renderPass) {`);
        src.push("   gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("   return;"); // Cull vertex
        src.push("} else {");

        // get vertex base
        src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));");

        src.push("ivec4 packedEdgeIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+6, objectIndexCoords.y), 0));");

        src.push("int edgeIndexBaseOffset = (packedEdgeIndexBaseOffset.r << 24) + (packedEdgeIndexBaseOffset.g << 16) + (packedEdgeIndexBaseOffset.b << 8) + packedEdgeIndexBaseOffset.a;");

        src.push("int h_index = (edgeIndex - edgeIndexBaseOffset) & 4095;");
        src.push("int v_index = (edgeIndex - edgeIndexBaseOffset) >> 12;");

        src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdEdgeIndices, ivec2(h_index, v_index), 0));");
        src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;");

        src.push("int indexPositionH = uniqueVertexIndexes[gl_VertexID % 2] & 4095;");
        src.push("int indexPositionV = uniqueVertexIndexes[gl_VertexID % 2] >> 12;");

        src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");
        src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");

        // get flags & flags2
        src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);");
        src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);");

        // get position
        src.push("vec3 position = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH, indexPositionV), 0));");

        src.push("mat4 matrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");
        src.push("vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); ");
        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");

        if (clipping) {
            src.push("  vWorldPosition = worldPosition;");
            src.push("  vFlags2 = flags2.r;");
        }

        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("vColor = vec4(color.r, color.g, color.b, color.a);");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// DTXTrianglesEdgesRenderer fragment shader");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("#extension GL_EXT_frag_depth : enable");
        }
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("flat in uint vFlags2;");
            for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
            src.push("uniform float sliceThickness;");
            src.push("uniform vec4 sliceColor;");
        }
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        src.push("  vec4 newColor;");
        src.push("  newColor = vColor;");
        if (clipping) {
            src.push("  bool clippable = vFlags2 > 0u;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > sliceThickness) { ");
            src.push("      discard;");
            src.push("  }");
            src.push("  if (dist > 0.0) { ");
            src.push("      newColor = sliceColor;");
            src.push("  }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("   outColor            = newColor;");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

const tempVec3a$m = math.vec3();
const tempVec3b$i = math.vec3();
const tempMat4a$9 = math.mat4();

/**
 * @private
 */
class DTXTrianglesEdgesColorRenderer {

    constructor(scene) {
        this._scene = scene;
        this._hash = this._getHash();
        this._allocate();
    }

    getValid() {
        return this._hash === this._getHash();
    };

    _getHash() {
        return this._scene._sectionPlanesState.getHash();
    }

    drawLayer(frameCtx, dataTextureLayer, renderPass) {
        const model = dataTextureLayer.model;
        const scene = model.scene;
        const camera = scene.camera;
        const gl = scene.canvas.gl;
        const state = dataTextureLayer._state;
        const textureState = state.textureState;
        const origin = dataTextureLayer._state.origin;
        const {position, rotationMatrix} = model;
        const viewMatrix = camera.viewMatrix;

        if (!this._program) {
            this._allocate();
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram();
        }

        textureState.bindCommonTextures(
            this._program,
            this.uTexturePerObjectPositionsDecodeMatrix,
            this._uTexturePerVertexIdCoordinates,
            this.uTexturePerObjectColorsAndFlags,
            this._uTexturePerObjectMatrix
        );

        let rtcViewMatrix;

        const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0);
        const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0);
        if (gotOrigin || gotPosition) {
            const rtcOrigin = tempVec3a$m;
            if (gotOrigin) {
                const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3b$i);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$9);
        } else {
            rtcViewMatrix = viewMatrix;
        }

        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniformMatrix4fv(this._uSceneModelMatrix, false, rotationMatrix);
        gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix);
        gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix);

        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes;
            const renderFlags = model.renderFlags;
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$m, model.matrix);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }

        if (state.numEdgeIndices8Bits > 0) {
            textureState.bindEdgeIndicesTextures(
                this._program,
                this._uTexturePerEdgeIdPortionIds,
                this._uTexturePerPolygonIdEdgeIndices,
                8 // 8 bits edge indices
            );

            gl.drawArrays(gl.LINES, 0, state.numEdgeIndices8Bits);
        }

        if (state.numEdgeIndices16Bits > 0) {
            textureState.bindEdgeIndicesTextures(
                this._program,
                this._uTexturePerEdgeIdPortionIds,
                this._uTexturePerPolygonIdEdgeIndices,
                16 // 16 bits edge indices
            );

            gl.drawArrays(gl.LINES, 0, state.numEdgeIndices16Bits);
        }

        if (state.numEdgeIndices32Bits > 0) {
            textureState.bindEdgeIndicesTextures(
                this._program,
                this._uTexturePerEdgeIdPortionIds,
                this._uTexturePerPolygonIdEdgeIndices,
                32 // 32 bits edge indices
            );

            gl.drawArrays(gl.LINES, 0, state.numEdgeIndices32Bits);
        }

        frameCtx.drawElements++;
    }

    _allocate() {

        const scene = this._scene;
        const gl = scene.canvas.gl;

        this._program = new Program(gl, this._buildShader());

        if (this._program.errors) {
            this.errors = this._program.errors;
            return;
        }

        const program = this._program;

        this._uRenderPass = program.getLocation("renderPass");
        this._uSceneModelMatrix = program.getLocation("sceneModelMatrix");
        this._uViewMatrix = program.getLocation("viewMatrix");
        this._uProjMatrix = program.getLocation("projMatrix");
        this._uSectionPlanes = [];

        for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            this._uSectionPlanes.push({
                active: program.getLocation("sectionPlaneActive" + i),
                pos: program.getLocation("sectionPlanePos" + i),
                dir: program.getLocation("sectionPlaneDir" + i)
            });
        }

        //this._aOffset = program.getAttribute("offset");

        if (scene.logarithmicDepthBufferEnabled) {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }

        this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix";
        this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags";
        this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates";
        this._uTexturePerPolygonIdEdgeIndices = "uTexturePerPolygonIdEdgeIndices";
        this._uTexturePerEdgeIdPortionIds = "uTexturePerEdgeIdPortionIds";
        this._uTexturePerObjectMatrix = "uTexturePerObjectMatrix";
    }

    _bindProgram() {

        const scene = this._scene;
        const gl = scene.canvas.gl;
        const program = this._program;
        const project = scene.camera.project;

        program.bind();

        if (scene.logarithmicDepthBufferEnabled) {
            const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }
    }

    _buildShader() {
        return {
            vertex: this._buildVertexShader(),
            fragment: this._buildFragmentShader()
        };
    }

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// TrianglesDataTextureEdgesColorRenderer");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");

        src.push("uniform int renderPass;");

        if (scene.entityOffsetsEnabled) ;

        src.push("uniform mat4 sceneModelMatrix;");
        src.push("uniform mat4 viewMatrix;");
        src.push("uniform mat4 projMatrix;");

        src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;");
        src.push("uniform highp sampler2D uTexturePerObjectMatrix;");
        src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;");
        src.push("uniform highp sampler2D uObjectPerObjectOffsets;");
        src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;");
        src.push("uniform highp usampler2D uTexturePerPolygonIdEdgeIndices;");
        src.push("uniform mediump usampler2D uTexturePerEdgeIdPortionIds;");

        //  src.push("uniform vec4 color;");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("flat out uint vFlags2;");
        }
        src.push("out vec4 vColor;");

        src.push("void main(void) {");

        // constants
        src.push("int edgeIndex = gl_VertexID / 2;");

        // get packed object-id
        src.push("int h_packed_object_id_index = (edgeIndex >> 3) & 4095;");
        src.push("int v_packed_object_id_index = (edgeIndex >> 3) >> 12;");

        src.push("int objectIndex = int(texelFetch(uTexturePerEdgeIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);");
        src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);");

        // get flags & flags2
        src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);");
        src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);");

        // flags.z = NOT_RENDERED | EDGES_COLOR_OPAQUE | EDGES_COLOR_TRANSPARENT | EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED
        // renderPass = EDGES_COLOR_OPAQUE | EDGES_COLOR_TRANSPARENT

        src.push(`if (int(flags.z) != renderPass) {`);
        src.push("   gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("   return;"); // Cull vertex
        src.push("} else {");

        // get vertex base
        src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));");

        src.push("ivec4 packedEdgeIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+6, objectIndexCoords.y), 0));");

        src.push("int edgeIndexBaseOffset = (packedEdgeIndexBaseOffset.r << 24) + (packedEdgeIndexBaseOffset.g << 16) + (packedEdgeIndexBaseOffset.b << 8) + packedEdgeIndexBaseOffset.a;");

        src.push("int h_index = (edgeIndex - edgeIndexBaseOffset) & 4095;");
        src.push("int v_index = (edgeIndex - edgeIndexBaseOffset) >> 12;");

        src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdEdgeIndices, ivec2(h_index, v_index), 0));");
        src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;");

        src.push("int indexPositionH = uniqueVertexIndexes[gl_VertexID % 2] & 4095;");
        src.push("int indexPositionV = uniqueVertexIndexes[gl_VertexID % 2] >> 12;");

        src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");

        src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");

        // get flags & flags2
        src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);");
        src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);");

        // get position
        src.push("vec3 position = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH, indexPositionV), 0));");

        // get color
        src.push("uvec4 color = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+0, objectIndexCoords.y), 0);");

        src.push(`if (color.a == 0u) {`);
        src.push("   gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("   return;");
        src.push("};");

        src.push("vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); ");
        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");

        if (clipping) {
            src.push("  vWorldPosition = worldPosition;");
            src.push("  vFlags2 = flags2.r;");
        }

        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("vec4 rgb = vec4(color.rgba);");
        //src.push("vColor = vec4(float(color.r-100.0) / 255.0, float(color.g-100.0) / 255.0, float(color.b-100.0) / 255.0, float(color.a) / 255.0);");
        src.push("vColor = vec4(float(rgb.r*0.5) / 255.0, float(rgb.g*0.5) / 255.0, float(rgb.b*0.5) / 255.0, float(rgb.a) / 255.0);");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// TrianglesDataTextureEdgesColorRenderer");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("flat in uint vFlags2;");
            for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
            src.push("uniform float sliceThickness;");
            src.push("uniform vec4 sliceColor;");
        }
        src.push("in vec4 vColor;");
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        src.push("  vec4 newColor;");
        src.push("  newColor = vColor;");
        if (clipping) {
            src.push("  bool clippable = vFlags2 > 0u;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > sliceThickness) { ");
            src.push("      discard;");
            src.push("  }");
            src.push("  if (dist > 0.0) { ");
            src.push("      newColor = sliceColor;");
            src.push("  }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
              src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("   outColor            = newColor;");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

const tempVec3a$l = math.vec3();
const tempVec3b$h = math.vec3();
const tempVec3c$f = math.vec3();
const tempMat4a$8 = math.mat4();

/**
 * @private
 */
class DTXTrianglesPickMeshRenderer {

    constructor(scene) {
        this._scene = scene;
        this._hash = this._getHash();
        this._allocate();
    }

    getValid() {
        return this._hash === this._getHash();
    }

    _getHash() {
        return this._scene._sectionPlanesState.getHash();
    }

    drawLayer(frameCtx, dataTextureLayer, renderPass) {
        if (!this._program) {
            this._allocate(dataTextureLayer);
            if (this.errors) {
                return;
            }
        }
        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram(frameCtx);
        }
        const model = dataTextureLayer.model;
        const scene = model.scene;
        const camera = scene.camera;
        const gl = scene.canvas.gl;
        const state = dataTextureLayer._state;
        const textureState = state.textureState;
        const origin = dataTextureLayer._state.origin;
        const {position, rotationMatrix} = model;

        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;
        const projMatrix = frameCtx.pickProjMatrix || camera.projMatrix;
        const eye = frameCtx.pickOrigin || camera.eye;
        const far = frameCtx.pickProjMatrix ? frameCtx.pickZFar : camera.project.far;

        textureState.bindCommonTextures(
            this._program,
            this.uTexturePerObjectPositionsDecodeMatrix,
            this._uTexturePerVertexIdCoordinates,
            this.uTexturePerObjectColorsAndFlags,
            this._uTexturePerObjectMatrix
        );
        let rtcViewMatrix;
        let rtcCameraEye;
        const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0);
        const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0);
        if (gotOrigin || gotPosition) {
            const rtcOrigin = tempVec3a$l;
            if (gotOrigin) {
                const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3b$h);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$8);
            rtcCameraEye = tempVec3c$f;
            rtcCameraEye[0] = eye[0] - rtcOrigin[0];
            rtcCameraEye[1] = eye[1] - rtcOrigin[1];
            rtcCameraEye[2] = eye[2] - rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            rtcCameraEye = eye;
        }
        gl.uniform2fv(this._uPickClipPos, frameCtx.pickClipPos);
        gl.uniform2f(this._uDrawingBufferSize, gl.drawingBufferWidth, gl.drawingBufferHeight);
        gl.uniformMatrix4fv(this._uSceneModelMatrix, false, rotationMatrix);
        gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix);
        gl.uniformMatrix4fv(this._uProjMatrix, false, projMatrix);
        gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye);
        gl.uniform1i(this._uRenderPass, renderPass);
        if (scene.logarithmicDepthBufferEnabled) {
            const logDepthBufFC = 2.0 / (Math.log(far + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }
        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes;
            const renderFlags = model.renderFlags;
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$l, model.matrix);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }
        if (state.numIndices8Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                8 // 8 bits indices
            );
            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices8Bits);
        }
        if (state.numIndices16Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                16 // 16 bits indices
            );
            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices16Bits);
        }
        if (state.numIndices32Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                32 // 32 bits indices
            );
            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices32Bits);
        }
        frameCtx.drawElements++;
    }

    _allocate() {
        const scene = this._scene;
        const gl = scene.canvas.gl;
        this._program = new Program(gl, this._buildShader());
        if (this._program.errors) {
            this.errors = this._program.errors;
            return;
        }
        const program = this._program;
        this._uRenderPass = program.getLocation("renderPass");
        this._uPickInvisible = program.getLocation("pickInvisible");
        this._uPickClipPos = program.getLocation("pickClipPos");
        this._uDrawingBufferSize = program.getLocation("drawingBufferSize");
        this._uSceneModelMatrix = program.getLocation("sceneModelMatrix");
        this._uViewMatrix = program.getLocation("viewMatrix");
        this._uProjMatrix = program.getLocation("projMatrix");
        this._uSectionPlanes = [];
        for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            this._uSectionPlanes.push({
                active: program.getLocation("sectionPlaneActive" + i),
                pos: program.getLocation("sectionPlanePos" + i),
                dir: program.getLocation("sectionPlaneDir" + i)
            });
        }
        if (scene.logarithmicDepthBufferEnabled) {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
        this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix";
        this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags";
        this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates";
        this._uTexturePerPolygonIdNormals = "uTexturePerPolygonIdNormals";
        this._uTexturePerPolygonIdIndices = "uTexturePerPolygonIdIndices";
        this._uTexturePerPolygonIdPortionIds = "uTexturePerPolygonIdPortionIds";
        this._uTexturePerObjectMatrix = "uTexturePerObjectMatrix";
        this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc");
    }

    _bindProgram(frameCtx) {
        const scene = this._scene;
        const gl = scene.canvas.gl;
        this._program.bind();
        gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible);
    }

    _buildShader() {
        return {
            vertex: this._buildVertexShader(),
            fragment: this._buildFragmentShader()
        };
    }

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Batched geometry picking vertex shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");

        src.push("uniform int renderPass;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        src.push("uniform mat4 sceneModelMatrix;");
        src.push("uniform mat4 viewMatrix;");
        src.push("uniform mat4 projMatrix;");

        src.push("uniform bool pickInvisible;");
        // src.push("uniform sampler2D uOcclusionTexture;"); 

        src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;");
        src.push("uniform highp sampler2D uTexturePerObjectMatrix;");
        src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;");
        src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;");
        src.push("uniform highp usampler2D uTexturePerPolygonIdIndices;");
        src.push("uniform mediump usampler2D uTexturePerPolygonIdPortionIds;");
        src.push("uniform vec3 uCameraEyeRtc;");

        src.push("vec3 positions[3];");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("out float isPerspective;");
        }

        src.push("uniform vec2 pickClipPos;");
        src.push("uniform vec2 drawingBufferSize;");

        src.push("vec4 remapClipPos(vec4 clipPos) {");
        src.push("    clipPos.xy /= clipPos.w;");
        src.push(`    clipPos.xy = (clipPos.xy - pickClipPos) * drawingBufferSize;`);
        src.push("    clipPos.xy *= clipPos.w;");
        src.push("    return clipPos;");
        src.push("}");

        src.push("bool isPerspectiveMatrix(mat4 m) {");
        src.push("    return (m[2][3] == - 1.0);");
        src.push("}");

        if (clipping) {
            src.push("smooth out vec4 vWorldPosition;");
            src.push("flat out uvec4 vFlags2;");
        }

        src.push("out vec4 vPickColor;");

        src.push("void main(void) {");

        src.push("int polygonIndex = gl_VertexID / 3;");

        // get packed object-id
        src.push("int h_packed_object_id_index = (polygonIndex >> 3) & 4095;");
        src.push("int v_packed_object_id_index = (polygonIndex >> 3) >> 12;");

        src.push("int objectIndex = int(texelFetch(uTexturePerPolygonIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);");
        src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);");

        // get flags & flags2
        src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);");
        src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);");

        // flags.w = NOT_RENDERED | PICK
        // renderPass = PICK

        src.push(`if (int(flags.w) != renderPass) {`);
        src.push("   gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("   return;"); // Cull vertex
        src.push("} else {");

        // get vertex base
        src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));");

        src.push("ivec4 packedIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+5, objectIndexCoords.y), 0));");

        src.push("int indexBaseOffset = (packedIndexBaseOffset.r << 24) + (packedIndexBaseOffset.g << 16) + (packedIndexBaseOffset.b << 8) + packedIndexBaseOffset.a;");

        src.push("int h_index = (polygonIndex - indexBaseOffset) & 4095;");
        src.push("int v_index = (polygonIndex - indexBaseOffset) >> 12;");

        src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdIndices, ivec2(h_index, v_index), 0));");
        src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;");

        src.push("ivec3 indexPositionH = uniqueVertexIndexes & 4095;");
        src.push("ivec3 indexPositionV = uniqueVertexIndexes >> 12;");

        src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");

        src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");

        src.push("uint solid = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+7, objectIndexCoords.y), 0).r;");

        // get position
        src.push("positions[0] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.r, indexPositionV.r), 0));");
        src.push("positions[1] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.g, indexPositionV.g), 0));");
        src.push("positions[2] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.b, indexPositionV.b), 0));");

        // get color
        src.push("uvec4 color = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+0, objectIndexCoords.y), 0);");

        // get pick-color
        src.push("vPickColor = vec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+1, objectIndexCoords.y), 0)) / 255.0;");

        // get normal
        src.push("vec3 normal = normalize(cross(positions[2] - positions[0], positions[1] - positions[0]));");

        src.push("vec3 position;");
        src.push("position = positions[gl_VertexID % 3];");

        // when the geometry is not solid, if needed, flip the triangle winding
        src.push("if (solid != 1u) {");
        src.push("if (isPerspectiveMatrix(projMatrix)) {");
        src.push("vec3 uCameraEyeRtcInQuantizedSpace = (inverse(sceneModelMatrix * objectDecodeAndInstanceMatrix) * vec4(uCameraEyeRtc, 1)).xyz;");
        src.push("if (dot(position.xyz - uCameraEyeRtcInQuantizedSpace, normal) < 0.0) {");
        src.push("position = positions[2 - (gl_VertexID % 3)];");
        src.push("}");
        src.push("} else {");
        src.push("vec3 viewNormal = -normalize((transpose(inverse(viewMatrix*objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);");
        src.push("if (viewNormal.z < 0.0) {");
        src.push("position = positions[2 - (gl_VertexID % 3)];");
        src.push("}");
        src.push("}");
        src.push("}");

        src.push("vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); ");

        src.push("vec4 viewPosition = viewMatrix * worldPosition; ");

        if (clipping) {
            src.push("      vWorldPosition = worldPosition;");
            src.push("      vFlags2 = flags2;");
        }
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = remapClipPos(clipPos);");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Batched geometry picking fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("flat in uvec4 vFlags2;");
            for (var i = 0; i < scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vPickColor;");
        src.push("out vec4 outPickColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (float(vFlags2.x) > 0.0);");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (var i = 0; i < scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
            //src.push("    gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("   outPickColor = vPickColor; ");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

const tempVec3a$k = math.vec3();
const tempVec3b$g = math.vec3();
const tempVec3c$e = math.vec3();
math.vec3();
const tempMat4a$7 = math.mat4();

/**
 * @private
 */
class DTXTrianglesPickDepthRenderer {

    constructor(scene) {
        this._scene = scene;
        this._hash = this._getHash();
        this._allocate();
    }

    getValid() {
        return this._hash === this._getHash();
    }

    _getHash() {
        return this._scene._sectionPlanesState.getHash();
    }

    drawLayer(frameCtx, dataTextureLayer, renderPass) {

        const model = dataTextureLayer.model;
        const scene = model.scene;
        const camera = scene.camera;
        const gl = scene.canvas.gl;
        const state = dataTextureLayer._state;
        const textureState = state.textureState;
        const origin = dataTextureLayer._state.origin;
        const {position, rotationMatrix} = model;

        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;
        const projMatrix = frameCtx.pickProjMatrix || camera.projMatrix;
        const eye = frameCtx.pickOrigin || camera.eye;
        const far = frameCtx.pickProjMatrix ? frameCtx.pickZFar : camera.project.far;

        if (!this._program) {
            this._allocate();
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram();
        }

        textureState.bindCommonTextures(
            this._program,
            this.uTexturePerObjectPositionsDecodeMatrix,
            this._uTexturePerVertexIdCoordinates,
            this.uTexturePerObjectColorsAndFlags,
            this._uTexturePerObjectMatrix
        );

        let rtcViewMatrix;
        let rtcCameraEye;

        if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) {
            const rtcOrigin = tempVec3a$k;
            if (origin) {
                const rotatedOrigin = tempVec3b$g;
                math.transformPoint3(rotationMatrix, origin, rotatedOrigin);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$7);
            rtcCameraEye = tempVec3c$e;
            rtcCameraEye[0] = eye[0] - rtcOrigin[0];
            rtcCameraEye[1] = eye[1] - rtcOrigin[1];
            rtcCameraEye[2] = eye[2] - rtcOrigin[2];
            frameCtx.snapPickOrigin[0] = rtcOrigin[0];
            frameCtx.snapPickOrigin[1] = rtcOrigin[1];
            frameCtx.snapPickOrigin[2] = rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            rtcCameraEye = eye;
            frameCtx.snapPickOrigin[0] = 0;
            frameCtx.snapPickOrigin[1] = 0;
            frameCtx.snapPickOrigin[2] = 0;
        }

        gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye);
        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible);
        gl.uniform2fv(this._uPickClipPos, frameCtx.pickClipPos);
        gl.uniform2f(this._uDrawingBufferSize, gl.drawingBufferWidth, gl.drawingBufferHeight);
        gl.uniform1f(this._uPickZNear, frameCtx.pickZNear);
        gl.uniform1f(this._uPickZFar, frameCtx.pickZFar);
        gl.uniformMatrix4fv(this._uSceneModelMatrix, false, rotationMatrix);
        gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix);
        gl.uniformMatrix4fv(this._uProjMatrix, false, projMatrix);
        if (scene.logarithmicDepthBufferEnabled) {
            const logDepthBufFC = 2.0 / (Math.log(far + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }

        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes;
            const renderFlags = model.renderFlags;
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$k, model.matrix);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }
        if (state.numIndices8Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                8 // 8 bits indices
            );
            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices8Bits);
        }
        if (state.numIndices16Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                16 // 16 bits indices
            );
            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices16Bits);
        }
        if (state.numIndices32Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                32 // 32 bits indices
            );
            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices32Bits);
        }
        frameCtx.drawElements++;
    }

    _allocate() {
        const scene = this._scene;
        const gl = scene.canvas.gl;
        this._program = new Program(gl, this._buildShader());
        if (this._program.errors) {
            this.errors = this._program.errors;
            return;
        }
        const program = this._program;
        this._uRenderPass = program.getLocation("renderPass");
        this._uPickInvisible = program.getLocation("pickInvisible");
        this._uPickClipPos = program.getLocation("pickClipPos");
        this._uDrawingBufferSize = program.getLocation("drawingBufferSize");
        this._uSceneModelMatrix = program.getLocation("sceneModelMatrix");
        this._uViewMatrix = program.getLocation("viewMatrix");
        this._uProjMatrix = program.getLocation("projMatrix");
        this._uSectionPlanes = [];
        for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            this._uSectionPlanes.push({
                active: program.getLocation("sectionPlaneActive" + i),
                pos: program.getLocation("sectionPlanePos" + i),
                dir: program.getLocation("sectionPlaneDir" + i)
            });
        }
        this._uPickZNear = program.getLocation("pickZNear");
        this._uPickZFar = program.getLocation("pickZFar");
        if (scene.logarithmicDepthBufferEnabled) {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
        this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix";
        this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags";
        this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates";
        this._uTexturePerPolygonIdNormals = "uTexturePerPolygonIdNormals";
        this._uTexturePerPolygonIdIndices = "uTexturePerPolygonIdIndices";
        this._uTexturePerPolygonIdPortionIds = "uTexturePerPolygonIdPortionIds";
        this._uTexturePerObjectMatrix= "uTexturePerObjectMatrix";
        this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc");
    }

    _bindProgram() {
        this._program.bind();
    }

    _buildShader() {
        return {
            vertex: this._buildVertexShader(),
            fragment: this._buildFragmentShader()
        };
    }

    _buildVertexShader() {

        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles dataTexture pick depth vertex shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");

        src.push("uniform int renderPass;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        src.push("uniform mat4 sceneModelMatrix;");
        src.push("uniform mat4 viewMatrix;");
        src.push("uniform mat4 projMatrix;");
        src.push("uniform bool pickInvisible;");

        src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;");
        src.push("uniform highp sampler2D uTexturePerObjectMatrix;");
        src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;");
        src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;");
        src.push("uniform highp usampler2D uTexturePerPolygonIdIndices;");
        src.push("uniform mediump usampler2D uTexturePerPolygonIdPortionIds;");
        src.push("uniform vec3 uCameraEyeRtc;");

        src.push("vec3 positions[3];");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("out float isPerspective;");
        }

        src.push("uniform vec2 pickClipPos;");
        src.push("uniform vec2 drawingBufferSize;");

        src.push("vec4 remapClipPos(vec4 clipPos) {");
        src.push("    clipPos.xy /= clipPos.w;");
        src.push(`    clipPos.xy = (clipPos.xy - pickClipPos) * drawingBufferSize;`);
        src.push("    clipPos.xy *= clipPos.w;");
        src.push("    return clipPos;");
        src.push("}");

        src.push("bool isPerspectiveMatrix(mat4 m) {");
        src.push("    return (m[2][3] == - 1.0);");
        src.push("}");

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("flat out uint vFlags2;");
        }
        src.push("out vec4 vViewPosition;");
        src.push("void main(void) {");

        // constants
        src.push("int polygonIndex = gl_VertexID / 3;");

        // get packed object-id
        src.push("int h_packed_object_id_index = (polygonIndex >> 3) & 4095;");
        src.push("int v_packed_object_id_index = (polygonIndex >> 3) >> 12;");

        src.push("int objectIndex = int(texelFetch(uTexturePerPolygonIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);");
        src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);");

        // get flags & flags2
        src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);");
        src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);");

        // flags.w = NOT_RENDERED | PICK
        // renderPass = PICK

        src.push(`if (int(flags.w) != renderPass) {`);
        src.push("   gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("   return;"); // Cull vertex
        src.push("} else {");

        // get vertex base
        src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));");

        src.push("ivec4 packedIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+5, objectIndexCoords.y), 0));");

        src.push("int indexBaseOffset = (packedIndexBaseOffset.r << 24) + (packedIndexBaseOffset.g << 16) + (packedIndexBaseOffset.b << 8) + packedIndexBaseOffset.a;");

        src.push("int h_index = (polygonIndex - indexBaseOffset) & 4095;");
        src.push("int v_index = (polygonIndex - indexBaseOffset) >> 12;");

        src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdIndices, ivec2(h_index, v_index), 0));");
        src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;");

        src.push("ivec3 indexPositionH = uniqueVertexIndexes & 4095;");
        src.push("ivec3 indexPositionV = uniqueVertexIndexes >> 12;");

        src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");

        src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");

        src.push("uint solid = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+7, objectIndexCoords.y), 0).r;");

        // get position
        src.push("positions[0] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.r, indexPositionV.r), 0));");
        src.push("positions[1] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.g, indexPositionV.g), 0));");
        src.push("positions[2] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.b, indexPositionV.b), 0));");

        // get color
        src.push("uvec4 color = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+0, objectIndexCoords.y), 0);");

        src.push(`if (color.a == 0u) {`);
        src.push("   gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("   return;");
        src.push("};");

        // get normal
        src.push("vec3 normal = normalize(cross(positions[2] - positions[0], positions[1] - positions[0]));");

        src.push("vec3 position;");
        src.push("position = positions[gl_VertexID % 3];");

        // when the geometry is not solid, if needed, flip the triangle winding
        src.push("if (solid != 1u) {");
        src.push("if (isPerspectiveMatrix(projMatrix)) {");
        src.push("vec3 uCameraEyeRtcInQuantizedSpace = (inverse(sceneModelMatrix * objectDecodeAndInstanceMatrix) * vec4(uCameraEyeRtc, 1)).xyz;");
        src.push("if (dot(position.xyz - uCameraEyeRtcInQuantizedSpace, normal) < 0.0) {");
        src.push("position = positions[2 - (gl_VertexID % 3)];");
        src.push("}");
        src.push("} else {");
        src.push("vec3 viewNormal = -normalize((transpose(inverse(viewMatrix*objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);");
        src.push("if (viewNormal.z < 0.0) {");
        src.push("position = positions[2 - (gl_VertexID % 3)];");
        src.push("}");
        src.push("}");
        src.push("}");

        src.push("vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); ");

        src.push("vec4 viewPosition = viewMatrix * worldPosition; ");

        if (clipping) {
            src.push("      vWorldPosition = worldPosition;");
            src.push("      vFlags2 = flags2.r;");
        }
        src.push("vViewPosition = viewPosition;");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = remapClipPos(clipPos);");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {

        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Triangles dataTexture pick depth fragment shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }

        src.push("uniform float pickZNear;");
        src.push("uniform float pickZFar;");

        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("flat in uint vFlags2;");
            for (var i = 0; i < scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec4 vViewPosition;");
        src.push("vec4 packDepth(const in float depth) {");
        src.push("  const vec4 bitShift = vec4(256.0*256.0*256.0, 256.0*256.0, 256.0, 1.0);");
        src.push("  const vec4 bitMask  = vec4(0.0, 1.0/256.0, 1.0/256.0, 1.0/256.0);");
        src.push("  vec4 res = fract(depth * bitShift);");
        src.push("  res -= res.xxyz * bitMask;");
        src.push("  return res;");
        src.push("}");

        src.push("out vec4 outPackedDepth;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = vFlags2 > 0u;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (var i = 0; i < scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("    float zNormalizedDepth = abs((pickZNear + vViewPosition.z) / (pickZFar - pickZNear));");
        src.push("    outPackedDepth = packDepth(zNormalizedDepth); ");  // Must be linear depth
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

const tempVec3a$j = math.vec3();
const tempVec3b$f = math.vec3();
const tempVec3c$d = math.vec3();
const tempVec3d$4 = math.vec3();
math.vec3();
const tempMat4a$6 = math.mat4();

/**
 * @private
 */
class DTXTrianglesSnapRenderer {

    constructor(scene) {
        this._scene = scene;
        this._hash = this._getHash();
        this._allocate();
    }

    getValid() {
        return this._hash === this._getHash();
    };

    _getHash() {
        return this._scene._sectionPlanesState.getHash();
    }

    drawLayer(frameCtx, dataTextureLayer, renderPass) {

        if (!this._program) {
            this._allocate();
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram();
        }

        const model = dataTextureLayer.model;
        const scene = model.scene;
        const camera = scene.camera;
        const gl = scene.canvas.gl;
        const state = dataTextureLayer._state;
        const textureState = state.textureState;
        const origin = dataTextureLayer._state.origin;
        const {position, rotationMatrix} = model;
        const aabb = dataTextureLayer.aabb; // Per-layer AABB for best RTC accuracy
        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;

        const coordinateScaler = tempVec3a$j;
        coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT;
        coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT;
        coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT;
        
        frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]);
        frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]);
        frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]);

        textureState.bindCommonTextures(
            this._program,
            this.uTexturePerObjectPositionsDecodeMatrix,
            this._uTexturePerVertexIdCoordinates,
            this.uTexturePerObjectColorsAndFlags,
            this._uTexturePerObjectMatrix
        );

        let rtcViewMatrix;
        let rtcCameraEye;

        const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0);
        const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0);
        if (gotOrigin || gotPosition) {
            const rtcOrigin = tempVec3b$f;
            if (gotOrigin) {
                const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3c$d);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$6);
            rtcCameraEye = tempVec3d$4;
            rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0];
            rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1];
            rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2];
            frameCtx.snapPickOrigin[0] = rtcOrigin[0];
            frameCtx.snapPickOrigin[1] = rtcOrigin[1];
            frameCtx.snapPickOrigin[2] = rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            rtcCameraEye = camera.eye;
            frameCtx.snapPickOrigin[0] = 0;
            frameCtx.snapPickOrigin[1] = 0;
            frameCtx.snapPickOrigin[2] = 0;
        }

        gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye);
        gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA);
        gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB);
        gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber);
        gl.uniform3fv(this._uCoordinateScaler, coordinateScaler);
        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible);
        gl.uniformMatrix4fv(this._uSceneModelMatrix, false, rotationMatrix);
        gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix);
        gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix);
        {
            const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }
        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes;
            const renderFlags = model.renderFlags;
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$j, model.matrix);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }
        const glMode = (frameCtx.snapMode === "edge") ? gl.LINES : gl.POINTS;
        if (state.numEdgeIndices8Bits > 0) {
            textureState.bindEdgeIndicesTextures(
                this._program,
                this._uTexturePerEdgeIdPortionIds,
                this._uTexturePerPolygonIdEdgeIndices,
                8 // 8 bits edge indices
            );
            gl.drawArrays(glMode, 0, state.numEdgeIndices8Bits);
        }
        if (state.numEdgeIndices16Bits > 0) {
            textureState.bindEdgeIndicesTextures(
                this._program,
                this._uTexturePerEdgeIdPortionIds,
                this._uTexturePerPolygonIdEdgeIndices,
                16 // 16 bits edge indices
            );
            gl.drawArrays(glMode, 0, state.numEdgeIndices16Bits);
        }
        if (state.numEdgeIndices32Bits > 0) {
            textureState.bindEdgeIndicesTextures(
                this._program,
                this._uTexturePerEdgeIdPortionIds,
                this._uTexturePerPolygonIdEdgeIndices,
                32 // 32 bits edge indices
            );
            gl.drawArrays(glMode, 0, state.numEdgeIndices32Bits);
        }
        frameCtx.drawElements++;
    }

    _allocate() {
        const scene = this._scene;
        const gl = scene.canvas.gl;
        this._program = new Program(gl, this._buildShader());
        if (this._program.errors) {
            this.errors = this._program.errors;
            return;
        }
        const program = this._program;
        this._uRenderPass = program.getLocation("renderPass");
        this._uPickInvisible = program.getLocation("pickInvisible");
        this._uSceneModelMatrix = program.getLocation("sceneModelMatrix");
        this._uViewMatrix = program.getLocation("viewMatrix");
        this._uProjMatrix = program.getLocation("projMatrix");
        this._uSectionPlanes = [];
        for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            this._uSectionPlanes.push({
                active: program.getLocation("sectionPlaneActive" + i),
                pos: program.getLocation("sectionPlanePos" + i),
                dir: program.getLocation("sectionPlaneDir" + i)
            });
        }
        {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
        this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix";
        this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags";
        this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates";
        this._uTexturePerPolygonIdEdgeIndices = "uTexturePerPolygonIdEdgeIndices";
        this._uTexturePerEdgeIdPortionIds = "uTexturePerEdgeIdPortionIds";
        this._uTextureModelMatrices = "uTextureModelMatrices";
        this._uTexturePerObjectMatrix= "uTexturePerObjectMatrix";
        this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc");
        this.uVectorA = program.getLocation("uSnapVectorA");
        this.uInverseVectorAB = program.getLocation("uSnapInvVectorAB");
        this._uLayerNumber = program.getLocation("uLayerNumber");
        this._uCoordinateScaler = program.getLocation("uCoordinateScaler");
    }

    _bindProgram() {
        this._program.bind();
    }

    _buildShader() {
        return {
            vertex: this._buildVertexShader(),
            fragment: this._buildFragmentShader()
        };
    }

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Batched geometry edges drawing vertex shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");

        src.push("uniform int renderPass;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        src.push("uniform mat4 sceneModelMatrix;");
        src.push("uniform mat4 viewMatrix;");
        src.push("uniform mat4 projMatrix;");

        src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;");
        src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;");
        src.push("uniform highp sampler2D uTexturePerObjectMatrix;");
        src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;");
        src.push("uniform highp usampler2D uTexturePerPolygonIdEdgeIndices;");
        src.push("uniform mediump usampler2D uTexturePerEdgeIdPortionIds;");
        src.push("uniform vec3 uCameraEyeRtc;");
        src.push("uniform vec2 uSnapVectorA;");
        src.push("uniform vec2 uSnapInvVectorAB;");

        src.push("vec3 positions[3];");

        {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("out float isPerspective;");
        }

        src.push("vec2 remapClipPos(vec2 clipPos) {");
        src.push("    float x = (clipPos.x - uSnapVectorA.x) * uSnapInvVectorAB.x;");
        src.push("    float y = (clipPos.y - uSnapVectorA.y) * uSnapInvVectorAB.y;");
        src.push("    return vec2(x, y);");
        src.push("}");

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("flat out uint vFlags2;");
        }
        src.push("out vec4 vViewPosition;");
        src.push("out highp vec3 relativeToOriginPosition;");
        src.push("void main(void) {");

        // constants
        src.push("int edgeIndex = gl_VertexID / 2;");

        // get packed object-id
        src.push("int h_packed_object_id_index = (edgeIndex >> 3) & 4095;");
        src.push("int v_packed_object_id_index = (edgeIndex >> 3) >> 12;");

        src.push("int objectIndex = int(texelFetch(uTexturePerEdgeIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);");
        src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);");

        // get flags & flags2
        src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);");
        src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);");

        // flags.w = NOT_RENDERED | PICK
        // renderPass = PICK

        src.push(`if (int(flags.w) != renderPass) {`);
        src.push("   gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("   return;"); // Cull vertex
        src.push("}");

        src.push("{");

        // get vertex base
        src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));");
        src.push("ivec4 packedEdgeIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+6, objectIndexCoords.y), 0));");
        src.push("int edgeIndexBaseOffset = (packedEdgeIndexBaseOffset.r << 24) + (packedEdgeIndexBaseOffset.g << 16) + (packedEdgeIndexBaseOffset.b << 8) + packedEdgeIndexBaseOffset.a;");
   
        src.push("int h_index = (edgeIndex - edgeIndexBaseOffset) & 4095;");
        src.push("int v_index = (edgeIndex - edgeIndexBaseOffset) >> 12;");
   
        src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdEdgeIndices, ivec2(h_index, v_index), 0));");
   
        src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;");
   
        src.push("int indexPositionH = uniqueVertexIndexes[gl_VertexID % 2] & 4095;");
        src.push("int indexPositionV = uniqueVertexIndexes[gl_VertexID % 2] >> 12;");

        src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");
        src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");
   
        src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);");
        src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);");
   
        src.push("vec3 position = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH, indexPositionV), 0));");
   
        src.push("vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); ");
        src.push("relativeToOriginPosition = worldPosition.xyz;");
        src.push("      vec4 viewPosition  = viewMatrix * worldPosition; ");
        if (clipping) {
            src.push("  vWorldPosition = worldPosition;");
            src.push("  vFlags2 = flags2.r;");
        }
        src.push("vViewPosition = viewPosition;");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        src.push("float tmp = clipPos.w;");
        src.push("clipPos.xyzw /= tmp;");
        src.push("clipPos.xy = remapClipPos(clipPos.xy);");
        src.push("clipPos.xyzw *= tmp;");
        src.push("vViewPosition = clipPos;");
        {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("gl_PointSize = 1.0;"); // Windows needs this?
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {

        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Triangles dataTexture pick depth fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("uniform int uLayerNumber;");
        src.push("uniform vec3 uCoordinateScaler;");
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("flat in uint vFlags2;");
            for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in highp vec3 relativeToOriginPosition;");
        src.push("out highp ivec4 outCoords;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = vFlags2 > 0u;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (var i = 0; i < scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("outCoords = ivec4(relativeToOriginPosition.xyz * uCoordinateScaler.xyz, uLayerNumber);");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

const tempVec3a$i = math.vec3();
const tempVec3b$e = math.vec3();
const tempVec3c$c = math.vec3();
const tempVec3d$3 = math.vec3();
math.vec3();
const tempMat4a$5 = math.mat4();
/**
 * @private
 */
class DTXTrianglesSnapInitRenderer {

    constructor(scene) {
        this._scene = scene;
        this._hash = this._getHash();
        this._allocate();
    }

    getValid() {
        return this._hash === this._getHash();
    };

    _getHash() {
        return this._scene._sectionPlanesState.getHash();
    }

    drawLayer(frameCtx, dataTextureLayer, renderPass) {

        if (!this._program) {
            this._allocate();
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram();
        }

        const model = dataTextureLayer.model;
        const scene = model.scene;
        const camera = scene.camera;
        const gl = scene.canvas.gl;
        const state = dataTextureLayer._state;
        const textureState = state.textureState;
        const origin = dataTextureLayer._state.origin;
        const {position, rotationMatrix} = model;
        const aabb = dataTextureLayer.aabb; // Per-layer AABB for best RTC accuracy
        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;

        const coordinateScaler = tempVec3a$i;
        coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT;
        coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT;
        coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT;

        frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]);
        frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]);
        frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]);

        textureState.bindCommonTextures(
            this._program,
            this.uTexturePerObjectPositionsDecodeMatrix,
            this._uTexturePerVertexIdCoordinates,
            this.uTexturePerObjectColorsAndFlags,
            this._uTexturePerObjectMatrix
        );
        let rtcViewMatrix;
        let rtcCameraEye;
        const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0);
        const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0);
        if (gotOrigin || gotPosition) {
            const rtcOrigin = tempVec3b$e;
            if (gotOrigin) {
                const rotatedOrigin = tempVec3c$c;
                math.transformPoint3(rotationMatrix, origin, rotatedOrigin);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$5);
            rtcCameraEye = tempVec3d$3;
            rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0];
            rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1];
            rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2];
            frameCtx.snapPickOrigin[0] = rtcOrigin[0];
            frameCtx.snapPickOrigin[1] = rtcOrigin[1];
            frameCtx.snapPickOrigin[2] = rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            rtcCameraEye = camera.eye;
            frameCtx.snapPickOrigin[0] = 0;
            frameCtx.snapPickOrigin[1] = 0;
            frameCtx.snapPickOrigin[2] = 0;
        }

        gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye);
        gl.uniform2fv(this._uVectorA, frameCtx.snapVectorA);
        gl.uniform2fv(this._uInverseVectorAB, frameCtx.snapInvVectorAB);
        gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber);
        gl.uniform3fv(this._uCoordinateScaler, coordinateScaler);
        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible);
        gl.uniformMatrix4fv(this._uSceneWorldModelMatrix, false, rotationMatrix);
        gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix);
        gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix);
        {
            const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }
        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes;
            const renderFlags = model.renderFlags;
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$i, model.matrix);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }
        if (state.numIndices8Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                8 // 8 bits indices
            );
            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices8Bits);
        }
        if (state.numIndices16Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                16 // 16 bits indices
            );
            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices16Bits);
        }
        if (state.numIndices32Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                32 // 32 bits indices
            );
            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices32Bits);
        }
        frameCtx.drawElements++;
    }

    _allocate() {
        const scene = this._scene;
        const gl = scene.canvas.gl;
        this._program = new Program(gl, this._buildShader());
        if (this._program.errors) {
            this.errors = this._program.errors;
            return;
        }
        const program = this._program;
        this._uRenderPass = program.getLocation("renderPass");
        this._uPickInvisible = program.getLocation("pickInvisible");
        this._uSceneWorldModelMatrix = program.getLocation("sceneModelMatrix");
        this._uViewMatrix = program.getLocation("viewMatrix");
        this._uProjMatrix = program.getLocation("projMatrix");
        this._uSectionPlanes = [];
        for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            this._uSectionPlanes.push({
                active: program.getLocation("sectionPlaneActive" + i),
                pos: program.getLocation("sectionPlanePos" + i),
                dir: program.getLocation("sectionPlaneDir" + i)
            });
        }
        {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
        this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix";
        this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags";
        this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates";
        this._uTexturePerPolygonIdIndices = "uTexturePerPolygonIdIndices";
        this._uTexturePerPolygonIdPortionIds = "uTexturePerPolygonIdPortionIds";
        this._uTexturePerObjectMatrix= "uTexturePerObjectMatrix";
        this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc");
        this._uVectorA = program.getLocation("uVectorAB");
        this._uInverseVectorAB = program.getLocation("uInverseVectorAB");
        this._uLayerNumber = program.getLocation("uLayerNumber");
        this._uCoordinateScaler = program.getLocation("uCoordinateScaler");
    }

    _bindProgram() {
        this._program.bind();
    }

    _buildShader() {
        return {
            vertex: this._buildVertexShader(),
            fragment: this._buildFragmentShader()
        };
    }

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// DTXTrianglesSnapInitRenderer vertex shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");

        src.push("uniform int renderPass;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        src.push("uniform mat4 sceneModelMatrix;");
        src.push("uniform mat4 viewMatrix;");
        src.push("uniform mat4 projMatrix;");

        src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;");
        src.push("uniform highp sampler2D uTexturePerObjectMatrix;");
        src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;");
        src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;");
        src.push("uniform highp usampler2D uTexturePerPolygonIdIndices;");
        src.push("uniform mediump usampler2D uTexturePerPolygonIdPortionIds;");
        src.push("uniform vec3 uCameraEyeRtc;");
        src.push("uniform vec2 uVectorAB;");
        src.push("uniform vec2 uInverseVectorAB;");

        src.push("vec3 positions[3];");

        {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("out float isPerspective;");
        }

        src.push("bool isPerspectiveMatrix(mat4 m) {");
        src.push("    return (m[2][3] == - 1.0);");
        src.push("}");

        src.push("vec2 remapClipPos(vec2 clipPos) {");
        src.push("    float x = (clipPos.x - uVectorAB.x) * uInverseVectorAB.x;");
        src.push("    float y = (clipPos.y - uVectorAB.y) * uInverseVectorAB.y;");
        src.push("    return vec2(x, y);");
        src.push("}");

        src.push("flat out vec4 vPickColor;");
        src.push("out vec4 vWorldPosition;");
        if (clipping) {
            src.push("flat out uint vFlags2;");
        }
        src.push("out highp vec3 relativeToOriginPosition;");
        src.push("void main(void) {");

        // constants
        src.push("int polygonIndex = gl_VertexID / 3;");

        // get packed object-id
        src.push("int h_packed_object_id_index = (polygonIndex >> 3) & 4095;");
        src.push("int v_packed_object_id_index = (polygonIndex >> 3) >> 12;");

        src.push("int objectIndex = int(texelFetch(uTexturePerPolygonIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);");
        src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);");

        // get flags & flags2
        src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);");
        src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);");

        // flags.w = NOT_RENDERED | PICK
        // renderPass = PICK

        src.push(`if (int(flags.w) != renderPass) {`);
        src.push("   gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("   return;"); // Cull vertex
        src.push("}");

        src.push("{");

        // get color
        src.push("uvec4 color = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+0, objectIndexCoords.y), 0);");

        src.push(`if (color.a == 0u) {`);
        src.push("   gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("   return;");
        src.push("};");

        // get vertex base
        src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));");

        src.push("ivec4 packedIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+5, objectIndexCoords.y), 0));");

        src.push("int indexBaseOffset = (packedIndexBaseOffset.r << 24) + (packedIndexBaseOffset.g << 16) + (packedIndexBaseOffset.b << 8) + packedIndexBaseOffset.a;");

        src.push("int h_index = (polygonIndex - indexBaseOffset) & 4095;");
        src.push("int v_index = (polygonIndex - indexBaseOffset) >> 12;");

        src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdIndices, ivec2(h_index, v_index), 0));");
        src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;");

        src.push("ivec3 indexPositionH = uniqueVertexIndexes & 4095;");
        src.push("ivec3 indexPositionV = uniqueVertexIndexes >> 12;");

        src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");

        src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");
        src.push("uint solid = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+7, objectIndexCoords.y), 0).r;");

        // get position
        src.push("positions[0] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.r, indexPositionV.r), 0));");
        src.push("positions[1] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.g, indexPositionV.g), 0));");
        src.push("positions[2] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.b, indexPositionV.b), 0));");

        // get normal
        src.push("vec3 normal = normalize(cross(positions[2] - positions[0], positions[1] - positions[0]));");
        src.push("vec3 position;");
        src.push("position = positions[gl_VertexID % 3];");
        src.push("vec3 viewNormal = -normalize((transpose(inverse(viewMatrix*objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);");

        // when the geometry is not solid, if needed, flip the triangle winding
        src.push("if (solid != 1u) {");
        src.push("  if (isPerspectiveMatrix(projMatrix)) {");
        src.push("      vec3 uCameraEyeRtcInQuantizedSpace = (inverse(sceneModelMatrix * objectDecodeAndInstanceMatrix) * vec4(uCameraEyeRtc, 1)).xyz;");
        src.push("      if (dot(position.xyz - uCameraEyeRtcInQuantizedSpace, normal) < 0.0) {");
        src.push("          position = positions[2 - (gl_VertexID % 3)];");
        src.push("          viewNormal = -viewNormal;");
        src.push("      }");
        src.push("  } else {");
        src.push("      if (viewNormal.z < 0.0) {");
        src.push("          position = positions[2 - (gl_VertexID % 3)];");
        src.push("          viewNormal = -viewNormal;");
        src.push("      }");
        src.push("  }");
        src.push("}");

        src.push("vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); ");
        src.push("relativeToOriginPosition = worldPosition.xyz;");
        src.push("vec4 viewPosition  = viewMatrix * worldPosition; ");

        src.push("vWorldPosition = worldPosition;");
        if (clipping) {
            src.push("vFlags2 = flags2.r;");
        }
        src.push("vPickColor = vec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+1, objectIndexCoords.y), 0));");

        // TODO: Normalized color? See here:
        //src.push("vPickColor = vec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+1, objectIndexCoords.y), 0)) /255.0;");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        src.push("float tmp = clipPos.w;");
        src.push("clipPos.xyzw /= tmp;");
        src.push("clipPos.xy = remapClipPos(clipPos.xy);");
        src.push("clipPos.xyzw *= tmp;");
        {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// DTXTrianglesSnapInitRenderer fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("uniform int uLayerNumber;");
        src.push("uniform vec3 uCoordinateScaler;");
        src.push("in vec4 vWorldPosition;");
        src.push("flat in vec4 vPickColor;");
        if (clipping) {
            src.push("flat in uint vFlags2;");
            for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in highp vec3 relativeToOriginPosition;");
        src.push("layout(location = 0) out highp ivec4 outCoords;");
        src.push("layout(location = 1) out highp ivec4 outNormal;");
        src.push("layout(location = 2) out lowp uvec4 outPickColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = vFlags2 > 0u;");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (var i = 0; i < scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        {
            src.push("    float dx = dFdx(vFragDepth);");
            src.push("    float dy = dFdy(vFragDepth);");
            src.push("    float diff = sqrt(dx*dx+dy*dy);");
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth + diff ) * logDepthBufFC * 0.5;");
        }
        src.push("outCoords = ivec4(relativeToOriginPosition.xyz * uCoordinateScaler.xyz, - uLayerNumber);");

        src.push("vec3 xTangent = dFdx( vWorldPosition.xyz );");
        src.push("vec3 yTangent = dFdy( vWorldPosition.xyz );");
        src.push("vec3 worldNormal = normalize( cross( xTangent, yTangent ) );");
        src.push(`outNormal = ivec4(worldNormal * float(${math.MAX_INT}), 1.0);`);
        src.push("outPickColor = uvec4(vPickColor);");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

const tempVec3a$h = math.vec3();
const tempVec3b$d = math.vec3();
const tempVec3c$b = math.vec3();
math.vec3();
const tempMat4a$4 = math.mat4();

/**
 * @private
 */
class DTXTrianglesOcclusionRenderer {

    constructor(scene) {
        this._scene = scene;
        this._hash = this._getHash();
        this._allocate();
    }

    getValid() {
        return this._hash === this._getHash();
    };

    _getHash() {
        return this._scene._sectionPlanesState.getHash();
    }

    drawLayer(frameCtx, dataTextureLayer, renderPass) {

        const model = dataTextureLayer.model;
        const scene = model.scene;
        const camera = scene.camera;
        const gl = scene.canvas.gl;
        const state = dataTextureLayer._state;
        const textureState = state.textureState;
        const origin = dataTextureLayer._state.origin;
        const {position, rotationMatrix} = model;
        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;

        if (!this._program) {
            this._allocate(dataTextureLayer);
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram();
        }

        textureState.bindCommonTextures(
            this._program,
            this.uTexturePerObjectPositionsDecodeMatrix,
            this._uTexturePerVertexIdCoordinates,
            this.uTexturePerObjectColorsAndFlags,
            this._uTexturePerObjectMatrix
        );

        let rtcViewMatrix;
        let rtcCameraEye;

        if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) {
            const rtcOrigin = tempVec3a$h;
            if (origin) {
                const rotatedOrigin = tempVec3b$d;
                math.transformPoint3(rotationMatrix, origin, rotatedOrigin);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$4);
            rtcCameraEye = tempVec3c$b;
            rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0];
            rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1];
            rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            rtcCameraEye = camera.eye;
        }

        gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye);
        gl.uniform1i(this._uRenderPass, renderPass);
        gl.uniformMatrix4fv(this._uWorldMatrix, false, rotationMatrix);
        gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix);

        gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix);

        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes;
            const renderFlags = model.renderFlags;
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$h, model.matrix);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }

        if (state.numIndices8Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                8 // 8 bits indices
            );
            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices8Bits);
        }
        if (state.numIndices16Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                16 // 16 bits indices
            );
            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices16Bits);
        }
        if (state.numIndices32Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                32 // 32 bits indices
            );
            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices32Bits);
        }
        frameCtx.drawElements++;
    }

    _allocate() {
        const scene = this._scene;
        const gl = scene.canvas.gl;
        this._program = new Program(gl, this._buildShader());
        if (this._program.errors) {
            this.errors = this._program.errors;
            return;
        }
        const program = this._program;
        this._uRenderPass = program.getLocation("renderPass");
        this._uPickInvisible = program.getLocation("pickInvisible");
        this._uWorldMatrix = program.getLocation("sceneModelMatrix");
        this._uViewMatrix = program.getLocation("viewMatrix");
        this._uProjMatrix = program.getLocation("projMatrix");
        this._uSectionPlanes = [];
        for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            this._uSectionPlanes.push({
                active: program.getLocation("sectionPlaneActive" + i),
                pos: program.getLocation("sectionPlanePos" + i),
                dir: program.getLocation("sectionPlaneDir" + i)
            });
        }
        this._uPickZNear = program.getLocation("pickZNear");
        this._uPickZFar = program.getLocation("pickZFar");
        if (scene.logarithmicDepthBufferEnabled) {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
        this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix";
        this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags";
        this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates";
        this._uTexturePerPolygonIdNormals = "uTexturePerPolygonIdNormals";
        this._uTexturePerPolygonIdIndices = "uTexturePerPolygonIdIndices";
        this._uTexturePerPolygonIdPortionIds = "uTexturePerPolygonIdPortionIds";
        this._uTexturePerObjectMatrix= "uTexturePerObjectMatrix";
        this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc");
    }

    _bindProgram() {

        const scene = this._scene;
        scene.canvas.gl;
        scene.camera.project;

        this._program.bind();
    }

    _buildShader() {
        return {
            vertex: this._buildVertexShader(),
            fragment: this._buildFragmentShader()
        };
    }

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// TrianglesDataTextureOcclusionRenderer vertex shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");

        src.push("uniform int renderPass;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        src.push("uniform mat4 sceneModelMatrix;");
        src.push("uniform mat4 viewMatrix;");
        src.push("uniform mat4 projMatrix;");

        src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;");
        src.push("uniform highp sampler2D uTexturePerObjectMatrix;");
        src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;");
        src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;");
        src.push("uniform highp usampler2D uTexturePerPolygonIdIndices;");
        src.push("uniform mediump usampler2D uTexturePerPolygonIdPortionIds;");
        src.push("uniform vec3 uCameraEyeRtc;");

        src.push("vec3 positions[3];");
        src.push("bool isPerspectiveMatrix(mat4 m) {");
        src.push("    return (m[2][3] == - 1.0);");
        src.push("}");
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("flat out uint vFlags2;");
        }
        src.push("void main(void) {");

        // constants
        src.push("int polygonIndex = gl_VertexID / 3;");

        // get packed object-id
        src.push("int h_packed_object_id_index = (polygonIndex >> 3) & 4095;");
        src.push("int v_packed_object_id_index = (polygonIndex >> 3) >> 12;");

        src.push("int objectIndex = int(texelFetch(uTexturePerPolygonIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);");
        src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);");

        // get flags & flags2
        src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);");
        src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);");

        // flags.x = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE
        // Only opaque objects can be occluders

        src.push(`if (int(flags.x) != renderPass) {`);
        src.push("      gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex

        src.push("  } else {");

        // get vertex base
        src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));");

        src.push("ivec4 packedIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+5, objectIndexCoords.y), 0));");

        src.push("int indexBaseOffset = (packedIndexBaseOffset.r << 24) + (packedIndexBaseOffset.g << 16) + (packedIndexBaseOffset.b << 8) + packedIndexBaseOffset.a;");

        src.push("int h_index = (polygonIndex - indexBaseOffset) & 4095;");
        src.push("int v_index = (polygonIndex - indexBaseOffset) >> 12;");

        src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdIndices, ivec2(h_index, v_index), 0));");
        src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;");

        src.push("ivec3 indexPositionH = uniqueVertexIndexes & 4095;");
        src.push("ivec3 indexPositionV = uniqueVertexIndexes >> 12;");

        src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");

        src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");

        src.push("uint solid = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+7, objectIndexCoords.y), 0).r;");

        // get position
        src.push("positions[0] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.r, indexPositionV.r), 0));");
        src.push("positions[1] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.g, indexPositionV.g), 0));");
        src.push("positions[2] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.b, indexPositionV.b), 0));");

        // get color
        src.push("uvec4 color = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+0, objectIndexCoords.y), 0);");

        src.push(`if (color.a == 0u) {`);
        src.push("   gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("   return;");
        src.push("};");

        src.push("vec3 normal = normalize(cross(positions[2] - positions[0], positions[1] - positions[0]));");
        src.push("vec3 position;");
        src.push("position = positions[gl_VertexID % 3];");

        // when the geometry is not solid, if needed, flip the triangle winding
        src.push("if (solid != 1u) {");
        src.push("  if (isPerspectiveMatrix(projMatrix)) {");
        src.push("      vec3 uCameraEyeRtcInQuantizedSpace = (inverse(sceneModelMatrix * objectDecodeAndInstanceMatrix) * vec4(uCameraEyeRtc, 1)).xyz;");
        src.push("      if (dot(position.xyz - uCameraEyeRtcInQuantizedSpace, normal) < 0.0) {");
        src.push("          position = positions[2 - (gl_VertexID % 3)];");
        src.push("      }");
        src.push("  } else {");
        src.push("      vec3 viewNormal = -normalize((transpose(inverse(viewMatrix*objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);");
        src.push("      if (viewNormal.z < 0.0) {");
        src.push("          position = positions[2 - (gl_VertexID % 3)];");
        src.push("      }");
        src.push("  }");
        src.push("}");

         src.push("vec4 worldPosition = sceneModelMatrix *  (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); ");

        src.push("vec4 viewPosition = viewMatrix * worldPosition; ");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags2 = flags2.r;");
        }
        src.push("gl_Position = clipPos;");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// TrianglesDataTextureColorRenderer fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("flat in uint vFlags2;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (float(vFlags2) > 0.0);");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        src.push("   outColor = vec4(0.0, 0.0, 1.0, 1.0); "); // Occluders are blue
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

const tempVec3a$g = math.vec3();
const tempVec3b$c = math.vec3();
const tempVec3c$a = math.vec3();
math.vec3();
const tempMat4a$3 = math.mat4();

/**
 * @private
 */
class DTXTrianglesDepthRenderer {

    constructor(scene) {
        this._scene = scene;
        this._allocate();
        this._hash = this._getHash();
    }

    getValid() {
        return this._hash === this._getHash();
    };

    _getHash() {
        return this._scene._sectionPlanesState.getHash();
    }

    drawLayer(frameCtx, dataTextureLayer, renderPass) {

        const scene = this._scene;
        const camera = scene.camera;
        const model = dataTextureLayer.model;
        const gl = scene.canvas.gl;
        const state = dataTextureLayer._state;
        const textureState = state.textureState;
        const origin = dataTextureLayer._state.origin;
        const {position, rotationMatrix} = model;

        if (!this._program) {
            this._allocate();
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram(frameCtx, state);
        }

        textureState.bindCommonTextures(
            this._program,
            this.uTexturePerObjectPositionsDecodeMatrix,
            this._uTexturePerVertexIdCoordinates,
            this.uTexturePerObjectColorsAndFlags,
            this._uTexturePerObjectMatrix
        );

        let rtcViewMatrix;
        let rtcCameraEye;

        const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0);
        const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0);
        if (gotOrigin || gotPosition) {
            const rtcOrigin = tempVec3a$g;
            if (gotOrigin) {
                const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3b$c);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(camera.viewMatrix, rtcOrigin, tempMat4a$3);
            rtcCameraEye = tempVec3c$a;
            rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0];
            rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1];
            rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2];
        } else {
            rtcViewMatrix = camera.viewMatrix;
            rtcCameraEye = camera.eye;
        }

        gl.uniformMatrix4fv(this._uSceneModelMatrix, false, rotationMatrix);
        gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix);
        gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix);
        gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye);
        gl.uniform1i(this._uRenderPass, renderPass);

        if (scene.logarithmicDepthBufferEnabled) {
            const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }

        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes;
            const renderFlags = model.renderFlags;
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$g, model.matrix);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }

        if (state.numIndices8Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                8 // 8 bits indices
            );

            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices8Bits);
        }

        if (state.numIndices16Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                16 // 16 bits indices
            );

            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices16Bits);
        }

        if (state.numIndices32Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                32 // 32 bits indices
            );

            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices32Bits);
        }

        frameCtx.drawElements++;
    }

    _allocate() {

        const scene = this._scene;
        const gl = scene.canvas.gl;

        this._program = new Program(gl, this._buildShader());

        if (this._program.errors) {
            this.errors = this._program.errors;
            return;
        }

        const program = this._program;

        this._uRenderPass = program.getLocation("renderPass");
        this._uPositionsDecodeMatrix = program.getLocation("objectDecodeAndInstanceMatrix");
        this._uSceneModelMatrix = program.getLocation("sceneModelMatrix");
        this._uViewMatrix = program.getLocation("viewMatrix");
        this._uProjMatrix = program.getLocation("projMatrix");
        this._uSectionPlanes = [];

        for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            this._uSectionPlanes.push({
                active: program.getLocation("sectionPlaneActive" + i),
                pos: program.getLocation("sectionPlanePos" + i),
                dir: program.getLocation("sectionPlaneDir" + i)
            });
        }

        if (scene.logarithmicDepthBufferEnabled) {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }

        this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix";
        this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags";
        this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates";
        this._uTexturePerPolygonIdNormals = "uTexturePerPolygonIdNormals";
        this._uTexturePerPolygonIdIndices = "uTexturePerPolygonIdIndices";
        this._uTexturePerPolygonIdPortionIds = "uTexturePerPolygonIdPortionIds";
        this._uTexturePerObjectMatrix= "uTexturePerObjectMatrix";
        this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc");
    }

    _bindProgram(frameCtx) {

        const scene = this._scene;
        const gl = scene.canvas.gl;
        const program = this._program;
        const project = scene.camera.project;

        program.bind();

        gl.uniformMatrix4fv(this._uProjMatrix, false, project.matrix);

        if (scene.logarithmicDepthBufferEnabled) {
            const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }
    }

    _buildShader() {
        return {
            vertex: this._buildVertexShader(),
            fragment: this._buildFragmentShader()
        };
    }

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// Triangles dataTexture draw vertex shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");

        src.push("uniform int renderPass;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        src.push("uniform mat4 sceneModelMatrix;");
        src.push("uniform mat4 viewMatrix;");
        src.push("uniform mat4 projMatrix;");

        src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;");
        src.push("uniform highp sampler2D uTexturePerObjectMatrix;");
        src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;");
        src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;");
        src.push("uniform highp usampler2D uTexturePerPolygonIdIndices;");
        src.push("uniform mediump usampler2D uTexturePerPolygonIdPortionIds;");
        src.push("uniform vec3 uCameraEyeRtc;");

        src.push("vec3 positions[3];");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("out float isPerspective;");
        }

        src.push("bool isPerspectiveMatrix(mat4 m) {");
        src.push("    return (m[2][3] == - 1.0);");
        src.push("}");

        src.push("out highp vec2 vHighPrecisionZW;");

        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("flat out uint vFlags2;");
        }

        src.push("void main(void) {");

        // constants
        src.push("int polygonIndex = gl_VertexID / 3;");

        // get packed object-id
        src.push("int h_packed_object_id_index = (polygonIndex >> 3) & 4095;");
        src.push("int v_packed_object_id_index = (polygonIndex >> 3) >> 12;");

        src.push("int objectIndex = int(texelFetch(uTexturePerPolygonIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);");
        src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);");

        // get flags & flags2
        src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);");
        src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);");

        // flags.x = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE

        src.push(`if (int(flags.x) != renderPass) {`);
        src.push("   gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("   return;"); // Cull vertex
        src.push("} else {");

        // get vertex base
        src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));");

        src.push("ivec4 packedIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+5, objectIndexCoords.y), 0));");

        src.push("int indexBaseOffset = (packedIndexBaseOffset.r << 24) + (packedIndexBaseOffset.g << 16) + (packedIndexBaseOffset.b << 8) + packedIndexBaseOffset.a;");

        src.push("int h_index = (polygonIndex - indexBaseOffset) & 4095;");
        src.push("int v_index = (polygonIndex - indexBaseOffset) >> 12;");

        src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdIndices, ivec2(h_index, v_index), 0));");
        src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;");

        src.push("ivec3 indexPositionH = uniqueVertexIndexes & 4095;");
        src.push("ivec3 indexPositionV = uniqueVertexIndexes >> 12;");

        src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");
        src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");

        src.push("uint solid = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+7, objectIndexCoords.y), 0).r;");

        // get position
        src.push("positions[0] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.r, indexPositionV.r), 0));");
        src.push("positions[1] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.g, indexPositionV.g), 0));");
        src.push("positions[2] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.b, indexPositionV.b), 0));");

        // get color
        src.push("uvec4 color = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+0, objectIndexCoords.y), 0);");

        src.push(`if (color.a == 0u) {`);
        src.push("   gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("   return;");
        src.push("};");

        // get normal
        src.push("vec3 normal = normalize(cross(positions[2] - positions[0], positions[1] - positions[0]));");

        src.push("vec3 position;");
        src.push("position = positions[gl_VertexID % 3];");

        src.push("vec3 viewNormal = -normalize((transpose(inverse(viewMatrix*objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);");

        // when the geometry is not solid, if needed, flip the triangle winding
        src.push("if (solid != 1u) {");
        src.push("if (isPerspectiveMatrix(projMatrix)) {");
        src.push("vec3 uCameraEyeRtcInQuantizedSpace = (inverse(sceneModelMatrix * objectDecodeAndInstanceMatrix) * vec4(uCameraEyeRtc, 1)).xyz;");
        src.push("if (dot(position.xyz - uCameraEyeRtcInQuantizedSpace, normal) < 0.0) {");
        src.push("position = positions[2 - (gl_VertexID % 3)];");
        src.push("viewNormal = -viewNormal;");
        src.push("}");
        src.push("} else {");
        src.push("if (viewNormal.z < 0.0) {");
        src.push("position = positions[2 - (gl_VertexID % 3)];");
        src.push("viewNormal = -viewNormal;");
        src.push("}");
        src.push("}");
        src.push("}");

         src.push("vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); ");
        src.push("vec4 viewPosition = viewMatrix * worldPosition; ");

        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        if (clipping) {
            src.push("vWorldPosition = worldPosition;");
            src.push("vFlags2 = flags2.r;");
        }
        src.push("gl_Position = clipPos;");
        src.push("vHighPrecisionZW = gl_Position.zw;");
        src.push("}");

        src.push("}");

        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// Triangles dataTexture draw fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");

        src.push("in highp vec2 vHighPrecisionZW;");
        src.push("out vec4 outColor;");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("flat in uint vFlags2;");
            for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("void main(void) {");

        if (clipping) {
            src.push("  bool clippable = vFlags2 > 0u;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > 0.0) { ");
            src.push("      discard;");
            src.push("  }");
            src.push("}");
        }

        if (scene.logarithmicDepthBufferEnabled) {
             src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
            //src.push("    gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("float fragCoordZ = 0.5 * vHighPrecisionZW[0] / vHighPrecisionZW[1] + 0.5;");
        src.push("    outColor = vec4(vec3(1.0 - fragCoordZ), 1.0); ");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

const tempVec3a$f = math.vec3();
const tempVec3b$b = math.vec3();
const tempVec3c$9 = math.vec3();
math.vec3();
const tempMat4a$2 = math.mat4();

/**
 * @private
 */
class DTXTrianglesNormalsRenderer {

    constructor(scene) {
        this._scene = scene;
        this._hash = this._getHash();
        this._allocate();
    }

    getValid() {
        return this._hash === this._getHash();
    };

    _getHash() {
        return this._scene._sectionPlanesState.getHash();
    }

    drawLayer(frameCtx, dataTextureLayer, renderPass) {

        const model = dataTextureLayer.model;
        const scene = model.scene;
        const camera = scene.camera;
        const gl = scene.canvas.gl;
        const state = dataTextureLayer._state;
        const origin = dataTextureLayer._state.origin;
        const {position, rotationMatrix} = model;
        const viewMatrix = camera.viewMatrix;

        if (!this._program) {
            this._allocate(dataTextureLayer);
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram(dataTextureLayer);
        }

        let rtcViewMatrix;
        let rtcCameraEye;

        const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0);
        const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0);
        if (gotOrigin || gotPosition) {
            const rtcOrigin = tempVec3a$f;
            if (gotOrigin) {
                const rotatedOrigin = tempVec3b$b;
                math.transformPoint3(rotationMatrix, origin, rotatedOrigin);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$2);
            rtcCameraEye = tempVec3c$9;
            rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0];
            rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1];
            rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            rtcCameraEye = camera.eye;
        }

        gl.uniform1i(this._uRenderPass, renderPass);

        gl.uniformMatrix4fv(this._uWorldMatrix, false, rotationMatrix);
        gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix);
        gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix);

        gl.uniformMatrix4fv(this._uViewNormalMatrix, false, camera.viewNormalMatrix);
        gl.uniformMatrix4fv(this._uWorldNormalMatrix, false, model.worldNormalMatrix);

        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes;
            const renderFlags = model.renderFlags;
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$f, model.matrix);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }

        gl.uniformMatrix4fv(this._uPositionsDecodeMatrix, false, dataTextureLayer._state.objectDecodeAndInstanceMatrix);

        this._aPosition.bindArrayBuffer(state.positionsBuf);
        this._aOffset.bindArrayBuffer(state.offsetsBuf);
        this._aNormal.bindArrayBuffer(state.normalsBuf);
        this._aColor.bindArrayBuffer(state.colorsBuf);// Needed for masking out transparent entities using alpha channel
        this._aFlags.bindArrayBuffer(state.flagsBuf);
        if (this._aFlags2) {
            this._aFlags2.bindArrayBuffer(state.flags2Buf);
        }
        state.indicesBuf.bind();

        gl.drawElements(gl.TRIANGLES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0);
    }

    _allocate() {
        const scene = this._scene;
        const gl = scene.canvas.gl;
        this._program = new Program(gl, this._buildShader());
        if (this._program.errors) {
            this.errors = this._program.errors;
            return;
        }
        const program = this._program;
        this._uRenderPass = program.getLocation("renderPass");
        this._uPositionsDecodeMatrix = program.getLocation("objectDecodeAndInstanceMatrix");
        this._uWorldMatrix = program.getLocation("worldMatrix");
        this._uWorldNormalMatrix = program.getLocation("worldNormalMatrix");
        this._uViewMatrix = program.getLocation("viewMatrix");
        this._uViewNormalMatrix = program.getLocation("viewNormalMatrix");
        this._uProjMatrix = program.getLocation("projMatrix");
        this._uSectionPlanes = [];
        for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            this._uSectionPlanes.push({
                active: program.getLocation("sectionPlaneActive" + i),
                pos: program.getLocation("sectionPlanePos" + i),
                dir: program.getLocation("sectionPlaneDir" + i)
            });
        }
        this._aPosition = program.getAttribute("position");
        this._aOffset = program.getAttribute("offset");
        this._aNormal = program.getAttribute("normal");
        this._aColor = program.getAttribute("color");
        this._aFlags = program.getAttribute("flags");
        if (this._aFlags2) { // Won't be in shader when not clipping
            this._aFlags2 = program.getAttribute("flags2");
        }
        if ( scene.logarithmicDepthBufferEnabled) {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
    }

    _bindProgram() {
        const scene = this._scene;
        const gl = scene.canvas.gl;
        const project = scene.camera.project;
        this._program.bind();
        gl.uniformMatrix4fv(this._uProjMatrix, false, project.matrix);
        if ( scene.logarithmicDepthBufferEnabled) {
            const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }
    }

    _buildShader() {
        return {
            vertex: this._buildVertexShader(),
            fragment: this._buildFragmentShader()
        };
    }

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("// Batched geometry normals vertex shader");
        if (scene.logarithmicDepthBufferEnabled && WEBGL_INFO.SUPPORTED_EXTENSIONS["EXT_frag_depth"]) {
            src.push("#extension GL_EXT_frag_depth : enable");
        }
        src.push("uniform int renderPass;");
        src.push("attribute vec3 position;");
        if (scene.entityOffsetsEnabled) {
            src.push("attribute vec3 offset;");
        }
        src.push("attribute vec3 normal;");
        src.push("attribute vec4 color;");
        src.push("attribute vec4 flags;");
        src.push("attribute vec4 flags2;");
        src.push("uniform mat4 worldMatrix;");
        src.push("uniform mat4 worldNormalMatrix;");
        src.push("uniform mat4 viewMatrix;");
        src.push("uniform mat4 projMatrix;");
        src.push("uniform mat4 viewNormalMatrix;");
        src.push("uniform mat4 objectDecodeAndInstanceMatrix;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            if (WEBGL_INFO.SUPPORTED_EXTENSIONS["EXT_frag_depth"]) {
                src.push("out float vFragDepth;");
            }
            src.push("bool isPerspectiveMatrix(mat4 m) {");
            src.push("    return (m[2][3] == - 1.0);");
            src.push("}");
            src.push("varying float isPerspective;");
        }
        src.push("vec3 octDecode(vec2 oct) {");
        src.push("    vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));");
        src.push("    if (v.z < 0.0) {");
        src.push("        v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);");
        src.push("    }");
        src.push("    return normalize(v);");
        src.push("}");
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
            src.push("out vec4 vFlags2;");
        }
        src.push("out vec3 vViewNormal;");
        src.push("void main(void) {");

        // flags.x = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT
        // renderPass = COLOR_OPAQUE

        src.push(`if (int(flags.x) != renderPass) {`);
        src.push("      gl_Position = vec4(0.0, 0.0, 0.0, 0.0);");

        src.push("  } else {");
        src.push("      vec4 worldPosition = worldMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); ");
        if (scene.entityOffsetsEnabled) {
            src.push("      worldPosition.xyz = worldPosition.xyz + offset;");
        }
        src.push("      vec4 viewPosition   = viewMatrix * worldPosition; ");
        src.push("      vec4 worldNormal    = worldNormalMatrix * vec4(octDecode(normal.xy), 0.0); ");
        src.push("      vec3 viewNormal     = normalize((viewNormalMatrix * worldNormal).xyz);");
        if (clipping) {
            src.push("      vWorldPosition  = worldPosition;");
            src.push("      vFlags2         = flags2;");
        }
        src.push("      vViewNormal = viewNormal;");
        src.push("vec4 clipPos = projMatrix * viewPosition;");
        if (scene.logarithmicDepthBufferEnabled) {
            if (WEBGL_INFO.SUPPORTED_EXTENSIONS["EXT_frag_depth"]) {
                src.push("vFragDepth = 1.0 + clipPos.w;");
            } else {
                src.push("clipPos.z = log2( max( 1e-6, clipPos.w + 1.0 ) ) * logDepthBufFC - 1.0;");
                src.push("clipPos.z *= clipPos.w;");
            }
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }
        src.push("gl_Position = clipPos;");
        src.push("  }");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const clipping = (scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0);
        const src = [];
        src.push("#version 300 es");
        src.push("// Batched geometry normals fragment shader");

        if (scene.logarithmicDepthBufferEnabled && WEBGL_INFO.SUPPORTED_EXTENSIONS["EXT_frag_depth"]) {
            src.push("#extension GL_EXT_frag_depth : enable");
        }

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");

        if (scene.logarithmicDepthBufferEnabled && WEBGL_INFO.SUPPORTED_EXTENSIONS["EXT_frag_depth"]) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }

        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            src.push("in vec4 vFlags2;");
            for (let i = 0; i < scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("in vec3 vViewNormal;");
        src.push("vec3 packNormalToRGB( const in vec3 normal ) {");
        src.push("    return normalize( normal ) * 0.5 + 0.5;");
        src.push("}");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = (float(vFlags2.x) > 0.0);");
            src.push("  if (clippable) {");
            src.push("      float dist = 0.0;");
            for (var i = 0; i < scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i++) {
                src.push("      if (sectionPlaneActive" + i + ") {");
                src.push("          dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("      }");
            }
            src.push("      if (dist > 0.0) { discard; }");
            src.push("  }");
        }
        if (scene.logarithmicDepthBufferEnabled && WEBGL_INFO.SUPPORTED_EXTENSIONS["EXT_frag_depth"]) {
            src.push("    gl_FragDepthEXT = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("    gl_FragColor = vec4(packNormalToRGB(vViewNormal), 1.0); ");
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

const tempVec3a$e = math.vec3();
const tempVec3b$a = math.vec3();
const tempVec3c$8 = math.vec3();
math.vec3();

math.vec4();

const tempMat4a$1 = math.mat4();

/**
 * @private
 */
class DTXTrianglesPickNormalsFlatRenderer {

    constructor(scene, withSAO) {
        this._scene = scene;
        this._withSAO = withSAO;
        this._hash = this._getHash();
        this._allocate();
    }

    getValid() {
        return this._hash === this._getHash();
    };

    _getHash() {
        const scene = this._scene;
        return [scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";");
    }

    drawLayer(frameCtx, dataTextureLayer, renderPass) {

        const scene = this._scene;
        const camera = scene.camera;
        const model = dataTextureLayer.model;
        const gl = scene.canvas.gl;
        const state = dataTextureLayer._state;
        const textureState = state.textureState;
        const origin = dataTextureLayer._state.origin;
        const {position, rotationMatrix} = model;

        if (!this._program) {
            this._allocate();
            if (this.errors) {
                return;
            }
        }

        if (frameCtx.lastProgramId !== this._program.id) {
            frameCtx.lastProgramId = this._program.id;
            this._bindProgram(frameCtx, state);
        }

        const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix;
        const projMatrix = frameCtx.pickProjMatrix || camera.projMatrix;
        const eye = frameCtx.pickOrigin || camera.eye;
        const far = frameCtx.pickProjMatrix ? frameCtx.pickZFar : camera.project.far;

        textureState.bindCommonTextures(
            this._program,
            this.uTexturePerObjectPositionsDecodeMatrix,
            this._uTexturePerVertexIdCoordinates,
            this.uTexturePerObjectColorsAndFlags,
            this._uTexturePerObjectMatrix
        );

        let rtcViewMatrix;
        let rtcCameraEye;

        const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0);
        const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0);
        if (gotOrigin || gotPosition) {
            const rtcOrigin = tempVec3a$e;
            if (gotOrigin) {
                const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3b$a);
                rtcOrigin[0] = rotatedOrigin[0];
                rtcOrigin[1] = rotatedOrigin[1];
                rtcOrigin[2] = rotatedOrigin[2];
            } else {
                rtcOrigin[0] = 0;
                rtcOrigin[1] = 0;
                rtcOrigin[2] = 0;
            }
            rtcOrigin[0] += position[0];
            rtcOrigin[1] += position[1];
            rtcOrigin[2] += position[2];
            rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$1);
            rtcCameraEye = tempVec3c$8;
            rtcCameraEye[0] = eye[0] - rtcOrigin[0];
            rtcCameraEye[1] = eye[1] - rtcOrigin[1];
            rtcCameraEye[2] = eye[2] - rtcOrigin[2];
        } else {
            rtcViewMatrix = viewMatrix;
            rtcCameraEye = eye;
        }
        gl.uniform2fv(this._uPickClipPos, frameCtx.pickClipPos);
        gl.uniform2f(this._uDrawingBufferSize, gl.drawingBufferWidth, gl.drawingBufferHeight);
        gl.uniformMatrix4fv(this._uSceneModelMatrix, false, rotationMatrix);
        gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix);
        gl.uniformMatrix4fv(this._uProjMatrix, false, projMatrix);
        gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye);
        gl.uniform1i(this._uRenderPass, renderPass);
        if (scene.logarithmicDepthBufferEnabled) {
            const logDepthBufFC = 2.0 / (Math.log(far + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }
        const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes();
        const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length;
        if (numAllocatedSectionPlanes > 0) {
            const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
            const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes;
            const renderFlags = model.renderFlags;
            for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) {
                const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                if (sectionPlaneUniforms) {
                    if (sectionPlaneIndex < numSectionPlanes) {
                        const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            if (origin) {
                                const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$e, model.matrix);
                                gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos);
                            } else {
                                gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
                            }
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    } else {
                        gl.uniform1i(sectionPlaneUniforms.active, 0);
                    }
                }
            }
        }

        if (state.numIndices8Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                8 // 8 bits indices
            );
            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices8Bits);
        }

        if (state.numIndices16Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                16 // 16 bits indices
            );
            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices16Bits);
        }

        if (state.numIndices32Bits > 0) {
            textureState.bindTriangleIndicesTextures(
                this._program,
                this._uTexturePerPolygonIdPortionIds,
                this._uTexturePerPolygonIdIndices,
                32 // 32 bits indices
            );
            gl.drawArrays(gl.TRIANGLES, 0, state.numIndices32Bits);
        }

        frameCtx.drawElements++;
    }

    _allocate() {
        const scene = this._scene;
        const gl = scene.canvas.gl;
        this._program = new Program(gl, this._buildShader());
        if (this._program.errors) {
            this.errors = this._program.errors;
            return;
        }
        const program = this._program;
        this._uRenderPass = program.getLocation("renderPass");
        this._uPickInvisible = program.getLocation("pickInvisible");
        this._uPickClipPos = program.getLocation("pickClipPos");
        this._uDrawingBufferSize = program.getLocation("drawingBufferSize");
        this._uSceneModelMatrix = program.getLocation("sceneModelMatrix");
        this._uViewMatrix = program.getLocation("viewMatrix");
        this._uProjMatrix = program.getLocation("projMatrix");
        this._uSectionPlanes = [];
        for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
            this._uSectionPlanes.push({
                active: program.getLocation("sectionPlaneActive" + i),
                pos: program.getLocation("sectionPlanePos" + i),
                dir: program.getLocation("sectionPlaneDir" + i)
            });
        }
        if (scene.logarithmicDepthBufferEnabled) {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
        this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix";
        this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags";
        this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates";
        this._uTexturePerPolygonIdNormals = "uTexturePerPolygonIdNormals";
        this._uTexturePerPolygonIdIndices = "uTexturePerPolygonIdIndices";
        this._uTexturePerPolygonIdPortionIds = "uTexturePerPolygonIdPortionIds";
        this._uTexturePerObjectMatrix = "uTexturePerObjectMatrix";
        this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc");
    }

    _bindProgram(frameCtx) {
        const scene = this._scene;
        const gl = scene.canvas.gl;
        const program = this._program;
        const project = scene.camera.project;
        program.bind();
        if (scene.logarithmicDepthBufferEnabled) {
            const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }
    }

    _buildShader() {
        return {
            vertex: this._buildVertexShader(),
            fragment: this._buildFragmentShader()
        };
    }

    _buildVertexShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// trianglesDatatextureNormalsRenderer vertex shader");

        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("precision highp usampler2D;");
        src.push("precision highp isampler2D;");
        src.push("precision highp sampler2D;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("precision mediump usampler2D;");
        src.push("precision mediump isampler2D;");
        src.push("precision mediump sampler2D;");
        src.push("#endif");

        src.push("uniform int renderPass;");

        if (scene.entityOffsetsEnabled) {
            src.push("in vec3 offset;");
        }

        src.push("uniform mat4 sceneModelMatrix;");
        src.push("uniform mat4 viewMatrix;");
        src.push("uniform mat4 projMatrix;");

        src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;");
        src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;");
        src.push("uniform highp sampler2D uTexturePerObjectMatrix;");
        src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;");
        src.push("uniform highp usampler2D uTexturePerPolygonIdIndices;");
        src.push("uniform mediump usampler2D uTexturePerPolygonIdPortionIds;");
        src.push("uniform vec3 uCameraEyeRtc;");

        src.push("vec3 positions[3];");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
            src.push("out float isPerspective;");
        }

        src.push("uniform vec2 pickClipPos;");
        src.push("uniform vec2 drawingBufferSize;");

        src.push("vec4 remapClipPos(vec4 clipPos) {");
        src.push("    clipPos.xy /= clipPos.w;");
        src.push(`    clipPos.xy = (clipPos.xy - pickClipPos) * drawingBufferSize;`);
        src.push("    clipPos.xy *= clipPos.w;");
        src.push("    return clipPos;");
        src.push("}");

        src.push("bool isPerspectiveMatrix(mat4 m) {");
        src.push("    return (m[2][3] == - 1.0);");
        src.push("}");

        src.push("out vec4 vWorldPosition;");

        if (clipping) {
            src.push("flat out uint vFlags2;");
        }

        src.push("void main(void) {");

        // constants
        src.push("int polygonIndex = gl_VertexID / 3;");

        // get packed object-id
        src.push("int h_packed_object_id_index = (polygonIndex >> 3) & 4095;");
        src.push("int v_packed_object_id_index = (polygonIndex >> 3) >> 12;");

        src.push("int objectIndex = int(texelFetch(uTexturePerPolygonIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);");
        src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);");

        // get flags & flags2
        src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);");
        src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);");  // pickFlag = NOT_RENDERED | PICK

        // renderPass = PICK
        src.push(`if (int(flags.w) != renderPass) {`);
        src.push("   gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("   return;"); // Cull vertex
        src.push("} else {");

        // get vertex base
        src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));");
        src.push("ivec4 packedIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+5, objectIndexCoords.y), 0));");
        src.push("int indexBaseOffset = (packedIndexBaseOffset.r << 24) + (packedIndexBaseOffset.g << 16) + (packedIndexBaseOffset.b << 8) + packedIndexBaseOffset.a;");
        src.push("int h_index = (polygonIndex - indexBaseOffset) & 4095;");
        src.push("int v_index = (polygonIndex - indexBaseOffset) >> 12;");
        src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdIndices, ivec2(h_index, v_index), 0));");
        src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;");
        src.push("ivec3 indexPositionH = uniqueVertexIndexes & 4095;");
        src.push("ivec3 indexPositionV = uniqueVertexIndexes >> 12;");

        src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");

        src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));");

        src.push("uint solid = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+7, objectIndexCoords.y), 0).r;");
        // get position
        src.push("positions[0] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.r, indexPositionV.r), 0));");
        src.push("positions[1] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.g, indexPositionV.g), 0));");
        src.push("positions[2] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.b, indexPositionV.b), 0));");

        // get color
        src.push("uvec4 color = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+0, objectIndexCoords.y), 0);");

        src.push(`if (color.a == 0u) {`);
        src.push("   gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex
        src.push("   return;");
        src.push("};");

        // get normal

        src.push("vec3 normal = normalize(cross(positions[2] - positions[0], positions[1] - positions[0]));");

        src.push("vec3 position;");
        src.push("position = positions[gl_VertexID % 3];");

        src.push("vec3 viewNormal = -normalize((transpose(inverse(viewMatrix*objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);");

        // when the geometry is not solid, if needed, flip the triangle winding
        src.push("if (solid != 1u) {");
        src.push("if (isPerspectiveMatrix(projMatrix)) {");
        src.push("vec3 uCameraEyeRtcInQuantizedSpace = (inverse(sceneModelMatrix * objectDecodeAndInstanceMatrix) * vec4(uCameraEyeRtc, 1)).xyz;");
        // src.push("vColor = vec4(vec3(1, -1, 0)*dot(normalize(position.xyz - uCameraEyeRtcInQuantizedSpace), normal), 1);")
        src.push("if (dot(position.xyz - uCameraEyeRtcInQuantizedSpace, normal) < 0.0) {");
        src.push("position = positions[2 - (gl_VertexID % 3)];");
        src.push("viewNormal = -viewNormal;");
        src.push("}");
        src.push("} else {");
        src.push("vec3 viewNormal = -normalize((transpose(inverse(viewMatrix*objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);");
        src.push("if (viewNormal.z < 0.0) {");
        src.push("position = positions[2 - (gl_VertexID % 3)];");
        src.push("}");
        src.push("}");
        src.push("}");

        src.push("vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); ");

        src.push("vec4 viewPosition = viewMatrix * worldPosition; ");

        src.push("vec4 clipPos = projMatrix * viewPosition;");

        if (scene.logarithmicDepthBufferEnabled) {
            src.push("vFragDepth = 1.0 + clipPos.w;");
            src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));");
        }

        src.push("vWorldPosition = worldPosition;");

        if (clipping) {
            src.push("vFlags2 = flags2.r;");
        }
        src.push("gl_Position = remapClipPos(clipPos);");
        src.push("}");
        src.push("}");
        return src;
    }

    _buildFragmentShader() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0;
        const src = [];
        src.push('#version 300 es');
        src.push("// TrianglesDataTexturePickNormalsRenderer fragment shader");
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("in float isPerspective;");
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        src.push("in vec4 vWorldPosition;");
        if (clipping) {
            src.push("flat in uint vFlags2;");
            for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("out highp ivec4 outNormal;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  bool clippable = vFlags2 > 0u;");
            src.push("  if (clippable) {");
            src.push("  float dist = 0.0;");
            for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > 0.0) { ");
            src.push("      discard;");
            src.push("  }");
            src.push("}");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("    gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("  vec3 xTangent = dFdx( vWorldPosition.xyz );");
        src.push("  vec3 yTangent = dFdy( vWorldPosition.xyz );");
        src.push("  vec3 worldNormal = normalize( cross( xTangent, yTangent ) );");
        src.push(`  outNormal = ivec4(worldNormal * float(${math.MAX_INT}), 1.0);`);
        src.push("}");
        return src;
    }

    webglContextRestored() {
        this._program = null;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
        }
        this._program = null;
    }
}

/**
 * @private
 */
class DTXTrianglesRenderers {

    constructor(scene) {
        this._scene = scene;
    }

    _compile() {
        if (this._colorRenderer && (!this._colorRenderer.getValid())) {
            this._colorRenderer.destroy();
            this._colorRenderer = null;
        }
        if (this._colorRendererWithSAO && (!this._colorRendererWithSAO.getValid())) {
            this._colorRendererWithSAO.destroy();
            this._colorRendererWithSAO = null;
        }
        if (this._flatColorRenderer && (!this._flatColorRenderer.getValid())) {
            this._flatColorRenderer.destroy();
            this._flatColorRenderer = null;
        }
        if (this._flatColorRendererWithSAO && (!this._flatColorRendererWithSAO.getValid())) {
            this._flatColorRendererWithSAO.destroy();
            this._flatColorRendererWithSAO = null;
        }
        if (this._colorQualityRendererWithSAO && (!this._colorQualityRendererWithSAO.getValid())) {
            this._colorQualityRendererWithSAO.destroy();
            this._colorQualityRendererWithSAO = null;
        }
        if (this._depthRenderer && (!this._depthRenderer.getValid())) {
            this._depthRenderer.destroy();
            this._depthRenderer = null;
        }
        if (this._normalsRenderer && (!this._normalsRenderer.getValid())) {
            this._normalsRenderer.destroy();
            this._normalsRenderer = null;
        }
        if (this._silhouetteRenderer && (!this._silhouetteRenderer.getValid())) {
            this._silhouetteRenderer.destroy();
            this._silhouetteRenderer = null;
        }
        if (this._edgesRenderer && (!this._edgesRenderer.getValid())) {
            this._edgesRenderer.destroy();
            this._edgesRenderer = null;
        }
        if (this._edgesColorRenderer && (!this._edgesColorRenderer.getValid())) {
            this._edgesColorRenderer.destroy();
            this._edgesColorRenderer = null;
        }
        if (this._pickMeshRenderer && (!this._pickMeshRenderer.getValid())) {
            this._pickMeshRenderer.destroy();
            this._pickMeshRenderer = null;
        }
        if (this._pickDepthRenderer && (!this._pickDepthRenderer.getValid())) {
            this._pickDepthRenderer.destroy();
            this._pickDepthRenderer = null;
        }
        if (this._snapRenderer && (!this._snapRenderer.getValid())) {
            this._snapRenderer.destroy();
            this._snapRenderer = null;
        }
        if (this._snapInitRenderer && (!this._snapInitRenderer.getValid())) {
            this._snapInitRenderer.destroy();
            this._snapInitRenderer = null;
        }
        if (this._pickNormalsRenderer && this._pickNormalsRenderer.getValid() === false) {
            this._pickNormalsRenderer.destroy();
            this._pickNormalsRenderer = null;
        }
        if (this._pickNormalsFlatRenderer && this._pickNormalsFlatRenderer.getValid() === false) {
            this._pickNormalsFlatRenderer.destroy();
            this._pickNormalsFlatRenderer = null;
        }
        if (this._occlusionRenderer && this._occlusionRenderer.getValid() === false) {
            this._occlusionRenderer.destroy();
            this._occlusionRenderer = null;
        }
    }

    eagerCreateRenders() {

        // Pre-initialize certain renderers that would otherwise be lazy-initialised
        // on user interaction, such as picking or emphasis, so that there is no delay
        // when user first begins interacting with the viewer.

        if (!this._silhouetteRenderer) { // Used for highlighting and selection
            this._silhouetteRenderer = new DTXTrianglesSilhouetteRenderer(this._scene);
        }
        if (!this._pickMeshRenderer) {
            this._pickMeshRenderer = new DTXTrianglesPickMeshRenderer(this._scene);
        }
        if (!this._pickDepthRenderer) {
            this._pickDepthRenderer = new DTXTrianglesPickDepthRenderer(this._scene);
        }
        if (!this._pickNormalsRenderer) {
            this._pickNormalsRenderer = new DTXTrianglesPickNormalsFlatRenderer(this._scene);
        }
        if (!this._snapRenderer) {
            this._snapRenderer = new DTXTrianglesSnapRenderer(this._scene);
        }
        if (!this._snapInitRenderer) {
            this._snapInitRenderer = new DTXTrianglesSnapInitRenderer(this._scene);
        }
        if (!this._snapRenderer) {
            this._snapRenderer = new DTXTrianglesSnapRenderer(this._scene);
        }
    }


    get colorRenderer() {
        if (!this._colorRenderer) {
            this._colorRenderer = new DTXTrianglesColorRenderer(this._scene, false);
        }
        return this._colorRenderer;
    }

    get colorRendererWithSAO() {
        if (!this._colorRendererWithSAO) {
            this._colorRendererWithSAO = new DTXTrianglesColorRenderer(this._scene, true);
        }
        return this._colorRendererWithSAO;
    }

    get colorQualityRendererWithSAO() {
        // if (!this._colorQualityRendererWithSAO) {
        //     this._colorQualityRendererWithSAO = new TrianglesDataTextureColorQualityRenderer(this._scene, true);
        // }
        return this._colorQualityRendererWithSAO;
    }

    get silhouetteRenderer() {
        if (!this._silhouetteRenderer) {
            this._silhouetteRenderer = new DTXTrianglesSilhouetteRenderer(this._scene);
        }
        return this._silhouetteRenderer;
    }

    get depthRenderer() {
        if (!this._depthRenderer) {
            this._depthRenderer = new DTXTrianglesDepthRenderer(this._scene);
        }
        return this._depthRenderer;
    }

    get normalsRenderer() {
        if (!this._normalsRenderer) {
            this._normalsRenderer = new DTXTrianglesNormalsRenderer(this._scene);
        }
        return this._normalsRenderer;
    }

    get edgesRenderer() {
        if (!this._edgesRenderer) {
            this._edgesRenderer = new DTXTrianglesEdgesRenderer(this._scene);
        }
        return this._edgesRenderer;
    }

    get edgesColorRenderer() {
        if (!this._edgesColorRenderer) {
            this._edgesColorRenderer = new DTXTrianglesEdgesColorRenderer(this._scene);
        }
        return this._edgesColorRenderer;
    }

    get pickMeshRenderer() {
        if (!this._pickMeshRenderer) {
            this._pickMeshRenderer = new DTXTrianglesPickMeshRenderer(this._scene);
        }
        return this._pickMeshRenderer;
    }

    get pickNormalsRenderer() {
        if (!this._pickNormalsRenderer) {
            this._pickNormalsRenderer = new DTXTrianglesPickNormalsFlatRenderer(this._scene);
        }
        return this._pickNormalsRenderer;
    }

    get pickNormalsFlatRenderer() {
        if (!this._pickNormalsFlatRenderer) {
            this._pickNormalsFlatRenderer = new DTXTrianglesPickNormalsFlatRenderer(this._scene);
        }
        return this._pickNormalsFlatRenderer;
    }

    get pickDepthRenderer() {
        if (!this._pickDepthRenderer) {
            this._pickDepthRenderer = new DTXTrianglesPickDepthRenderer(this._scene);
        }
        return this._pickDepthRenderer;
    }

    get snapRenderer() {
        if (!this._snapRenderer) {
            this._snapRenderer = new DTXTrianglesSnapRenderer(this._scene);
        }
        return this._snapRenderer;
    }

    get snapInitRenderer() {
        if (!this._snapInitRenderer) {
            this._snapInitRenderer = new DTXTrianglesSnapInitRenderer(this._scene);
        }
        return this._snapInitRenderer;
    }

    get occlusionRenderer() {
        if (!this._occlusionRenderer) {
            this._occlusionRenderer = new DTXTrianglesOcclusionRenderer(this._scene);
        }
        return this._occlusionRenderer;
    }

    _destroy() {
        if (this._colorRenderer) {
            this._colorRenderer.destroy();
        }
        if (this._colorRendererWithSAO) {
            this._colorRendererWithSAO.destroy();
        }
        if (this._flatColorRenderer) {
            this._flatColorRenderer.destroy();
        }
        if (this._flatColorRendererWithSAO) {
            this._flatColorRendererWithSAO.destroy();
        }
        if (this._colorQualityRendererWithSAO) {
            this._colorQualityRendererWithSAO.destroy();
        }
        if (this._depthRenderer) {
            this._depthRenderer.destroy();
        }
        if (this._normalsRenderer) {
            this._normalsRenderer.destroy();
        }
        if (this._silhouetteRenderer) {
            this._silhouetteRenderer.destroy();
        }
        if (this._edgesRenderer) {
            this._edgesRenderer.destroy();
        }
        if (this._edgesColorRenderer) {
            this._edgesColorRenderer.destroy();
        }
        if (this._pickMeshRenderer) {
            this._pickMeshRenderer.destroy();
        }
        if (this._pickDepthRenderer) {
            this._pickDepthRenderer.destroy();
        }
        if (this._snapRenderer) {
            this._snapRenderer.destroy();
        }
        if (this._snapInitRenderer) {
            this._snapInitRenderer.destroy();
        }
        if (this._pickNormalsRenderer) {
            this._pickNormalsRenderer.destroy();
        }
        if (this._pickNormalsFlatRenderer) {
            this._pickNormalsFlatRenderer.destroy();
        }
        if (this._occlusionRenderer) {
            this._occlusionRenderer.destroy();
        }
    }
}

const cachdRenderers = {};

/**
 * @private
 */
function getRenderers(scene) {
    const sceneId = scene.id;
    let dataTextureRenderers = cachdRenderers[sceneId];
    if (!dataTextureRenderers) {
        dataTextureRenderers = new DTXTrianglesRenderers(scene);
        cachdRenderers[sceneId] = dataTextureRenderers;
        dataTextureRenderers._compile();
        dataTextureRenderers.eagerCreateRenders();
        scene.on("compile", () => {
            dataTextureRenderers._compile();
            dataTextureRenderers.eagerCreateRenders();
        });
        scene.on("destroyed", () => {
            delete cachdRenderers[sceneId];
            dataTextureRenderers._destroy();
        });
    }
    return dataTextureRenderers;
}

/**
 * @private
 */
class DTXTrianglesBuffer {

    constructor() {
        this.positionsCompressed = [];
        this.lenPositionsCompressed = 0;

        this.metallicRoughness = [];

        this.indices8Bits = [];
        this.lenIndices8Bits = 0;

        this.indices16Bits = [];
        this.lenIndices16Bits = 0;

        this.indices32Bits = [];
        this.lenIndices32Bits = 0;

        this.edgeIndices8Bits = [];
        this.lenEdgeIndices8Bits = 0;

        this.edgeIndices16Bits = [];
        this.lenEdgeIndices16Bits = 0;

        this.edgeIndices32Bits = [];
        this.lenEdgeIndices32Bits = 0;

        this.perObjectColors = [];
        this.perObjectPickColors = [];
        this.perObjectSolid = [];
        this.perObjectOffsets = [];
        this.perObjectPositionsDecodeMatrices = [];
        this.perObjectInstancePositioningMatrices = [];
        this.perObjectVertexBases = [];
        this.perObjectIndexBaseOffsets = [];
        this.perObjectEdgeIndexBaseOffsets = [];
        this.perTriangleNumberPortionId8Bits = [];
        this.perTriangleNumberPortionId16Bits = [];
        this.perTriangleNumberPortionId32Bits = [];
        this.perEdgeNumberPortionId8Bits = [];
        this.perEdgeNumberPortionId16Bits = [];
        this.perEdgeNumberPortionId32Bits = [];
    }
}

// Imports used to complete the JSDocs arguments to methods

/**
 * @private
 */
class DTXTrianglesState {
    constructor() {
        /**
         * Texture that holds colors/pickColors/flags/flags2 per-object:
         * - columns: one concept per column => color / pick-color / ...
         * - row: the object Id
         *
         * @type BindableDataTexture
         */
        this.texturePerObjectColorsAndFlags = null;

        /**
         * Texture that holds the XYZ offsets per-object:
         * - columns: just 1 column with the XYZ-offset
         * - row: the object Id
         *
         * @type BindableDataTexture
         */
        this.texturePerObjectOffsets = null;

        this.texturePerObjectInstanceMatrices = null;

        /**
         * Texture that holds the objectDecodeAndInstanceMatrix per-object:
         * - columns: each column is one column of the matrix
         * - row: the object Id
         *
         * @type BindableDataTexture
         */
        this.texturePerObjectPositionsDecodeMatrix = null;

        /**
         * Texture that holds all the `different-vertices` used by the layer.
         *
         * @type BindableDataTexture
         */
        this.texturePerVertexIdCoordinates = null;

        /**
         * Texture that holds the PortionId that corresponds to a given polygon-id.
         *
         * Variant of the texture for 8-bit based polygon-ids.
         *
         * @type BindableDataTexture
         */
        this.texturePerPolygonIdPortionIds8Bits = null;

        /**
         * Texture that holds the PortionId that corresponds to a given polygon-id.
         *
         * Variant of the texture for 16-bit based polygon-ids.
         *
         * @type BindableDataTexture
         */
        this.texturePerPolygonIdPortionIds16Bits = null;

        /**
         * Texture that holds the PortionId that corresponds to a given polygon-id.
         *
         * Variant of the texture for 32-bit based polygon-ids.
         *
         * @type BindableDataTexture
         */
        this.texturePerPolygonIdPortionIds32Bits = null;

        /**
         * Texture that holds the PortionId that corresponds to a given edge-id.
         *
         * Variant of the texture for 8-bit based polygon-ids.
         *
         * @type BindableDataTexture
         */
        this.texturePerEdgeIdPortionIds8Bits = null;

        /**
         * Texture that holds the PortionId that corresponds to a given edge-id.
         *
         * Variant of the texture for 16-bit based polygon-ids.
         *
         * @type BindableDataTexture
         */
        this.texturePerEdgeIdPortionIds16Bits = null;

        /**
         * Texture that holds the PortionId that corresponds to a given edge-id.
         *
         * Variant of the texture for 32-bit based polygon-ids.
         *
         * @type BindableDataTexture
         */
        this.texturePerEdgeIdPortionIds32Bits = null;

        /**
         * Texture that holds the unique-vertex-indices for 8-bit based indices.
         *
         * @type BindableDataTexture
         */
        this.texturePerPolygonIdIndices8Bits = null;

        /**
         * Texture that holds the unique-vertex-indices for 16-bit based indices.
         *
         * @type BindableDataTexture
         */
        this.texturePerPolygonIdIndices16Bits = null;

        /**
         * Texture that holds the unique-vertex-indices for 32-bit based indices.
         *
         * @type BindableDataTexture
         */
        this.texturePerPolygonIdIndices32Bits = null;

        /**
         * Texture that holds the unique-vertex-indices for 8-bit based edge indices.
         *
         * @type BindableDataTexture
         */
        this.texturePerPolygonIdEdgeIndices8Bits = null;

        /**
         * Texture that holds the unique-vertex-indices for 16-bit based edge indices.
         *
         * @type BindableDataTexture
         */
        this.texturePerPolygonIdEdgeIndices16Bits = null;

        /**
         * Texture that holds the unique-vertex-indices for 32-bit based edge indices.
         *
         * @type BindableDataTexture
         */
        this.texturePerPolygonIdEdgeIndices32Bits = null;

        /**
         * Texture that holds the model matrices
         * - columns: each column in the texture is a model matrix column.
         * - row: each row is a different model matrix.
         *
         * @type BindableDataTexture
         */
        this.textureModelMatrices = null;
    }

    finalize() {
        this.indicesPerBitnessTextures = {
            8: this.texturePerPolygonIdIndices8Bits,
            16: this.texturePerPolygonIdIndices16Bits,
            32: this.texturePerPolygonIdIndices32Bits,
        };

        this.indicesPortionIdsPerBitnessTextures = {
            8: this.texturePerPolygonIdPortionIds8Bits,
            16: this.texturePerPolygonIdPortionIds16Bits,
            32: this.texturePerPolygonIdPortionIds32Bits,
        };

        this.edgeIndicesPerBitnessTextures = {
            8: this.texturePerPolygonIdEdgeIndices8Bits,
            16: this.texturePerPolygonIdEdgeIndices16Bits,
            32: this.texturePerPolygonIdEdgeIndices32Bits,
        };

        this.edgeIndicesPortionIdsPerBitnessTextures = {
            8: this.texturePerEdgeIdPortionIds8Bits,
            16: this.texturePerEdgeIdPortionIds16Bits,
            32: this.texturePerEdgeIdPortionIds32Bits,
        };
    }

    /**
     *
     * @param {Program} glProgram
     * @param {string} objectDecodeMatricesShaderName
     * @param {string} vertexTextureShaderName
     * @param {string} objectAttributesTextureShaderName
     * @param {string} objectMatricesShaderName
     */
    bindCommonTextures(
        glProgram,
        objectDecodeMatricesShaderName,
        vertexTextureShaderName,
        objectAttributesTextureShaderName,
        objectMatricesShaderName
    ) {
        this.texturePerObjectPositionsDecodeMatrix.bindTexture(glProgram, objectDecodeMatricesShaderName, 1);
        this.texturePerVertexIdCoordinates.bindTexture(glProgram, vertexTextureShaderName, 2);
        this.texturePerObjectColorsAndFlags.bindTexture(glProgram, objectAttributesTextureShaderName, 3);
        this.texturePerObjectInstanceMatrices.bindTexture(glProgram, objectMatricesShaderName, 4);
    }

    /**
     *
     * @param {Program} glProgram
     * @param {string} portionIdsShaderName
     * @param {string} polygonIndicesShaderName
     * @param {8|16|32} textureBitness
     */
    bindTriangleIndicesTextures(
        glProgram,
        portionIdsShaderName,
        polygonIndicesShaderName,
        textureBitness,
    ) {
        this.indicesPortionIdsPerBitnessTextures[textureBitness].bindTexture(
            glProgram,
            portionIdsShaderName,
            5 // webgl texture unit
        );

        this.indicesPerBitnessTextures[textureBitness].bindTexture(
            glProgram,
            polygonIndicesShaderName,
            6 // webgl texture unit
        );
    }

    /**
     *
     * @param {Program} glProgram
     * @param {string} edgePortionIdsShaderName
     * @param {string} edgeIndicesShaderName
     * @param {8|16|32} textureBitness
     */
    bindEdgeIndicesTextures(
        glProgram,
        edgePortionIdsShaderName,
        edgeIndicesShaderName,
        textureBitness,
    ) {
        this.edgeIndicesPortionIdsPerBitnessTextures[textureBitness].bindTexture(
            glProgram,
            edgePortionIdsShaderName,
            5 // webgl texture unit
        );

        this.edgeIndicesPerBitnessTextures[textureBitness].bindTexture(
            glProgram,
            edgeIndicesShaderName,
            6 // webgl texture unit
        );
    }
}

const dataTextureRamStats = {
    sizeDataColorsAndFlags: 0,
    sizeDataPositionDecodeMatrices: 0,
    sizeDataTextureOffsets: 0,
    sizeDataTexturePositions: 0,
    sizeDataTextureIndices: 0,
    sizeDataTextureEdgeIndices: 0,
    sizeDataTexturePortionIds: 0,
    numberOfGeometries: 0,
    numberOfPortions: 0,
    numberOfLayers: 0,
    numberOfTextures: 0,
    totalPolygons: 0,
    totalPolygons8Bits: 0,
    totalPolygons16Bits: 0,
    totalPolygons32Bits: 0,
    totalEdges: 0,
    totalEdges8Bits: 0,
    totalEdges16Bits: 0,
    totalEdges32Bits: 0,
    cannotCreatePortion: {
        because10BitsObjectId: 0,
        becauseTextureSize: 0,
    },
    overheadSizeAlignementIndices: 0,
    overheadSizeAlignementEdgeIndices: 0,
};

window.printDataTextureRamStats = function () {

    console.log(JSON.stringify(dataTextureRamStats, null, 4));

    let totalRamSize = 0;

    Object.keys(dataTextureRamStats).forEach(key => {
        if (key.startsWith("size")) {
            totalRamSize += dataTextureRamStats[key];
        }
    });

    console.log(`Total size ${totalRamSize} bytes (${(totalRamSize / 1000 / 1000).toFixed(2)} MB)`);
    console.log(`Avg bytes / triangle: ${(totalRamSize / dataTextureRamStats.totalPolygons).toFixed(2)}`);

    let percentualRamStats = {};

    Object.keys(dataTextureRamStats).forEach(key => {
        if (key.startsWith("size")) {
            percentualRamStats[key] =
                `${(dataTextureRamStats[key] / totalRamSize * 100).toFixed(2)} % of total`;
        }
    });

    console.log(JSON.stringify({percentualRamUsage: percentualRamStats}, null, 4));
};

/**
 * @private
 */
class DTXTrianglesTextureFactory {

    constructor() {

    }

    /**
     * Enables the currently binded ````WebGLTexture```` to be used as a data texture.
     *
     * @param {WebGL2RenderingContext} gl
     *
     * @private
     */
    disableBindedTextureFiltering(gl) {
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    }

    /**
     * This will generate an RGBA texture for:
     * - colors
     * - pickColors
     * - flags
     * - flags2
     * - vertex bases
     * - vertex base offsets
     *
     * The texture will have:
     * - 4 RGBA columns per row: for each object (pick) color and flags(2)
     * - N rows where N is the number of objects
     *
     * @param {WebGL2RenderingContext} gl
     * @param {ArrayLike<ArrayLike<int>>} colors Array of colors for all objects in the layer
     * @param {ArrayLike<ArrayLike<int>>} pickColors Array of pickColors for all objects in the layer
     * @param {ArrayLike<int>} vertexBases Array of position-index-bases foteh all objects in the layer
     * @param {ArrayLike<int>} indexBaseOffsets For triangles: array of offests between the (gl_VertexID / 3) and the position where the indices start in the texture layer
     * @param {ArrayLike<int>} edgeIndexBaseOffsets For edges: Array of offests between the (gl_VertexID / 2) and the position where the edge indices start in the texture layer
     * @param {ArrayLike<boolean>} solid Array is-solid flag for all objects in the layer
     *
     * @returns {BindableDataTexture}
     */
    createTextureForColorsAndFlags(gl, colors, pickColors, vertexBases, indexBaseOffsets, edgeIndexBaseOffsets, solid) {
        const numPortions = colors.length;

        // The number of rows in the texture is the number of
        // objects in the layer.

        this.numPortions = numPortions;

        const textureWidth = 512 * 8;
        const textureHeight = Math.ceil(numPortions / (textureWidth / 8));

        if (textureHeight === 0) {
            throw "texture height===0";
        }

        // 8 columns per texture row:
        // - col0: (RGBA) object color RGBA
        // - col1: (packed Uint32 as RGBA) object pick color
        // - col2: (packed 4 bytes as RGBA) object flags
        // - col3: (packed 4 bytes as RGBA) object flags2
        // - col4: (packed Uint32 bytes as RGBA) vertex base
        // - col5: (packed Uint32 bytes as RGBA) index base offset
        // - col6: (packed Uint32 bytes as RGBA) edge index base offset
        // - col7: (packed 4 bytes as RGBA) is-solid flag for objects

        const texArray = new Uint8Array(4 * textureWidth * textureHeight);

        dataTextureRamStats.sizeDataColorsAndFlags += texArray.byteLength;
        dataTextureRamStats.numberOfTextures++;

        for (let i = 0; i < numPortions; i++) {
            // object color
            texArray.set(colors [i], i * 32 + 0);
            texArray.set(pickColors [i], i * 32 + 4); // object pick color
            texArray.set([0, 0, 0, 0], i * 32 + 8);     // object flags
            texArray.set([0, 0, 0, 0], i * 32 + 12);        // object flags2

            // vertex base
            texArray.set([
                    (vertexBases[i] >> 24) & 255,
                    (vertexBases[i] >> 16) & 255,
                    (vertexBases[i] >> 8) & 255,
                    (vertexBases[i]) & 255,
                ],
                i * 32 + 16
            );

            // triangles index base offset
            texArray.set(
                [
                    (indexBaseOffsets[i] >> 24) & 255,
                    (indexBaseOffsets[i] >> 16) & 255,
                    (indexBaseOffsets[i] >> 8) & 255,
                    (indexBaseOffsets[i]) & 255,
                ],
                i * 32 + 20
            );

            // edge index base offset
            texArray.set(
                [
                    (edgeIndexBaseOffsets[i] >> 24) & 255,
                    (edgeIndexBaseOffsets[i] >> 16) & 255,
                    (edgeIndexBaseOffsets[i] >> 8) & 255,
                    (edgeIndexBaseOffsets[i]) & 255,
                ],
                i * 32 + 24
            );

            // is-solid flag
            texArray.set([solid[i] ? 1 : 0, 0, 0, 0], i * 32 + 28);
        }
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8UI, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGBA_INTEGER, gl.UNSIGNED_BYTE, texArray, 0);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight, texArray);
    }

    /**
     * This will generate a texture for all object offsets.
     *
     * @param {WebGL2RenderingContext} gl
     * @param {int[]} offsets Array of int[3], one XYZ offset array for each object
     *
     * @returns {BindableDataTexture}
     */
    createTextureForObjectOffsets(gl, numOffsets) {
        const textureWidth = 512;
        const textureHeight = Math.ceil(numOffsets / textureWidth);
        if (textureHeight === 0) {
            throw "texture height===0";
        }
        const texArray = new Float32Array(3 * textureWidth * textureHeight).fill(0);
        dataTextureRamStats.sizeDataTextureOffsets += texArray.byteLength;
        dataTextureRamStats.numberOfTextures++;
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB32F, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB, gl.FLOAT, texArray, 0);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight, texArray);
    }

    /**
     * This will generate a texture for all positions decode matrices in the layer.
     *
     * The texture will have:
     * - 4 RGBA columns per row (each column will contain 4 packed half-float (16 bits) components).
     *   Thus, each row will contain 16 packed half-floats corresponding to a complete positions decode matrix)
     * - N rows where N is the number of objects
     *
     * @param {WebGL2RenderingContext} gl
     * @param {ArrayLike<Matrix4x4>} instanceMatrices Array of geometry instancing matrices for all objects in the layer. Null if the objects are not instanced.
     *
     * @returns {BindableDataTexture}
     */
    createTextureForInstancingMatrices(gl, instanceMatrices) {
        const numMatrices = instanceMatrices.length;
        if (numMatrices === 0) {
            throw "num instance matrices===0";
        }
        // in one row we can fit 512 matrices
        const textureWidth = 512 * 4;
        const textureHeight = Math.ceil(numMatrices / (textureWidth / 4));
        const texArray = new Float32Array(4 * textureWidth * textureHeight);
        // dataTextureRamStats.sizeDataPositionDecodeMatrices += texArray.byteLength;
        dataTextureRamStats.numberOfTextures++;
        for (let i = 0; i < instanceMatrices.length; i++) {            // 4x4 values
            texArray.set(instanceMatrices[i], i * 16);
        }
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGBA, gl.FLOAT, texArray, 0);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight, texArray);
    }

    /**
     * This will generate a texture for all positions decode matrices in the layer.
     *
     * The texture will have:
     * - 4 RGBA columns per row (each column will contain 4 packed half-float (16 bits) components).
     *   Thus, each row will contain 16 packed half-floats corresponding to a complete positions decode matrix)
     * - N rows where N is the number of objects
     *
     * @param {WebGL2RenderingContext} gl
     * @param {ArrayLike<Matrix4x4>} positionDecodeMatrices Array of positions decode matrices for all objects in the layer
     *
     * @returns {BindableDataTexture}
     */
    createTextureForPositionsDecodeMatrices(gl, positionDecodeMatrices) {
        const numMatrices = positionDecodeMatrices.length;
        if (numMatrices === 0) {
            throw "num decode+entity matrices===0";
        }
        // in one row we can fit 512 matrices
        const textureWidth = 512 * 4;
        const textureHeight = Math.ceil(numMatrices / (textureWidth / 4));
        const texArray = new Float32Array(4 * textureWidth * textureHeight);
        dataTextureRamStats.sizeDataPositionDecodeMatrices += texArray.byteLength;
        dataTextureRamStats.numberOfTextures++;
        for (let i = 0; i < positionDecodeMatrices.length; i++) {            // 4x4 values
            texArray.set(positionDecodeMatrices[i], i * 16);
        }
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGBA, gl.FLOAT, texArray, 0);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight);
    }

    /**
     * @param {WebGL2RenderingContext} gl
     * @param indicesArrays
     * @param lenIndices
     *
     * @returns {BindableDataTexture}
     */
    createTextureFor8BitIndices(gl, indicesArrays, lenIndices) {
        if (lenIndices === 0) {
            return {texture: null, textureHeight: 0,};
        }
        const textureWidth = 4096;
        const textureHeight = Math.ceil(lenIndices / 3 / textureWidth);
        if (textureHeight === 0) {
            throw "texture height===0";
        }
        const texArraySize = textureWidth * textureHeight * 3;
        const texArray = new Uint8Array(texArraySize);
        dataTextureRamStats.sizeDataTextureIndices += texArray.byteLength;
        dataTextureRamStats.numberOfTextures++;
        for (let i = 0, j = 0, len = indicesArrays.length; i < len; i++) {
            const pc = indicesArrays[i];
            texArray.set(pc, j);
            j += pc.length;
        }
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB8UI, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB_INTEGER, gl.UNSIGNED_BYTE, texArray, 0);
        this.disableBindedTextureFiltering(gl);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight);
    }

    /**
     * @param {WebGL2RenderingContext} gl
     * @param indicesArrays
     * @param lenIndices
     *
     * @returns {BindableDataTexture}
     */
    createTextureFor16BitIndices(gl, indicesArrays, lenIndices) {
        if (lenIndices === 0) {
            return {texture: null, textureHeight: 0,};
        }
        const textureWidth = 4096;
        const textureHeight = Math.ceil(lenIndices / 3 / textureWidth);
        if (textureHeight === 0) {
            throw "texture height===0";
        }
        const texArraySize = textureWidth * textureHeight * 3;
        const texArray = new Uint16Array(texArraySize);
        dataTextureRamStats.sizeDataTextureIndices += texArray.byteLength;
        dataTextureRamStats.numberOfTextures++;
        for (let i = 0, j = 0, len = indicesArrays.length; i < len; i++) {
            const pc = indicesArrays[i];
            texArray.set(pc, j);
            j += pc.length;
        }
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB16UI, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB_INTEGER, gl.UNSIGNED_SHORT, texArray, 0);
        this.disableBindedTextureFiltering(gl);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight);
    }

    /**
     * @param {WebGL2RenderingContext} gl
     * @param indicesArrays
     * @param lenIndices
     *
     * @returns {BindableDataTexture}
     */
    createTextureFor32BitIndices(gl, indicesArrays, lenIndices) {
        if (lenIndices === 0) {
            return {texture: null, textureHeight: 0,};
        }
        const textureWidth = 4096;
        const textureHeight = Math.ceil(lenIndices / 3 / textureWidth);
        if (textureHeight === 0) {
            throw "texture height===0";
        }
        const texArraySize = textureWidth * textureHeight * 3;
        const texArray = new Uint32Array(texArraySize);
        dataTextureRamStats.sizeDataTextureIndices += texArray.byteLength;
        dataTextureRamStats.numberOfTextures++;
        for (let i = 0, j = 0, len = indicesArrays.length; i < len; i++) {
            const pc = indicesArrays[i];
            texArray.set(pc, j);
            j += pc.length;
        }
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB32UI, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB_INTEGER, gl.UNSIGNED_INT, texArray, 0);
        this.disableBindedTextureFiltering(gl);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight);
    }

    /**
     * @param {WebGL2RenderingContext} gl
     * @param indicesArrays
     * @param lenIndices
     *
     * @returns {BindableDataTexture}
     */
    createTextureFor8BitsEdgeIndices(gl, indicesArrays, lenIndices) {
        if (lenIndices === 0) {
            return {texture: null, textureHeight: 0,};
        }
        const textureWidth = 4096;
        const textureHeight = Math.ceil(lenIndices / 2 / textureWidth);
        if (textureHeight === 0) {
            throw "texture height===0";
        }
        const texArraySize = textureWidth * textureHeight * 2;
        const texArray = new Uint8Array(texArraySize);
        dataTextureRamStats.sizeDataTextureEdgeIndices += texArray.byteLength;
        dataTextureRamStats.numberOfTextures++;
        for (let i = 0, j = 0, len = indicesArrays.length; i < len; i++) {
            const pc = indicesArrays[i];
            texArray.set(pc, j);
            j += pc.length;
        }
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RG8UI, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RG_INTEGER, gl.UNSIGNED_BYTE, texArray, 0);
        this.disableBindedTextureFiltering(gl);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight);
    }

    /**
     * @param {WebGL2RenderingContext} gl
     * @param indicesArrays
     * @param lenIndices
     *
     * @returns {BindableDataTexture}
     */
    createTextureFor16BitsEdgeIndices(gl, indicesArrays, lenIndices) {
        if (lenIndices === 0) {
            return {texture: null, textureHeight: 0,};
        }
        const textureWidth = 4096;
        const textureHeight = Math.ceil(lenIndices / 2 / textureWidth);
        if (textureHeight === 0) {
            throw "texture height===0";
        }
        const texArraySize = textureWidth * textureHeight * 2;
        const texArray = new Uint16Array(texArraySize);
        dataTextureRamStats.sizeDataTextureEdgeIndices += texArray.byteLength;
        dataTextureRamStats.numberOfTextures++;
        for (let i = 0, j = 0, len = indicesArrays.length; i < len; i++) {
            const pc = indicesArrays[i];
            texArray.set(pc, j);
            j += pc.length;
        }
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RG16UI, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RG_INTEGER, gl.UNSIGNED_SHORT, texArray, 0);
        this.disableBindedTextureFiltering(gl);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight);
    }

    /**
     * @param {WebGL2RenderingContext} gl
     * @param indicesArrays
     * @param lenIndices
     *
     * @returns {BindableDataTexture}
     */
    createTextureFor32BitsEdgeIndices(gl, indicesArrays, lenIndices) {
        if (lenIndices === 0) {
            return {texture: null, textureHeight: 0,};
        }
        const textureWidth = 4096;
        const textureHeight = Math.ceil(lenIndices / 2 / textureWidth);
        if (textureHeight === 0) {
            throw "texture height===0";
        }
        const texArraySize = textureWidth * textureHeight * 2;
        const texArray = new Uint32Array(texArraySize);
        dataTextureRamStats.sizeDataTextureEdgeIndices += texArray.byteLength;
        dataTextureRamStats.numberOfTextures++;
        for (let i = 0, j = 0, len = indicesArrays.length; i < len; i++) {
            const pc = indicesArrays[i];
            texArray.set(pc, j);
            j += pc.length;
        }
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RG32UI, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RG_INTEGER, gl.UNSIGNED_INT, texArray, 0);
        this.disableBindedTextureFiltering(gl);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight);
    }

    /**
     * @param {WebGL2RenderingContext} gl
     * @param {ArrayLike<int>} positionsArrays Arrays of  quantized positions in the layer
     * @param lenPositions
     *
     * This will generate a texture for positions in the layer.
     *
     * The texture will have:
     * - 1024 columns, where each pixel will be a 16-bit-per-component RGB texture, corresponding to the XYZ of the position
     * - a number of rows R where R*1024 is just >= than the number of vertices (positions / 3)
     *
     * @returns {BindableDataTexture}
     */
    createTextureForPositions(gl, positionsArrays, lenPositions) {
        const numVertices = lenPositions / 3;
        const textureWidth = 4096;
        const textureHeight = Math.ceil(numVertices / textureWidth);
        if (textureHeight === 0) {
            throw "texture height===0";
        }
        const texArraySize = textureWidth * textureHeight * 3;
        const texArray = new Uint16Array(texArraySize);
        dataTextureRamStats.sizeDataTexturePositions += texArray.byteLength;
        dataTextureRamStats.numberOfTextures++;
        for (let i = 0, j = 0, len = positionsArrays.length; i < len; i++) {
            const pc = positionsArrays[i];
            texArray.set(pc, j);
            j += pc.length;
        }
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB16UI, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB_INTEGER, gl.UNSIGNED_SHORT, texArray, 0);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight);
    }

    /**
     * @param {WebGL2RenderingContext} gl
     * @param {ArrayLike<int>} portionIdsArray
     *
     * @returns {BindableDataTexture}
     */
    createTextureForPackedPortionIds(gl, portionIdsArray) {
        if (portionIdsArray.length === 0) {
            return {texture: null, textureHeight: 0,};
        }
        const lenArray = portionIdsArray.length;
        const textureWidth = 4096;
        const textureHeight = Math.ceil(lenArray / textureWidth);
        if (textureHeight === 0) {
            throw "texture height===0";
        }
        const texArraySize = textureWidth * textureHeight;
        const texArray = new Uint16Array(texArraySize);
        texArray.set(portionIdsArray, 0);
        dataTextureRamStats.sizeDataTexturePortionIds += texArray.byteLength;
        dataTextureRamStats.numberOfTextures++;
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texStorage2D(gl.TEXTURE_2D, 1, gl.R16UI, textureWidth, textureHeight);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RED_INTEGER, gl.UNSIGNED_SHORT, texArray, 0);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return new BindableDataTexture(gl, texture, textureWidth, textureHeight);
    }
}

const configs = new Configs();

/**
 * 12-bits allowed for object ids.
 * Limits the per-object texture height in the layer.
 */
const MAX_NUMBER_OF_OBJECTS_IN_LAYER = (1 << 16);

/**
 * 4096 is max data texture height.
 * Limits the aggregated geometry texture height in the layer.
 */
const MAX_DATA_TEXTURE_HEIGHT = configs.maxDataTextureHeight;

/**
 * Align `indices` and `edgeIndices` memory layout to 8 elements.
 *
 * Used as an optimization for the `...portionIds...` texture, so it
 * can just be stored 1 out of 8 `portionIds` corresponding to a given
 * `triangle-index` or `edge-index`.
 */
const INDICES_EDGE_INDICES_ALIGNEMENT_SIZE = 8;

/**
 * Number of maximum allowed per-object flags update per render frame
 * before switching to batch update mode.
 */
const MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE = 10;

const tempVec4a$a = math.vec4();
const tempMat4a = new Float32Array(16);
const tempUint8Array4 = new Uint8Array(4);
const tempFloat32Array3 = new Float32Array(3);

let numLayers = 0;

const DEFAULT_MATRIX$1 = math.identityMat4();

/**
 * @private
 */
class DTXTrianglesLayer {

    constructor(model, cfg) {

        //console.info("Creating DTXTrianglesLayer");

        dataTextureRamStats.numberOfLayers++;

        this._layerNumber = numLayers++;
        this.sortId = `TriDTX-${this._layerNumber}`; // State sorting key.
        this.layerIndex = cfg.layerIndex; // Index of this TrianglesDataTextureLayer in {@link SceneModel#_layerList}.

        this._renderers = getRenderers(model.scene);
        this.model = model;
        this._buffer = new DTXTrianglesBuffer();
        this._dtxState = new DTXTrianglesState();
        this._dtxTextureFactory = new DTXTrianglesTextureFactory();

        this._state = new RenderState({
            origin: math.vec3(cfg.origin),
            metallicRoughnessBuf: null,
            textureState: this._dtxState,
            numIndices8Bits: 0,
            numIndices16Bits: 0,
            numIndices32Bits: 0,
            numEdgeIndices8Bits: 0,
            numEdgeIndices16Bits: 0,
            numEdgeIndices32Bits: 0,
            numVertices: 0,
        });

        this._numPortions = 0;        // These counts are used to avoid unnecessary render passes
        this._numVisibleLayerPortions = 0;
        this._numTransparentLayerPortions = 0;
        this._numXRayedLayerPortions = 0;
        this._numSelectedLayerPortions = 0;
        this._numHighlightedLayerPortions = 0;
        this._numClippableLayerPortions = 0;
        this._numEdgesLayerPortions = 0;
        this._numPickableLayerPortions = 0;
        this._numCulledLayerPortions = 0;

        this._subPortions = [];

        if (this.model.scene.readableGeometryEnabled) {
            this._subPortionReadableGeometries = {};
        }

        /**
         * Due to `index rebucketting` process in ```prepareMeshGeometry``` function, it's possible that a single
         * portion is expanded to more than 1 real sub-portion.
         *
         * This Array tracks the mapping between:
         *
         * - external `portionIds` as seen by consumers of this class.
         * - internal `sub-portionIds` actually managed by this class.
         *
         * The outer index of this array is the externally seen `portionId`.
         * The inner value of the array, are `sub-portionIds` corresponding to the `portionId`.
         */
        this._portionToSubPortionsMap = [];

        this._bucketGeometries = {};

        this._meshes = [];

        /**
         * The axis-aligned World-space boundary of this TrianglesDataTextureLayer's positions.
         */
        this._aabb = math.collapseAABB3();
        this.aabbDirty = true;

        /**
         * The number of updates in the current frame;
         */
        this._numUpdatesInFrame = 0;

        /**
         * The type of primitives in this layer.
         */
        this.primitive = cfg.primitive;

        this._finalized = false;
    }

    get aabb() {
        if (this.aabbDirty) {
            math.collapseAABB3(this._aabb);
            for (let i = 0, len = this._meshes.length; i < len; i++) {
                math.expandAABB3(this._aabb, this._meshes[i].aabb);
            }
            this.aabbDirty = false;
        }
        return this._aabb;
    }

    /**
     * Returns whether the ```TrianglesDataTextureLayer``` has room for more portions.
     *
     * @param {object} portionCfg An object containing the geometrical data (`positions`, `indices`, `edgeIndices`) for the portion.
     * @returns {Boolean} Wheter the requested portion can be created
     */
    canCreatePortion(portionCfg) {
        if (this._finalized) {
            throw "Already finalized";
        }
        const numNewPortions = portionCfg.buckets.length;
        if ((this._numPortions + numNewPortions) > MAX_NUMBER_OF_OBJECTS_IN_LAYER) {
            dataTextureRamStats.cannotCreatePortion.because10BitsObjectId++;
        }
        let retVal = (this._numPortions + numNewPortions) <= MAX_NUMBER_OF_OBJECTS_IN_LAYER;
        const bucketIndex = 0; // TODO: Is this a bug?
        const bucketGeometryId = portionCfg.geometryId !== undefined && portionCfg.geometryId !== null
            ? `${portionCfg.geometryId}#${bucketIndex}`
            : `${portionCfg.id}#${bucketIndex}`;
        const alreadyHasPortionGeometry = this._bucketGeometries[bucketGeometryId];
        if (!alreadyHasPortionGeometry) {
            const maxIndicesOfAnyBits = Math.max(this._state.numIndices8Bits, this._state.numIndices16Bits, this._state.numIndices32Bits,);
            let numVertices = 0;
            let numIndices = 0;
            portionCfg.buckets.forEach(bucket => {
                numVertices += bucket.positionsCompressed.length / 3;
                numIndices += bucket.indices.length / 3;
            });
            if ((this._state.numVertices + numVertices) > MAX_DATA_TEXTURE_HEIGHT * 4096 ||
                (maxIndicesOfAnyBits + numIndices) > MAX_DATA_TEXTURE_HEIGHT * 4096) {
                dataTextureRamStats.cannotCreatePortion.becauseTextureSize++;
            }
            retVal &&=
                (this._state.numVertices + numVertices) <= MAX_DATA_TEXTURE_HEIGHT * 4096 &&
                (maxIndicesOfAnyBits + numIndices) <= MAX_DATA_TEXTURE_HEIGHT * 4096;
        }
        return retVal;
    }

    /**
     * Creates a new portion within this TrianglesDataTextureLayer, returns the new portion ID.
     *
     * Gives the portion the specified geometry, color and matrix.
     *
     * @param mesh The SceneModelMesh that owns the portion
     * @param portionCfg.positionsCompressed Flat float Local-space positionsCompressed array.
     * @param [portionCfg.normals] Flat float normals array.
     * @param [portionCfg.colors] Flat float colors array.
     * @param portionCfg.indices  Flat int indices array.
     * @param [portionCfg.edgeIndices] Flat int edges indices array.
     * @param portionCfg.color Quantized RGB color [0..255,0..255,0..255,0..255]
     * @param portionCfg.metallic Metalness factor [0..255]
     * @param portionCfg.roughness Roughness factor [0..255]
     * @param portionCfg.opacity Opacity [0..255]
     * @param [portionCfg.meshMatrix] Flat float 4x4 matrix - transforms the portion within the coordinate system that's local to the SceneModel
     * @param portionCfg.worldAABB Flat float AABB World-space AABB
     * @param portionCfg.pickColor Quantized pick color
     * @returns {number} Portion ID
     */
    createPortion(mesh, portionCfg) {
        if (this._finalized) {
            throw "Already finalized";
        }
        const subPortionIds = [];
        //   const portionAABB = portionCfg.worldAABB;
        portionCfg.buckets.forEach((bucket, bucketIndex) => {
            const bucketGeometryId = portionCfg.geometryId !== undefined && portionCfg.geometryId !== null
                ? `${portionCfg.geometryId}#${bucketIndex}`
                : `${portionCfg.id}#${bucketIndex}`;
            let bucketGeometry = this._bucketGeometries[bucketGeometryId];
            if (!bucketGeometry) {
                bucketGeometry = this._createBucketGeometry(portionCfg, bucket);
                this._bucketGeometries[bucketGeometryId] = bucketGeometry;
            }
            //  const subPortionAABB = math.collapseAABB3(tempAABB3b);
            const subPortionId = this._createSubPortion(portionCfg, bucketGeometry, bucket);
            //math.expandAABB3(portionAABB, subPortionAABB);
            subPortionIds.push(subPortionId);
        });
        const portionId = this._portionToSubPortionsMap.length;
        this._portionToSubPortionsMap.push(subPortionIds);
        this.model.numPortions++;
        this._meshes.push(mesh);
        return portionId;
    }

    _createBucketGeometry(portionCfg, bucket) {

        // Indices alignement
        // This will make every mesh consume a multiple of INDICES_EDGE_INDICES_ALIGNEMENT_SIZE
        // array items for storing the triangles of the mesh, and it supports:
        // - a memory optimization of factor INDICES_EDGE_INDICES_ALIGNEMENT_SIZE
        // - in exchange for a small RAM overhead
        //   (by adding some padding until a size that is multiple of INDICES_EDGE_INDICES_ALIGNEMENT_SIZE)

        if (bucket.indices) {
            const alignedIndicesLen = Math.ceil((bucket.indices.length / 3) / INDICES_EDGE_INDICES_ALIGNEMENT_SIZE) * INDICES_EDGE_INDICES_ALIGNEMENT_SIZE * 3;
            dataTextureRamStats.overheadSizeAlignementIndices += 2 * (alignedIndicesLen - bucket.indices.length);
            const alignedIndices = new Uint32Array(alignedIndicesLen);
            alignedIndices.fill(0);
            alignedIndices.set(bucket.indices);
            bucket.indices = alignedIndices;
        }

        // EdgeIndices alignement
        // This will make every mesh consume a multiple of INDICES_EDGE_INDICES_ALIGNEMENT_SIZE
        // array items for storing the edges of the mesh, and it supports:
        // - a memory optimization of factor INDICES_EDGE_INDICES_ALIGNEMENT_SIZE
        // - in exchange for a small RAM overhead
        //   (by adding some padding until a size that is multiple of INDICES_EDGE_INDICES_ALIGNEMENT_SIZE)

        if (bucket.edgeIndices) {
            const alignedEdgeIndicesLen = Math.ceil((bucket.edgeIndices.length / 2) / INDICES_EDGE_INDICES_ALIGNEMENT_SIZE) * INDICES_EDGE_INDICES_ALIGNEMENT_SIZE * 2;
            dataTextureRamStats.overheadSizeAlignementEdgeIndices += 2 * (alignedEdgeIndicesLen - bucket.edgeIndices.length);
            const alignedEdgeIndices = new Uint32Array(alignedEdgeIndicesLen);
            alignedEdgeIndices.fill(0);
            alignedEdgeIndices.set(bucket.edgeIndices);
            bucket.edgeIndices = alignedEdgeIndices;
        }

        const positionsCompressed = bucket.positionsCompressed;
        const indices = bucket.indices;
        const edgeIndices = bucket.edgeIndices;
        const buffer = this._buffer;

        buffer.positionsCompressed.push(positionsCompressed);
        const vertexBase = buffer.lenPositionsCompressed / 3;
        const numVertices = positionsCompressed.length / 3;
        buffer.lenPositionsCompressed += positionsCompressed.length;

        let indicesBase;
        let numTriangles = 0;
        if (indices) {
            numTriangles = indices.length / 3;
            let indicesBuffer;
            if (numVertices <= (1 << 8)) {
                indicesBuffer = buffer.indices8Bits;
                indicesBase = buffer.lenIndices8Bits / 3;
                buffer.lenIndices8Bits += indices.length;
            } else if (numVertices <= (1 << 16)) {
                indicesBuffer = buffer.indices16Bits;
                indicesBase = buffer.lenIndices16Bits / 3;
                buffer.lenIndices16Bits += indices.length;
            } else {
                indicesBuffer = buffer.indices32Bits;
                indicesBase = buffer.lenIndices32Bits / 3;
                buffer.lenIndices32Bits += indices.length;
            }
            indicesBuffer.push(indices);
        }

        let edgeIndicesBase;
        let numEdges = 0;
        if (edgeIndices) {
            numEdges = edgeIndices.length / 2;
            let edgeIndicesBuffer;
            if (numVertices <= (1 << 8)) {
                edgeIndicesBuffer = buffer.edgeIndices8Bits;
                edgeIndicesBase = buffer.lenEdgeIndices8Bits / 2;
                buffer.lenEdgeIndices8Bits += edgeIndices.length;
            } else if (numVertices <= (1 << 16)) {
                edgeIndicesBuffer = buffer.edgeIndices16Bits;
                edgeIndicesBase = buffer.lenEdgeIndices16Bits / 2;
                buffer.lenEdgeIndices16Bits += edgeIndices.length;
            } else {
                edgeIndicesBuffer = buffer.edgeIndices32Bits;
                edgeIndicesBase = buffer.lenEdgeIndices32Bits / 2;
                buffer.lenEdgeIndices32Bits += edgeIndices.length;
            }
            edgeIndicesBuffer.push(edgeIndices);
        }

        this._state.numVertices += numVertices;

        dataTextureRamStats.numberOfGeometries++;

        const bucketGeometry = {
            vertexBase,
            numVertices,
            numTriangles,
            numEdges,
            indicesBase,
            edgeIndicesBase
        };

        return bucketGeometry;
    }

    _createSubPortion(portionCfg, bucketGeometry, bucket, subPortionAABB) {

        const color = portionCfg.color;
        portionCfg.metallic;
        portionCfg.roughness;
        const colors = portionCfg.colors;
        const opacity = portionCfg.opacity;
        const meshMatrix = portionCfg.meshMatrix;
        const pickColor = portionCfg.pickColor;
        const buffer = this._buffer;
        const state = this._state;

        buffer.perObjectPositionsDecodeMatrices.push(portionCfg.positionsDecodeMatrix);
        buffer.perObjectInstancePositioningMatrices.push(meshMatrix || DEFAULT_MATRIX$1);

        buffer.perObjectSolid.push(!!portionCfg.solid);

        if (colors) {
            buffer.perObjectColors.push([colors[0] * 255, colors[1] * 255, colors[2] * 255, 255]);
        } else if (color) { // Color is pre-quantized by SceneModel
            buffer.perObjectColors.push([color[0], color[1], color[2], opacity]);
        }

        buffer.perObjectPickColors.push(pickColor);
        buffer.perObjectVertexBases.push(bucketGeometry.vertexBase);

        {
            let currentNumIndices;
            if (bucketGeometry.numVertices <= (1 << 8)) {
                currentNumIndices = state.numIndices8Bits;
            } else if (bucketGeometry.numVertices <= (1 << 16)) {
                currentNumIndices = state.numIndices16Bits;
            } else {
                currentNumIndices = state.numIndices32Bits;
            }
            buffer.perObjectIndexBaseOffsets.push(currentNumIndices / 3 - bucketGeometry.indicesBase);
        }

        {
            let currentNumEdgeIndices;
            if (bucketGeometry.numVertices <= (1 << 8)) {
                currentNumEdgeIndices = state.numEdgeIndices8Bits;
            } else if (bucketGeometry.numVertices <= (1 << 16)) {
                currentNumEdgeIndices = state.numEdgeIndices16Bits;
            } else {
                currentNumEdgeIndices = state.numEdgeIndices32Bits;
            }
            buffer.perObjectEdgeIndexBaseOffsets.push(currentNumEdgeIndices / 2 - bucketGeometry.edgeIndicesBase);
        }

        const subPortionId = this._subPortions.length;
        if (bucketGeometry.numTriangles > 0) {
            let numIndices = bucketGeometry.numTriangles * 3;
            let indicesPortionIdBuffer;
            if (bucketGeometry.numVertices <= (1 << 8)) {
                indicesPortionIdBuffer = buffer.perTriangleNumberPortionId8Bits;
                state.numIndices8Bits += numIndices;
                dataTextureRamStats.totalPolygons8Bits += bucketGeometry.numTriangles;
            } else if (bucketGeometry.numVertices <= (1 << 16)) {
                indicesPortionIdBuffer = buffer.perTriangleNumberPortionId16Bits;
                state.numIndices16Bits += numIndices;
                dataTextureRamStats.totalPolygons16Bits += bucketGeometry.numTriangles;
            } else {
                indicesPortionIdBuffer = buffer.perTriangleNumberPortionId32Bits;
                state.numIndices32Bits += numIndices;
                dataTextureRamStats.totalPolygons32Bits += bucketGeometry.numTriangles;
            }
            dataTextureRamStats.totalPolygons += bucketGeometry.numTriangles;
            for (let i = 0; i < bucketGeometry.numTriangles; i += INDICES_EDGE_INDICES_ALIGNEMENT_SIZE) {
                indicesPortionIdBuffer.push(subPortionId);
            }
        }

        if (bucketGeometry.numEdges > 0) {
            let numEdgeIndices = bucketGeometry.numEdges * 2;
            let edgeIndicesPortionIdBuffer;
            if (bucketGeometry.numVertices <= (1 << 8)) {
                edgeIndicesPortionIdBuffer = buffer.perEdgeNumberPortionId8Bits;
                state.numEdgeIndices8Bits += numEdgeIndices;
                dataTextureRamStats.totalEdges8Bits += bucketGeometry.numEdges;
            } else if (bucketGeometry.numVertices <= (1 << 16)) {
                edgeIndicesPortionIdBuffer = buffer.perEdgeNumberPortionId16Bits;
                state.numEdgeIndices16Bits += numEdgeIndices;
                dataTextureRamStats.totalEdges16Bits += bucketGeometry.numEdges;
            } else {
                edgeIndicesPortionIdBuffer = buffer.perEdgeNumberPortionId32Bits;
                state.numEdgeIndices32Bits += numEdgeIndices;
                dataTextureRamStats.totalEdges32Bits += bucketGeometry.numEdges;
            }
            dataTextureRamStats.totalEdges += bucketGeometry.numEdges;
            for (let i = 0; i < bucketGeometry.numEdges; i += INDICES_EDGE_INDICES_ALIGNEMENT_SIZE) {
                edgeIndicesPortionIdBuffer.push(subPortionId);
            }
        }

        //   buffer.perObjectOffsets.push([0, 0, 0]);

        this._subPortions.push({
            // vertsBase: vertsIndex,
            numVertices: bucketGeometry.numTriangles
        });

        if (this.model.scene.readableGeometryEnabled) {
            this._subPortionReadableGeometries[subPortionId] = {
                indices: bucket.indices,
                positionsCompressed: bucket.positionsCompressed,
                positionsDecodeMatrix: portionCfg.positionsDecodeMatrix,
                meshMatrix: portionCfg.meshMatrix
            };
        }

        this._numPortions++;

        dataTextureRamStats.numberOfPortions++;

        return subPortionId;
    }

    /**
     * Builds data textures from the appended geometries and loads them into the GPU.
     *
     * No more portions can then be created.
     */
    finalize() {

        if (this._finalized) {
            return;
        }

        const state = this._state;
        const textureState = this._dtxState;
        const gl = this.model.scene.canvas.gl;
        const buffer = this._buffer;

        state.gl = gl;
        textureState.texturePerObjectColorsAndFlags = this._dtxTextureFactory.createTextureForColorsAndFlags(
            gl,
            buffer.perObjectColors,
            buffer.perObjectPickColors,
            buffer.perObjectVertexBases,
            buffer.perObjectIndexBaseOffsets,
            buffer.perObjectEdgeIndexBaseOffsets,
            buffer.perObjectSolid);

        textureState.texturePerObjectInstanceMatrices
            = this._dtxTextureFactory.createTextureForInstancingMatrices(gl, buffer.perObjectInstancePositioningMatrices);

        textureState.texturePerObjectPositionsDecodeMatrix
            = this._dtxTextureFactory.createTextureForPositionsDecodeMatrices(
            gl,
            buffer.perObjectPositionsDecodeMatrices);

        textureState.texturePerVertexIdCoordinates = this._dtxTextureFactory.createTextureForPositions(
            gl,
            buffer.positionsCompressed,
            buffer.lenPositionsCompressed);

        textureState.texturePerPolygonIdPortionIds8Bits = this._dtxTextureFactory.createTextureForPackedPortionIds(
            gl,
            buffer.perTriangleNumberPortionId8Bits);

        textureState.texturePerPolygonIdPortionIds16Bits = this._dtxTextureFactory.createTextureForPackedPortionIds(
            gl,
            buffer.perTriangleNumberPortionId16Bits);

        textureState.texturePerPolygonIdPortionIds32Bits = this._dtxTextureFactory.createTextureForPackedPortionIds(
            gl,
            buffer.perTriangleNumberPortionId32Bits);

        if (buffer.perEdgeNumberPortionId8Bits.length > 0) {
            textureState.texturePerEdgeIdPortionIds8Bits = this._dtxTextureFactory.createTextureForPackedPortionIds(
                gl,
                buffer.perEdgeNumberPortionId8Bits);
        }

        if (buffer.perEdgeNumberPortionId16Bits.length > 0) {
            textureState.texturePerEdgeIdPortionIds16Bits = this._dtxTextureFactory.createTextureForPackedPortionIds(
                gl,
                buffer.perEdgeNumberPortionId16Bits);
        }

        if (buffer.perEdgeNumberPortionId32Bits.length > 0) {
            textureState.texturePerEdgeIdPortionIds32Bits = this._dtxTextureFactory.createTextureForPackedPortionIds(
                gl,
                buffer.perEdgeNumberPortionId32Bits);
        }

        if (buffer.lenIndices8Bits > 0) {
            textureState.texturePerPolygonIdIndices8Bits = this._dtxTextureFactory.createTextureFor8BitIndices(
                gl,
                buffer.indices8Bits, buffer.lenIndices8Bits);
        }

        if (buffer.lenIndices16Bits > 0) {
            textureState.texturePerPolygonIdIndices16Bits = this._dtxTextureFactory.createTextureFor16BitIndices(
                gl,
                buffer.indices16Bits, buffer.lenIndices16Bits);
        }

        if (buffer.lenIndices32Bits > 0) {
            textureState.texturePerPolygonIdIndices32Bits = this._dtxTextureFactory.createTextureFor32BitIndices(
                gl,
                buffer.indices32Bits, buffer.lenIndices32Bits);
        }

        if (buffer.lenEdgeIndices8Bits > 0) {
            textureState.texturePerPolygonIdEdgeIndices8Bits = this._dtxTextureFactory.createTextureFor8BitsEdgeIndices(
                gl,
                buffer.edgeIndices8Bits, buffer.lenEdgeIndices8Bits);
        }

        if (buffer.lenEdgeIndices16Bits > 0) {
            textureState.texturePerPolygonIdEdgeIndices16Bits = this._dtxTextureFactory.createTextureFor16BitsEdgeIndices(
                gl,
                buffer.edgeIndices16Bits, buffer.lenEdgeIndices16Bits);
        }

        if (buffer.lenEdgeIndices32Bits > 0) {
            textureState.texturePerPolygonIdEdgeIndices32Bits = this._dtxTextureFactory.createTextureFor32BitsEdgeIndices(
                gl,
                buffer.edgeIndices32Bits, buffer.lenEdgeIndices32Bits);
        }

        textureState.finalize();

        // Free up memory
        this._buffer = null;
        this._bucketGeometries = {};
        this._finalized = true;
        this._deferredSetFlagsDirty = false; //

        this._onSceneRendering = this.model.scene.on("rendering", () => {
            if (this._deferredSetFlagsDirty) {
                this._uploadDeferredFlags();
            }
            this._numUpdatesInFrame = 0;
        });
    }

    isEmpty() {
        return this._numPortions === 0;
    }

    initFlags(portionId, flags, meshTransparent) {
        if (flags & ENTITY_FLAGS.VISIBLE) {
            this._numVisibleLayerPortions++;
            this.model.numVisibleLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.HIGHLIGHTED) {
            this._numHighlightedLayerPortions++;
            this.model.numHighlightedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.XRAYED) {
            this._numXRayedLayerPortions++;
            this.model.numXRayedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.SELECTED) {
            this._numSelectedLayerPortions++;
            this.model.numSelectedLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.CLIPPABLE) {
            this._numClippableLayerPortions++;
            this.model.numClippableLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.EDGES) {
            this._numEdgesLayerPortions++;
            this.model.numEdgesLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.PICKABLE) {
            this._numPickableLayerPortions++;
            this.model.numPickableLayerPortions++;
        }
        if (flags & ENTITY_FLAGS.CULLED) {
            this._numCulledLayerPortions++;
            this.model.numCulledLayerPortions++;
        }
        if (meshTransparent) {
            this._numTransparentLayerPortions++;
            this.model.numTransparentLayerPortions++;
        }
        const deferred = true;
        this._setFlags(portionId, flags, meshTransparent, deferred);
        this._setFlags2(portionId, flags, deferred);
    }

    flushInitFlags() {
        this._setDeferredFlags();
        this._setDeferredFlags2();
    }

    setVisible(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.VISIBLE) {
            this._numVisibleLayerPortions++;
            this.model.numVisibleLayerPortions++;
        } else {
            this._numVisibleLayerPortions--;
            this.model.numVisibleLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setHighlighted(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.HIGHLIGHTED) {
            this._numHighlightedLayerPortions++;
            this.model.numHighlightedLayerPortions++;
        } else {
            this._numHighlightedLayerPortions--;
            this.model.numHighlightedLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setXRayed(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.XRAYED) {
            this._numXRayedLayerPortions++;
            this.model.numXRayedLayerPortions++;
        } else {
            this._numXRayedLayerPortions--;
            this.model.numXRayedLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setSelected(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.SELECTED) {
            this._numSelectedLayerPortions++;
            this.model.numSelectedLayerPortions++;
        } else {
            this._numSelectedLayerPortions--;
            this.model.numSelectedLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setEdges(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.EDGES) {
            this._numEdgesLayerPortions++;
            this.model.numEdgesLayerPortions++;
        } else {
            this._numEdgesLayerPortions--;
            this.model.numEdgesLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setClippable(portionId, flags) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.CLIPPABLE) {
            this._numClippableLayerPortions++;
            this.model.numClippableLayerPortions++;
        } else {
            this._numClippableLayerPortions--;
            this.model.numClippableLayerPortions--;
        }
        this._setFlags2(portionId, flags);
    }

    /**
     * This will _start_ a "set-flags transaction".
     *
     * After invoking this method, calling setFlags/setFlags2 will not update
     * the colors+flags texture but only store the new flags/flag2 in the
     * colors+flags texture data array.
     *
     * After invoking this method, and when all desired setFlags/setFlags2 have
     * been called on needed portions of the layer, invoke `_uploadDeferredFlags`
     * to actually upload the data array into the texture.
     *
     * In massive "set-flags" scenarios like VFC or LOD mechanisms, the combination of
     * `_beginDeferredFlags` + `_uploadDeferredFlags`brings a speed-up of
     * up to 80x when e.g. objects are massively (un)culled 🚀.
     */
    _beginDeferredFlags() {
        this._deferredSetFlagsActive = true;
    }

    /**
     * This will _commit_ a "set-flags transaction".
     *
     * Invoking this method will update the colors+flags texture data with new
     * flags/flags2 set since the previous invocation of `_beginDeferredFlags`.
     */
    _uploadDeferredFlags() {
        this._deferredSetFlagsActive = false;
        if (!this._deferredSetFlagsDirty) {
            return;
        }
        this._deferredSetFlagsDirty = false;
        const gl = this.model.scene.canvas.gl;
        const textureState = this._dtxState;
        gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectColorsAndFlags._texture);
        gl.texSubImage2D(
            gl.TEXTURE_2D,
            0, // level
            0, // xoffset
            0, // yoffset
            textureState.texturePerObjectColorsAndFlags._textureWidth, // width
            textureState.texturePerObjectColorsAndFlags._textureHeight, // width
            gl.RGBA_INTEGER,
            gl.UNSIGNED_BYTE,
            textureState.texturePerObjectColorsAndFlags._textureData
        );
        // gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectInstanceMatrices._texture);
        // gl.texSubImage2D(
        //     gl.TEXTURE_2D,
        //     0, // level
        //     0, // xoffset
        //     0, // yoffset
        //     textureState.texturePerObjectInstanceMatrices._textureWidth, // width
        //     textureState.texturePerObjectInstanceMatrices._textureHeight, // width
        //     gl.RGB,
        //     gl.FLOAT,
        //     textureState.texturePerObjectInstanceMatrices._textureData
        // );
    }

    setCulled(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.CULLED) {
            this._numCulledLayerPortions += this._portionToSubPortionsMap[portionId].length;
            this.model.numCulledLayerPortions++;
        } else {
            this._numCulledLayerPortions -= this._portionToSubPortionsMap[portionId].length;
            this.model.numCulledLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setCollidable(portionId, flags) {
        if (!this._finalized) {
            throw "Not finalized";
        }
    }

    setPickable(portionId, flags, transparent) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        if (flags & ENTITY_FLAGS.PICKABLE) {
            this._numPickableLayerPortions++;
            this.model.numPickableLayerPortions++;
        } else {
            this._numPickableLayerPortions--;
            this.model.numPickableLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    setColor(portionId, color) {
        const subPortionIds = this._portionToSubPortionsMap[portionId];
        for (let i = 0, len = subPortionIds.length; i < len; i++) {
            this._subPortionSetColor(subPortionIds[i], color);
        }
    }

    _subPortionSetColor(subPortionId, color) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        // Color
        const textureState = this._dtxState;
        const gl = this.model.scene.canvas.gl;
        tempUint8Array4 [0] = color[0];
        tempUint8Array4 [1] = color[1];
        tempUint8Array4 [2] = color[2];
        tempUint8Array4 [3] = color[3];
        // object colors
        textureState.texturePerObjectColorsAndFlags._textureData.set(tempUint8Array4, subPortionId * 32);
        if (this._deferredSetFlagsActive) {
            //console.info("_subPortionSetColor defer");
            this._deferredSetFlagsDirty = true;
            return;
        }
        if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE) {
            this._beginDeferredFlags(); // Subsequent flags updates now deferred
        }
        //console.info("_subPortionSetColor write through");
        gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectColorsAndFlags._texture);
        gl.texSubImage2D(
            gl.TEXTURE_2D,
            0, // level
            (subPortionId % 512) * 8, // xoffset
            Math.floor(subPortionId / 512), // yoffset
            1, // width
            1, //height
            gl.RGBA_INTEGER,
            gl.UNSIGNED_BYTE,
            tempUint8Array4
        );
        // gl.bindTexture (gl.TEXTURE_2D, null);
    }

    setTransparent(portionId, flags, transparent) {
        if (transparent) {
            this._numTransparentLayerPortions++;
            this.model.numTransparentLayerPortions++;
        } else {
            this._numTransparentLayerPortions--;
            this.model.numTransparentLayerPortions--;
        }
        this._setFlags(portionId, flags, transparent);
    }

    _setFlags(portionId, flags, transparent, deferred = false) {
        const subPortionIds = this._portionToSubPortionsMap[portionId];
        for (let i = 0, len = subPortionIds.length; i < len; i++) {
            this._subPortionSetFlags(subPortionIds[i], flags, transparent, deferred);
        }
    }

    _subPortionSetFlags(subPortionId, flags, transparent, deferred = false) {
        if (!this._finalized) {
            throw "Not finalized";
        }

        const visible = !!(flags & ENTITY_FLAGS.VISIBLE);
        const xrayed = !!(flags & ENTITY_FLAGS.XRAYED);
        const highlighted = !!(flags & ENTITY_FLAGS.HIGHLIGHTED);
        const selected = !!(flags & ENTITY_FLAGS.SELECTED);
        const edges = !!(flags & ENTITY_FLAGS.EDGES);
        const pickable = !!(flags & ENTITY_FLAGS.PICKABLE);
        const culled = !!(flags & ENTITY_FLAGS.CULLED);

        // Color

        let f0;
        if (!visible || culled || xrayed
            || (highlighted && !this.model.scene.highlightMaterial.glowThrough)
            || (selected && !this.model.scene.selectedMaterial.glowThrough)) {
            f0 = RENDER_PASSES.NOT_RENDERED;
        } else {
            if (transparent) {
                f0 = RENDER_PASSES.COLOR_TRANSPARENT;
            } else {
                f0 = RENDER_PASSES.COLOR_OPAQUE;
            }
        }

        // Silhouette

        let f1;
        if (!visible || culled) {
            f1 = RENDER_PASSES.NOT_RENDERED;
        } else if (selected) {
            f1 = RENDER_PASSES.SILHOUETTE_SELECTED;
        } else if (highlighted) {
            f1 = RENDER_PASSES.SILHOUETTE_HIGHLIGHTED;
        } else if (xrayed) {
            f1 = RENDER_PASSES.SILHOUETTE_XRAYED;
        } else {
            f1 = RENDER_PASSES.NOT_RENDERED;
        }

        // Edges

        let f2 = 0;
        if (!visible || culled) {
            f2 = RENDER_PASSES.NOT_RENDERED;
        } else if (selected) {
            f2 = RENDER_PASSES.EDGES_SELECTED;
        } else if (highlighted) {
            f2 = RENDER_PASSES.EDGES_HIGHLIGHTED;
        } else if (xrayed) {
            f2 = RENDER_PASSES.EDGES_XRAYED;
        } else if (edges) {
            if (transparent) {
                f2 = RENDER_PASSES.EDGES_COLOR_TRANSPARENT;
            } else {
                f2 = RENDER_PASSES.EDGES_COLOR_OPAQUE;
            }
        } else {
            f2 = RENDER_PASSES.NOT_RENDERED;
        }

        // Pick

        let f3 = (visible && (!culled) && pickable) ? RENDER_PASSES.PICK : RENDER_PASSES.NOT_RENDERED;
        const textureState = this._dtxState;
        const gl = this.model.scene.canvas.gl;
        tempUint8Array4 [0] = f0;
        tempUint8Array4 [1] = f1;
        tempUint8Array4 [2] = f2;
        tempUint8Array4 [3] = f3;
        // object flags
        textureState.texturePerObjectColorsAndFlags._textureData.set(tempUint8Array4, subPortionId * 32 + 8);
        if (this._deferredSetFlagsActive || deferred) {
            this._deferredSetFlagsDirty = true;
            return;
        }
        if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE) {
            this._beginDeferredFlags(); // Subsequent flags updates now deferred
        }
        gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectColorsAndFlags._texture);
        gl.texSubImage2D(
            gl.TEXTURE_2D,
            0, // level
            (subPortionId % 512) * 8 + 2, // xoffset
            Math.floor(subPortionId / 512), // yoffset
            1, // width
            1, //height
            gl.RGBA_INTEGER,
            gl.UNSIGNED_BYTE,
            tempUint8Array4
        );
        // gl.bindTexture (gl.TEXTURE_2D, null);
    }

    _setDeferredFlags() {
    }

    _setFlags2(portionId, flags, deferred = false) {
        const subPortionIds = this._portionToSubPortionsMap[portionId];
        for (let i = 0, len = subPortionIds.length; i < len; i++) {
            this._subPortionSetFlags2(subPortionIds[i], flags, deferred);
        }
    }

    _subPortionSetFlags2(subPortionId, flags, deferred = false) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        const clippable = !!(flags & ENTITY_FLAGS.CLIPPABLE) ? 255 : 0;
        const textureState = this._dtxState;
        const gl = this.model.scene.canvas.gl;
        tempUint8Array4 [0] = clippable;
        tempUint8Array4 [1] = 0;
        tempUint8Array4 [2] = 1;
        tempUint8Array4 [3] = 2;
        // object flags2
        textureState.texturePerObjectColorsAndFlags._textureData.set(tempUint8Array4, subPortionId * 32 + 12);
        if (this._deferredSetFlagsActive || deferred) {
            // console.log("_subPortionSetFlags2 set flags defer");
            this._deferredSetFlagsDirty = true;
            return;
        }
        if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE) {
            this._beginDeferredFlags(); // Subsequent flags updates now deferred
        }
        gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectColorsAndFlags._texture);
        gl.texSubImage2D(
            gl.TEXTURE_2D,
            0, // level
            (subPortionId % 512) * 8 + 3, // xoffset
            Math.floor(subPortionId / 512), // yoffset
            1, // width
            1, //height
            gl.RGBA_INTEGER,
            gl.UNSIGNED_BYTE,
            tempUint8Array4
        );
        // gl.bindTexture (gl.TEXTURE_2D, null);
    }

    _setDeferredFlags2() {
    }

    setOffset(portionId, offset) {
        const subPortionIds = this._portionToSubPortionsMap[portionId];
        for (let i = 0, len = subPortionIds.length; i < len; i++) {
            this._subPortionSetOffset(subPortionIds[i], offset);
        }
    }

    _subPortionSetOffset(subPortionId, offset) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        // if (!this.model.scene.entityOffsetsEnabled) {
        //     this.model.error("Entity#offset not enabled for this Viewer"); // See Viewer entityOffsetsEnabled
        //     return;
        // }
        const textureState = this._dtxState;
        const gl = this.model.scene.canvas.gl;
        tempFloat32Array3 [0] = offset[0];
        tempFloat32Array3 [1] = offset[1];
        tempFloat32Array3 [2] = offset[2];
        // object offset
        textureState.texturePerObjectOffsets._textureData.set(tempFloat32Array3, subPortionId * 3);
        if (this._deferredSetFlagsActive) {
            this._deferredSetFlagsDirty = true;
            return;
        }
        if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE) {
            this._beginDeferredFlags(); // Subsequent flags updates now deferred
        }
        gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectOffsets._texture);
        gl.texSubImage2D(
            gl.TEXTURE_2D,
            0, // level
            0, // x offset
            subPortionId, // yoffset
            1, // width
            1, // height
            gl.RGB,
            gl.FLOAT,
            tempFloat32Array3
        );
        // gl.bindTexture (gl.TEXTURE_2D, null);
    }

    setMatrix(portionId, matrix) {
        const subPortionIds = this._portionToSubPortionsMap[portionId];
        for (let i = 0, len = subPortionIds.length; i < len; i++) {
            this._subPortionSetMatrix(subPortionIds[i], matrix);
        }
    }

    _subPortionSetMatrix(subPortionId, matrix) {
        if (!this._finalized) {
            throw "Not finalized";
        }
        // if (!this.model.scene.entityMatrixsEnabled) {
        //     this.model.error("Entity#matrix not enabled for this Viewer"); // See Viewer entityMatrixsEnabled
        //     return;
        // }
        const textureState = this._dtxState;
        const gl = this.model.scene.canvas.gl;
        tempMat4a.set(matrix);
        textureState.texturePerObjectInstanceMatrices._textureData.set(tempMat4a, subPortionId * 16);
        if (this._deferredSetFlagsActive) {
            this._deferredSetFlagsDirty = true;
            return;
        }
        if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE) {
            this._beginDeferredFlags(); // Subsequent flags updates now deferred
        }
        gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectInstanceMatrices._texture);
        gl.texSubImage2D(
            gl.TEXTURE_2D,
            0, // level
            (subPortionId % 512) * 4, // xoffset
            Math.floor(subPortionId / 512), // yoffset
            // 1,
            4, // width
            1, // height
            gl.RGBA,
            gl.FLOAT,
            tempMat4a
        );
        // gl.bindTexture (gl.TEXTURE_2D, null);
    }

    getEachVertex(portionId, callback) {
        if (!this.model.scene.readableGeometryEnabled) {
            return;
        }
        const state = this._state;
        const subPortionIds = this._portionToSubPortionsMap[portionId];
        if (!subPortionIds) {
            this.model.error("portion not found: " + portionId);
            return;
        }
        for (let i = 0, len = subPortionIds.length; i < len; i++) {
            const subPortionId = subPortionIds[i];
            const subPortionReadableGeometry = this._subPortionReadableGeometries[subPortionId];
            const positions = subPortionReadableGeometry.positionsCompressed;
            const positionsDecodeMatrix = subPortionReadableGeometry.positionsDecodeMatrix;
            subPortionReadableGeometry.meshMatrix;
            const origin = state.origin;
            const offsetX = origin[0] ;
            const offsetY = origin[1] ;
            const offsetZ = origin[2] ;
            const worldPos = tempVec4a$a;
            for (let i = 0, len = positions.length; i < len; i += 3) {
                worldPos[0] = positions[i];
                worldPos[1] = positions[i + 1];
                worldPos[2] = positions[i + 2];
                worldPos[3] = 1.0;
                math.decompressPosition(worldPos, positionsDecodeMatrix);
                math.mulMat4v4(this.model.worldMatrix, worldPos, worldPos);
                worldPos[0] += offsetX;
                worldPos[1] += offsetY;
                worldPos[2] += offsetZ;
                callback(worldPos);
            }
        }
    }

    getEachIndex(portionId, callback) {
        if (!this.model.scene.readableGeometryEnabled) {
            return;
        }
        const subPortionIds = this._portionToSubPortionsMap[portionId];
        if (!subPortionIds) {
            this.model.error("portion not found: " + portionId);
            return;
        }
        for (let i = 0, len = subPortionIds.length; i < len; i++) {
            const subPortionId = subPortionIds[i];
            const subPortionReadableGeometry = this._subPortionReadableGeometries[subPortionId];
            const indices = subPortionReadableGeometry.indices;
            for (let i = 0, len = indices.length; i < len; i++) {
                callback(indices[i]);
            }
        }
    }


    // ---------------------- COLOR RENDERING -----------------------------------

    drawColorOpaque(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (frameCtx.withSAO && this.model.saoEnabled) {
            if (this._renderers.colorRendererWithSAO) {
                this._renderers.colorRendererWithSAO.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
            }
        } else {
            if (this._renderers.colorRenderer) {
                this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
            }
        }
    }

    _updateBackfaceCull(renderFlags, frameCtx) {
        const backfaces = true; // See XCD-230
        if (frameCtx.backfaces !== backfaces) {
            const gl = frameCtx.gl;
            {
                gl.disable(gl.CULL_FACE);
            }
            frameCtx.backfaces = backfaces;
        }
    }

    drawColorTransparent(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === 0 || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.colorRenderer) {
            this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT);
        }
    }

    // ---------------------- RENDERING SAO POST EFFECT TARGETS --------------

    drawDepth(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.depthRenderer) {
            this._renderers.depthRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); // Assume whatever post-effect uses depth (eg SAO) does not apply to transparent objects
        }
    }

    drawNormals(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.normalsRenderer) {
            this._renderers.normalsRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);  // Assume whatever post-effect uses normals (eg SAO) does not apply to transparent objects
        }
    }

    // ---------------------- SILHOUETTE RENDERING -----------------------------------

    drawSilhouetteXRayed(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_XRAYED);
        }
    }

    drawSilhouetteHighlighted(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_HIGHLIGHTED);
        }
    }

    drawSilhouetteSelected(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.silhouetteRenderer) {
            this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_SELECTED);
        }
    }

    // ---------------------- EDGES RENDERING -----------------------------------

    drawEdgesColorOpaque(renderFlags, frameCtx) {
        if (this.model.scene.logarithmicDepthBufferEnabled) {
            if (!this.model.scene._loggedWarning) {
                console.log("Edge enhancement for SceneModel data texture layers currently disabled with logarithmic depth buffer");
                this.model.scene._loggedWarning = true;
            }
            return;
        }
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numEdgesLayerPortions === 0) {
            return;
        }
        if (this._renderers.edgesColorRenderer) {
            this._renderers.edgesColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_COLOR_OPAQUE);
        }
    }

    drawEdgesColorTransparent(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numEdgesLayerPortions === 0 || this._numTransparentLayerPortions === 0) {
            return;
        }
        if (this._renderers.edgesColorRenderer) {
            this._renderers.edgesColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_COLOR_TRANSPARENT);
        }
    }

    drawEdgesHighlighted(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) {
            return;
        }
        if (this._renderers.edgesRenderer) {
            this._renderers.edgesRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_HIGHLIGHTED);
        }
    }

    drawEdgesSelected(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) {
            return;
        }
        if (this._renderers.edgesRenderer) {
            this._renderers.edgesRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_SELECTED);
        }
    }

    drawEdgesXRayed(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) {
            return;
        }
        if (this._renderers.edgesRenderer) {
            this._renderers.edgesRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_XRAYED);
        }
    }

    // ---------------------- OCCLUSION CULL RENDERING -----------------------------------

    drawOcclusion(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.occlusionRenderer) {
            this._renderers.occlusionRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
        }
    }

    // ---------------------- SHADOW BUFFER RENDERING -----------------------------------

    drawShadow(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.shadowRenderer) {
            this._renderers.shadowRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE);
        }
    }

    //---- PICKING ----------------------------------------------------------------------------------------------------

    setPickMatrices(pickViewMatrix, pickProjMatrix) {
        // if (this._numVisibleLayerPortions === 0) {
        //     return;
        // }
        // this._dtxState.texturePickCameraMatrices.updateViewMatrix(pickViewMatrix, pickProjMatrix);
    }

    drawPickMesh(renderFlags, frameCtx) {
        if (this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.pickMeshRenderer) {
            this._renderers.pickMeshRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawPickDepths(renderFlags, frameCtx) {
        if (this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.pickDepthRenderer) {
            this._renderers.pickDepthRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawSnapInit(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.snapInitRenderer) {
            this._renderers.snapInitRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawSnap(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.snapRenderer) {
            this._renderers.snapRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    drawPickNormals(renderFlags, frameCtx) {
        if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) {
            return;
        }
        this._updateBackfaceCull(renderFlags, frameCtx);
        if (this._renderers.pickNormalsRenderer) {
            this._renderers.pickNormalsRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK);
        }
    }

    destroy() {
        if (this._destroyed) {
            return;
        }
        const state = this._state;
        if (state.metallicRoughnessBuf) {
            state.metallicRoughnessBuf.destroy();
            state.metallicRoughnessBuf = null;
        }
        this.model.scene.off(this._onSceneRendering);
        state.destroy();
        this._destroyed = true;
    }
}

/**
 * A texture set within a {@link SceneModel}.
 *
 * * Created with {@link SceneModel#createTextureSet}
 * * Belongs to many {@link SceneModelMesh}es
 * * Stored by ID in {@link SceneModel#textureSets}
 * * Referenced by {@link SceneModelMesh#textureSet}
 */
class SceneModelTextureSet {

    /**
     * @private
     */
    constructor(cfg) {

        /**
         * Unique ID of this SceneModelTextureSet.
         *
         * The SceneModelTextureSet is registered against this ID in {@link SceneModel#textureSets}.
         */
        this.id = cfg.id;

        /**
         * The color texture.
         * @type {SceneModelTexture|*}
         */
        this.colorTexture = cfg.colorTexture;

        /**
         * The alpha cutoff [float]
         */
        this.alphaCutoff = cfg.alphaCutoff;

        /**
         * The metallic-roughness texture.
         * @type {SceneModelTexture|*}
         */
        this.metallicRoughnessTexture = cfg.metallicRoughnessTexture;

        /**
         * The normal map texture.
         * @type {SceneModelTexture|*}
         */
        this.normalsTexture = cfg.normalsTexture;

        /**
         * The emissive color texture.
         * @type {SceneModelTexture|*}
         */
        this.emissiveTexture = cfg.emissiveTexture;

        /**
         * The ambient occlusion texture.
         * @type {SceneModelTexture|*}
         */
        this.occlusionTexture = cfg.occlusionTexture;
    }

    /**
     * @private
     */
    destroy() {
    }
}

/**
 * A texture within a {@link SceneModelTextureSet}.
 *
 * * Created with {@link SceneModel#createTexture}
 * * Belongs to many {@link SceneModelTextureSet}s
 * * Stored by ID in {@link SceneModel#textures}}
 */
class SceneModelTexture {

    /**
     * @private
     * @param cfg
     */
    constructor(cfg) {

        /**
         * Unique ID of this SceneModelTexture.
         *
         * The SceneModelTexture is registered against this ID in {@link SceneModel#textures}.
         */
        this.id = cfg.id;

        /**
         * @private
         */
        this.texture = cfg.texture;
    }

    /**
     * @private
     */
    destroy() {
        if (this.texture) {
            this.texture.destroy();
            this.texture = null;
        }
    }
}

const Cache$1 = {

    enabled: false,
    files: {},

    add: function (key, file) {
        if (this.enabled === false) {
            return;
        }
        this.files[key] = file;
    },

    get: function (key) {
        if (this.enabled === false) {
            return;
        }
        return this.files[key];
    },

    remove: function (key) {
        delete this.files[key];
    },

    clear: function () {
        this.files = {};
    }
};

class LoadingManager {

    constructor(onLoad, onProgress, onError) {

        this.isLoading = false;
        this.itemsLoaded = 0;
        this.itemsTotal = 0;
        this.urlModifier = undefined;
        this.handlers = [];

        this.onStart = undefined;
        this.onLoad = onLoad;
        this.onProgress = onProgress;
        this.onError = onError;
    }

    itemStart(url) {
        this.itemsTotal++;
        if (this.isLoading === false) {
            if (this.onStart !== undefined) {
                this.onStart(url, this.itemsLoaded, this.itemsTotal);
            }
        }
        this.isLoading = true;
    }

    itemEnd(url) {
        this.itemsLoaded++;
        if (this.onProgress !== undefined) {
            this.onProgress(url, this.itemsLoaded, this.itemsTotal);
        }
        if (this.itemsLoaded === this.itemsTotal) {
            this.isLoading = false;
            if (this.onLoad !== undefined) {
                this.onLoad();
            }
        }
    }

    itemError(url) {
        if (this.onError !== undefined) {
            this.onError(url);
        }
    }

    resolveURL(url) {
        if (this.urlModifier) {
            return this.urlModifier(url);
        }
        return url;
    }

    setURLModifier(transform) {
        this.urlModifier = transform;
        return this;
    }

    addHandler(regex, loader) {
        this.handlers.push(regex, loader);
        return this;
    }

    removeHandler(regex) {
        const index = this.handlers.indexOf(regex);
        if (index !== -1) {
            this.handlers.splice(index, 2);
        }
        return this;
    }

    getHandler(file) {
        for (let i = 0, l = this.handlers.length; i < l; i += 2) {
            const regex = this.handlers[i];
            const loader = this.handlers[i + 1];
            if (regex.global) regex.lastIndex = 0; // see #17920
            if (regex.test(file)) {
                return loader;
            }
        }
        return null;
    }
}

const DefaultLoadingManager = new LoadingManager();

class Loader {

    constructor(manager) {

        this.manager = (manager !== undefined) ? manager : DefaultLoadingManager;

        this.crossOrigin = 'anonymous';
        this.withCredentials = false;
        this.path = '';
        this.resourcePath = '';
        this.requestHeader = {};
    }

    load( /* url, onLoad, onProgress, onError */) {
    }

    loadAsync(url, onProgress) {
        const scope = this;
        return new Promise(function (resolve, reject) {
            scope.load(url, resolve, onProgress, reject);
        });
    }

    parse( /* data */) {
    }

    setCrossOrigin(crossOrigin) {
        this.crossOrigin = crossOrigin;
        return this;
    }

    setWithCredentials(value) {
        this.withCredentials = value;
        return this;
    }

    setPath(path) {
        this.path = path;
        return this;
    }

    setResourcePath(resourcePath) {
        this.resourcePath = resourcePath;
        return this;
    }

    setRequestHeader(requestHeader) {
        this.requestHeader = requestHeader;
        return this;
    }
}

const loading = {};

class FileLoader extends Loader {

    constructor(manager) {
        super(manager);
    }

    load(url, onLoad, onProgress, onError) {
        if (url === undefined) {
            url = '';
        }
        if (this.path !== undefined) {
            url = this.path + url;
        }
        url = this.manager.resolveURL(url);
        const cached = Cache$1.get(url);
        if (cached !== undefined) {
            this.manager.itemStart(url);
            core.scheduleTask(() => {
                if (onLoad) {
                    onLoad(cached);
                }
                this.manager.itemEnd(url);
            }, 0);
            return cached;
        }
        if (loading[url] !== undefined) {
            loading[url].push({onLoad, onProgress, onError});
            return;
        }
        loading[url] = [];
        loading[url].push({onLoad, onProgress, onError});
        const req = new Request(url, {
            headers: new Headers(this.requestHeader),
            credentials: this.withCredentials ? 'include' : 'same-origin'
        });
        const mimeType = this.mimeType;
        const responseType = this.responseType;
        fetch(req).then(response => {
            if (response.status === 200 || response.status === 0) {
                // Some browsers return HTTP Status 0 when using non-http protocol
                // e.g. 'file://' or 'data://'. Handle as success.
                if (response.status === 0) {
                    console.warn('FileLoader: HTTP Status 0 received.');
                }
                if (typeof ReadableStream === 'undefined' || response.body.getReader === undefined) {
                    return response;
                }
                const callbacks = loading[url];
                const reader = response.body.getReader();
                const contentLength = response.headers.get('Content-Length');
                const total = contentLength ? parseInt(contentLength) : 0;
                const lengthComputable = total !== 0;
                let loaded = 0;
                const stream = new ReadableStream({
                    start(controller) {
                        readData();

                        function readData() {
                            reader.read().then(({done, value}) => {
                                if (done) {
                                    controller.close();
                                } else {
                                    loaded += value.byteLength;
                                    const event = new ProgressEvent('progress', {lengthComputable, loaded, total});
                                    for (let i = 0, il = callbacks.length; i < il; i++) {
                                        const callback = callbacks[i];
                                        if (callback.onProgress) {
                                            callback.onProgress(event);
                                        }
                                    }
                                    controller.enqueue(value);
                                    readData();
                                }
                            });
                        }
                    }
                });
                return new Response(stream);
            } else {
                throw Error(`fetch for "${response.url}" responded with ${response.status}: ${response.statusText}`);
            }
        }).then(response => {
            switch (responseType) {
                case 'arraybuffer':
                    return response.arrayBuffer();
                case 'blob':
                    return response.blob();
                case 'document':
                    return response.text()
                        .then(text => {
                            const parser = new DOMParser();
                            return parser.parseFromString(text, mimeType);
                        });
                case 'json':
                    return response.json();
                default:
                    if (mimeType === undefined) {
                        return response.text();
                    } else {
                        // sniff encoding
                        const re = /charset="?([^;"\s]*)"?/i;
                        const exec = re.exec(mimeType);
                        const label = exec && exec[1] ? exec[1].toLowerCase() : undefined;
                        const decoder = new TextDecoder(label);
                        return response.arrayBuffer().then(ab => decoder.decode(ab));
                    }
            }
        }).then(data => {
            // Add to cache only on HTTP success, so that we do not cache
            // error response bodies as proper responses to requests.
            Cache$1.add(url, data);
            const callbacks = loading[url];
            delete loading[url];
            for (let i = 0, il = callbacks.length; i < il; i++) {
                const callback = callbacks[i];
                if (callback.onLoad) {
                    callback.onLoad(data);
                }
            }
        }).catch(err => {
            // Abort errors and other errors are handled the same
            const callbacks = loading[url];
            if (callbacks === undefined) {
                // When onLoad was called and url was deleted in `loading`
                this.manager.itemError(url);
                throw err;

            }
            delete loading[url];
            for (let i = 0, il = callbacks.length; i < il; i++) {
                const callback = callbacks[i];
                if (callback.onError) {
                    callback.onError(err);
                }
            }
            this.manager.itemError(url);
        }).finally(() => {
            this.manager.itemEnd(url);
        });
        this.manager.itemStart(url);
    }

    setResponseType(value) {
        this.responseType = value;
        return this;
    }

    setMimeType(value) {
        this.mimeType = value;
        return this;
    }
}

/**
 * @author Deepkolos / https://github.com/deepkolos
 */

class WorkerPool$1 {

    constructor(pool = 4) {
        this.pool = pool;
        this.queue = [];
        this.workers = [];
        this.workersResolve = [];
        this.workerStatus = 0;
    }

    _initWorker(workerId) {
        if (!this.workers[workerId]) {
            const worker = this.workerCreator();
            worker.addEventListener('message', this._onMessage.bind(this, workerId));
            this.workers[workerId] = worker;
        }
    }

    _getIdleWorker() {
        for (let i = 0; i < this.pool; i++)
            if (!(this.workerStatus & (1 << i))) return i;
        return -1;
    }

    _onMessage(workerId, msg) {
        const resolve = this.workersResolve[workerId];
        resolve && resolve(msg);
        if (this.queue.length) {
            const {resolve, msg, transfer} = this.queue.shift();
            this.workersResolve[workerId] = resolve;
            this.workers[workerId].postMessage(msg, transfer);
        } else {
            this.workerStatus ^= 1 << workerId;
        }
    }

    setWorkerCreator(workerCreator) {
        this.workerCreator = workerCreator;
    }

    setWorkerLimit(pool) {
        this.pool = pool;
    }

    postMessage(msg, transfer) {
        return new Promise((resolve) => {
            const workerId = this._getIdleWorker();
            if (workerId !== -1) {
                this._initWorker(workerId);
                this.workerStatus |= 1 << workerId;
                this.workersResolve[workerId] = resolve;
                this.workers[workerId].postMessage(msg, transfer);
            } else {
                this.queue.push({resolve, msg, transfer});
            }
        });
    }

    destroy() {

        this.workers.forEach((worker) => worker.terminate());
        this.workersResolve.length = 0;
        this.workers.length = 0;
        this.queue.length = 0;
        this.workerStatus = 0;

    }

}

const KTX2TransferSRGB = 2;
const KTX2_ALPHA_PREMULTIPLIED = 1;

let activeTranscoders = 0;

/**
 * Transcodes texture data from KTX2.
 *
 * ## Overview
 *
 * * Uses the [Basis Universal GPU Texture Codec](https://github.com/BinomialLLC/basis_universal) to
 * transcode [KTX2](https://github.khronos.org/KTX-Specification/) textures.
 * * {@link XKTLoaderPlugin} uses a KTX2TextureTranscoder to load textures in XKT files.
 * * {@link VBOSceneModel} uses a KTX2TextureTranscoder to enable us to add KTX2-encoded textures.
 * * Loads the Basis Codec from [CDN](https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/) by default, but can
 * also be configured to load the Codec from local files.
 * * We also bundle the Basis Codec with the xeokit-sdk npm package, and in the [repository](https://github.com/xeokit/xeokit-sdk/tree/master/dist/basis).
 *
 * ## What is KTX2?
 *
 * A [KTX2](https://github.khronos.org/KTX-Specification/) file stores GPU texture data in the Khronos Texture 2.0 (KTX2) container format. It contains image data for
 * a texture asset compressed with Basis Universal (BasisU) supercompression that can be transcoded to different formats
 * depending on the support provided by the target devices. KTX2 provides a lightweight format for distributing texture
 * assets to GPUs. Due to BasisU compression, KTX2 files can store any image format supported by GPUs.
 *
 * ## Loading XKT files containing KTX2 textures
 *
 * {@link XKTLoaderPlugin} uses a KTX2TextureTranscoder to load textures in XKT files. An XKTLoaderPlugin has its own
 * default KTX2TextureTranscoder, configured to load the Basis Codec from the [CDN](https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/). If we wish, we can override that with our own
 * KTX2TextureTranscoder, configured to load the Codec locally.
 *
 * In the example below, we'll create a {@link Viewer} and add an {@link XKTLoaderPlugin}
 * configured with a KTX2TextureTranscoder. Then we'll use the XKTLoaderPlugin to load an
 * XKT file that contains KTX2 textures, which the plugin will transcode using
 * its KTX2TextureTranscoder.
 *
 * We'll configure our KTX2TextureTranscoder to load the Basis Codec from a local directory. If we were happy with loading the
 * Codec from our [CDN](https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/) (ie. our app will always have an Internet connection) then we could just leave out the
 * KTX2TextureTranscoder altogether, and let the XKTLoaderPlugin use its internal default KTX2TextureTranscoder, which is configured to
 * load the Codec from the CDN. We'll stick with loading our own Codec, in case we want to run our app without an Internet connection.
 *
 * <a href="https://xeokit.github.io/xeokit-sdk/examples/buildings/#xkt_vbo_textures_HousePlan"><img src="https://xeokit.github.io/xeokit-sdk/assets/images/xktWithTextures.png"></a>
 *
 * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/buildings/#xkt_vbo_textures_HousePlan)]
 *
 * ````javascript
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.camera.eye = [-2.56, 8.38, 8.27];
 * viewer.camera.look = [13.44, 3.31, -14.83];
 * viewer.camera.up = [0.10, 0.98, -0.14];
 *
 * const textureTranscoder = new KTX2TextureTranscoder({
 *     viewer,
 *     transcoderPath: "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/" // <------ Path to Basis Universal transcoder
 * });
 *
 * const xktLoader = new XKTLoaderPlugin(viewer, {
 *     textureTranscoder // <<------------- Transcodes KTX2 textures in XKT files
 * });
 *
 * const sceneModel = xktLoader.load({
 *     id: "myModel",
 *     src: "./HousePlan.xkt" // <<------ XKT file with KTX2 textures
 * });
 * ````
 *
 * ## Loading KTX2 files into a VBOSceneModel
 *
 * A {@link SceneModel} that is configured with a KTX2TextureTranscoder will
 * allow us to load textures into it from KTX2-transcoded buffers or files.
 *
 * In the example below, we'll create a {@link Viewer}, containing a {@link VBOSceneModel} configured with a
 * KTX2TextureTranscoder.
 *
 * We'll then programmatically create a simple object within the VBOSceneModel, consisting of
 * a single box mesh with a texture loaded from a KTX2 file, which our VBOSceneModel internally transcodes, using
 * its KTX2TextureTranscoder.
 *
 * As in the previous example, we'll configure our KTX2TextureTranscoder to load the Basis Codec from a local directory.
 *
 * * [Run a similar example](https://xeokit.github.io/xeokit-sdk/examples/scenemodel/#vbo_batching_autocompressed_triangles_textures_ktx2)
 *
 * ````javascript
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.scene.camera.eye = [-21.80, 4.01, 6.56];
 * viewer.scene.camera.look = [0, -5.75, 0];
 * viewer.scene.camera.up = [0.37, 0.91, -0.11];
 *
 * const textureTranscoder = new KTX2TextureTranscoder({
 *     viewer,
 *     transcoderPath: "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/" // <------ Path to BasisU transcoder module
 * });
 *
 * const vboSceneModel = new VBOSceneModel(viewer.scene, {
 *      id: "myModel",
 *      textureTranscoder // <<-------------------- Configure model with our transcoder
 *  });
 *
 * vboSceneModel.createTexture({
 *      id: "myColorTexture",
 *      src: "../assets/textures/compressed/sample_uastc_zstd.ktx2" // <<----- KTX2 texture asset
 * });
 *
 * vboSceneModel.createTexture({
 *      id: "myMetallicRoughnessTexture",
 *      src: "../assets/textures/alpha/crosshatchAlphaMap.jpg" // <<----- JPEG texture asset
 * });
 *
 * vboSceneModel.createTextureSet({
 *      id: "myTextureSet",
 *      colorTextureId: "myColorTexture",
 *      metallicRoughnessTextureId: "myMetallicRoughnessTexture"
 *  });
 *
 * vboSceneModel.createMesh({
 *      id: "myMesh",
 *      textureSetId: "myTextureSet",
 *      primitive: "triangles",
 *      positions: [1, 1, 1, ...],
 *      normals: [0, 0, 1, 0, ...],
 *      uv: [1, 0, 0, ...],
 *      indices: [0, 1, 2, ...],
 *  });
 *
 * vboSceneModel.createEntity({
 *      id: "myEntity",
 *      meshIds: ["myMesh"]
 *  });
 *
 * vboSceneModel.finalize();
 * ````
 *
 * ## Loading KTX2 ArrayBuffers into a VBOSceneModel
 *
 * A {@link SceneModel} that is configured with a KTX2TextureTranscoder will also allow us to load textures into
 * it from KTX2 ArrayBuffers.
 *
 * In the example below, we'll create a {@link Viewer}, containing a {@link VBOSceneModel} configured with a
 * KTX2TextureTranscoder.
 *
 * We'll then programmatically create a simple object within the VBOSceneModel, consisting of
 * a single mesh with a texture loaded from a KTX2 ArrayBuffer, which our VBOSceneModel internally transcodes, using
 * its KTX2TextureTranscoder.
 *
 * ````javascript
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.scene.camera.eye = [-21.80, 4.01, 6.56];
 * viewer.scene.camera.look = [0, -5.75, 0];
 * viewer.scene.camera.up = [0.37, 0.91, -0.11];
 *
 * const textureTranscoder = new KTX2TextureTranscoder({
 *     viewer,
 *     transcoderPath: "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/" // <------ Path to BasisU transcoder module
 * });
 *
 * const vboSceneModel = new VBOSceneModel(viewer.scene, {
 *      id: "myModel",
 *      textureTranscoder // <<-------------------- Configure model with our transcoder
 * });
 *
 * utils.loadArraybuffer("../assets/textures/compressed/sample_uastc_zstd.ktx2",(arrayBuffer) => {
 *
 *     vboSceneModel.createTexture({
 *         id: "myColorTexture",
 *         buffers: [arrayBuffer] // <<----- KTX2 texture asset
 *     });
 *
 *     vboSceneModel.createTexture({
 *         id: "myMetallicRoughnessTexture",
 *         src: "../assets/textures/alpha/crosshatchAlphaMap.jpg" // <<----- JPEG texture asset
 *     });
 *
 *     vboSceneModel.createTextureSet({
 *        id: "myTextureSet",
 *        colorTextureId: "myColorTexture",
 *        metallicRoughnessTextureId: "myMetallicRoughnessTexture"
 *     });
 *
 *     vboSceneModel.createMesh({
 *          id: "myMesh",
 *          textureSetId: "myTextureSet",
 *          primitive: "triangles",
 *          positions: [1, 1, 1, ...],
 *          normals: [0, 0, 1, 0, ...],
 *          uv: [1, 0, 0, ...],
 *          indices: [0, 1, 2, ...],
 *     });
 *
 *     vboSceneModel.createEntity({
 *         id: "myEntity",
 *         meshIds: ["myMesh"]
 *     });
 *
 *     vboSceneModel.finalize();
 * });
 * ````
 *
 * @implements {TextureTranscoder}
 */
class KTX2TextureTranscoder {

    /**
     * Creates a new KTX2TextureTranscoder.
     *
     * @param {Viewer} viewer The Viewer that our KTX2TextureTranscoder will be used with. This KTX2TextureTranscoder
     * must only be used to transcode textures for this Viewer. This is because the Viewer's capabilities will decide
     * what target GPU formats this KTX2TextureTranscoder will transcode to.
     * @param {String} [transcoderPath="https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/"] Path to the Basis
     * transcoder module that internally does the heavy lifting for our KTX2TextureTranscoder. If we omit this configuration,
     * then our KTX2TextureTranscoder will load it from ````https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/```` by
     * default. Therefore, make sure your application is connected to the internet if you wish to use the default transcoder path.
     * @param {Number} [workerLimit] The maximum number of Workers to use for transcoding.
     */
    constructor({viewer, transcoderPath, workerLimit}) {

        this._transcoderPath = transcoderPath || "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/";
        this._transcoderBinary = null;
        this._transcoderPending = null;
        this._workerPool = new WorkerPool$1();
        this._workerSourceURL = '';

        if (workerLimit) {
            this._workerPool.setWorkerLimit(workerLimit);
        }

        const viewerCapabilities = viewer.capabilities;

        this._workerConfig = {
            astcSupported: viewerCapabilities.astcSupported,
            etc1Supported: viewerCapabilities.etc1Supported,
            etc2Supported: viewerCapabilities.etc2Supported,
            dxtSupported: viewerCapabilities.dxtSupported,
            bptcSupported: viewerCapabilities.bptcSupported,
            pvrtcSupported: viewerCapabilities.pvrtcSupported
        };

        this._supportedFileTypes = ["xkt2"];
    }

    _init() {
        if (!this._transcoderPending) {
            const jsLoader = new FileLoader();
            jsLoader.setPath(this._transcoderPath);
            jsLoader.setWithCredentials(this.withCredentials);
            const jsContent = jsLoader.loadAsync('basis_transcoder.js');
            const binaryLoader = new FileLoader();
            binaryLoader.setPath(this._transcoderPath);
            binaryLoader.setResponseType('arraybuffer');
            binaryLoader.setWithCredentials(this.withCredentials);
            const binaryContent = binaryLoader.loadAsync('basis_transcoder.wasm');
            this._transcoderPending = Promise.all([jsContent, binaryContent])
                .then(([jsContent, binaryContent]) => {
                    const fn = KTX2TextureTranscoder.BasisWorker.toString();
                    const body = [
                        '/* constants */',
                        'let _EngineFormat = ' + JSON.stringify(KTX2TextureTranscoder.EngineFormat),
                        'let _TranscoderFormat = ' + JSON.stringify(KTX2TextureTranscoder.TranscoderFormat),
                        'let _BasisFormat = ' + JSON.stringify(KTX2TextureTranscoder.BasisFormat),
                        '/* basis_transcoder.js */',
                        jsContent,
                        '/* worker */',
                        fn.substring(fn.indexOf('{') + 1, fn.lastIndexOf('}'))
                    ].join('\n');
                    this._workerSourceURL = URL.createObjectURL(new Blob([body]));
                    this._transcoderBinary = binaryContent;
                    this._workerPool.setWorkerCreator(() => {
                        const worker = new Worker(this._workerSourceURL);
                        const transcoderBinary = this._transcoderBinary.slice(0);
                        worker.postMessage({
                            type: 'init',
                            config: this._workerConfig,
                            transcoderBinary
                        }, [transcoderBinary]);
                        return worker;
                    });
                });
            if (activeTranscoders > 0) {
                console.warn('KTX2TextureTranscoder: Multiple active KTX2TextureTranscoder may cause performance issues.' + ' Use a single KTX2TextureTranscoder instance, or call .dispose() on old instances.');
            }
            activeTranscoders++;
        }
        return this._transcoderPending;
    }

    /**
     * Transcodes texture data from transcoded buffers into a {@link Texture2D}.
     *
     * @param {ArrayBuffer[]} buffers Transcoded texture data. Given as an array of buffers so that we can support multi-image textures, such as cube maps.
     * @param {*} config Transcoding options.
     * @param {Texture2D} texture The texture to load.
     * @returns {Promise} Resolves when the texture has loaded.
     */
    transcode(buffers, texture, config = {}) {
        return new Promise((resolve, reject) => {
            const taskConfig = config;
            this._init().then(() => {
                return this._workerPool.postMessage({
                    type: 'transcode',
                    buffers,
                    taskConfig: taskConfig
                }, buffers);
            }).then((e) => {
                const transcodeResult = e.data;
                const {mipmaps, width, height, format, type, error, dfdTransferFn, dfdFlags} = transcodeResult;
                if (type === 'error') {
                    return reject(error);
                }
                texture.setCompressedData({
                    mipmaps,
                    props: {
                        format: format,
                        minFilter: mipmaps.length === 1 ? LinearFilter : LinearMipmapLinearFilter,
                        magFilter: mipmaps.length === 1 ? LinearFilter : LinearMipmapLinearFilter,
                        encoding: dfdTransferFn === KTX2TransferSRGB ? sRGBEncoding : LinearEncoding,
                        premultiplyAlpha: !!(dfdFlags & KTX2_ALPHA_PREMULTIPLIED)
                    }
                });
                resolve();
            });
        });
    }

    /**
     * Destroys this KTX2TextureTranscoder
     */
    destroy() {
        URL.revokeObjectURL(this._workerSourceURL);
        this._workerPool.destroy();
        activeTranscoders--;
    }
}

/**
 * @private
 */
KTX2TextureTranscoder.BasisFormat = {
    ETC1S: 0,
    UASTC_4x4: 1
};

/**
 * @private
 */
KTX2TextureTranscoder.TranscoderFormat = {
    ETC1: 0,
    ETC2: 1,
    BC1: 2,
    BC3: 3,
    BC4: 4,
    BC5: 5,
    BC7_M6_OPAQUE_ONLY: 6,
    BC7_M5: 7,
    PVRTC1_4_RGB: 8,
    PVRTC1_4_RGBA: 9,
    ASTC_4x4: 10,
    ATC_RGB: 11,
    ATC_RGBA_INTERPOLATED_ALPHA: 12,
    RGBA32: 13,
    RGB565: 14,
    BGR565: 15,
    RGBA4444: 16
};

/**
 * @private
 */
KTX2TextureTranscoder.EngineFormat = {
    RGBAFormat: RGBAFormat,
    RGBA_ASTC_4x4_Format: RGBA_ASTC_4x4_Format,
    RGBA_BPTC_Format: RGBA_BPTC_Format,
    RGBA_ETC2_EAC_Format: RGBA_ETC2_EAC_Format,
    RGBA_PVRTC_4BPPV1_Format: RGBA_PVRTC_4BPPV1_Format,
    RGBA_S3TC_DXT5_Format: RGBA_S3TC_DXT5_Format,
    RGB_ETC1_Format: RGB_ETC1_Format,
    RGB_ETC2_Format: RGB_ETC2_Format,
    RGB_PVRTC_4BPPV1_Format: RGB_PVRTC_4BPPV1_Format,
    RGB_S3TC_DXT1_Format: RGB_S3TC_DXT1_Format
};

/* WEB WORKER */

/**
 * @private
 * @constructor
 */
KTX2TextureTranscoder.BasisWorker = function () {

    let config;
    let transcoderPending;
    let BasisModule;

    const EngineFormat = _EngineFormat; // eslint-disable-line no-undef
    const TranscoderFormat = _TranscoderFormat; // eslint-disable-line no-undef
    const BasisFormat = _BasisFormat; // eslint-disable-line no-undef

    self.addEventListener('message', function (e) {
        const message = e.data;
        switch (message.type) {
            case 'init':
                config = message.config;
                init(message.transcoderBinary);
                break;
            case 'transcode':
                transcoderPending.then(() => {
                    try {
                        const {
                            width,
                            height,
                            hasAlpha,
                            mipmaps,
                            format,
                            dfdTransferFn,
                            dfdFlags
                        } = transcode(message.buffers[0]);
                        const buffers = [];
                        for (let i = 0; i < mipmaps.length; ++i) {
                            buffers.push(mipmaps[i].data.buffer);
                        }
                        self.postMessage({
                            type: 'transcode',
                            id: message.id,
                            width,
                            height,
                            hasAlpha,
                            mipmaps,
                            format,
                            dfdTransferFn,
                            dfdFlags
                        }, buffers);
                    } catch (error) {
                        console.error(`[KTX2TextureTranscoder.BasisWorker]: ${error}`);
                        self.postMessage({type: 'error', id: message.id, error: error.message});
                    }
                });
                break;
        }
    });

    function init(wasmBinary) {
        transcoderPending = new Promise(resolve => {
            BasisModule = {
                wasmBinary,
                onRuntimeInitialized: resolve
            };
            BASIS(BasisModule); // eslint-disable-line no-undef
        }).then(() => {
            BasisModule.initializeBasis();
            if (BasisModule.KTX2File === undefined) {
                console.warn('KTX2TextureTranscoder: Please update Basis Universal transcoder.');
            }
        });
    }

    function transcode(buffer) {
        const ktx2File = new BasisModule.KTX2File(new Uint8Array(buffer));

        function cleanup() {
            ktx2File.close();
            ktx2File.delete();
        }

        if (!ktx2File.isValid()) {
            cleanup();
            throw new Error('KTX2TextureTranscoder: Invalid or unsupported .ktx2 file');
        }
        const basisFormat = ktx2File.isUASTC() ? BasisFormat.UASTC_4x4 : BasisFormat.ETC1S;
        const width = ktx2File.getWidth();
        const height = ktx2File.getHeight();
        const levels = ktx2File.getLevels();
        const hasAlpha = ktx2File.getHasAlpha();
        const dfdTransferFn = ktx2File.getDFDTransferFunc();
        const dfdFlags = ktx2File.getDFDFlags();
        const {transcoderFormat, engineFormat} = getTranscoderFormat(basisFormat, width, height, hasAlpha);
        if (!width || !height || !levels) {
            cleanup();
            throw new Error('KTX2TextureTranscoder: Invalid texture');
        }
        if (!ktx2File.startTranscoding()) {
            cleanup();
            throw new Error('KTX2TextureTranscoder: .startTranscoding failed');
        }
        const mipmaps = [];
        for (let mip = 0; mip < levels; mip++) {
            const levelInfo = ktx2File.getImageLevelInfo(mip, 0, 0);
            const mipWidth = levelInfo.origWidth;
            const mipHeight = levelInfo.origHeight;
            const dst = new Uint8Array(ktx2File.getImageTranscodedSizeInBytes(mip, 0, 0, transcoderFormat));
            const status = ktx2File.transcodeImage(dst, mip, 0, 0, transcoderFormat, 0, -1, -1);
            if (!status) {
                cleanup();
                throw new Error('KTX2TextureTranscoder: .transcodeImage failed.');
            }
            mipmaps.push({data: dst, width: mipWidth, height: mipHeight});
        }
        cleanup();
        return {width, height, hasAlpha, mipmaps, format: engineFormat, dfdTransferFn, dfdFlags};
    }

    // Optimal choice of a transcoder target format depends on the Basis format (ETC1S or UASTC),
    // device capabilities, and texture dimensions. The list below ranks the formats separately
    // for ETC1S and UASTC.
    //
    // In some cases, transcoding UASTC to RGBA32 might be preferred for higher quality (at
    // significant memory cost) compared to ETC1/2, BC1/3, and PVRTC. The transcoder currently
    // chooses RGBA32 only as a last resort and does not expose that option to the caller.

    const FORMAT_OPTIONS = [{
        if: 'astcSupported',
        basisFormat: [BasisFormat.UASTC_4x4],
        transcoderFormat: [TranscoderFormat.ASTC_4x4, TranscoderFormat.ASTC_4x4],
        engineFormat: [EngineFormat.RGBA_ASTC_4x4_Format, EngineFormat.RGBA_ASTC_4x4_Format],
        priorityETC1S: Infinity,
        priorityUASTC: 1,
        needsPowerOfTwo: false
    }, {
        if: 'bptcSupported',
        basisFormat: [BasisFormat.ETC1S, BasisFormat.UASTC_4x4],
        transcoderFormat: [TranscoderFormat.BC7_M5, TranscoderFormat.BC7_M5],
        engineFormat: [EngineFormat.RGBA_BPTC_Format, EngineFormat.RGBA_BPTC_Format],
        priorityETC1S: 3,
        priorityUASTC: 2,
        needsPowerOfTwo: false
    }, {
        if: 'dxtSupported',
        basisFormat: [BasisFormat.ETC1S, BasisFormat.UASTC_4x4],
        transcoderFormat: [TranscoderFormat.BC1, TranscoderFormat.BC3],
        engineFormat: [EngineFormat.RGB_S3TC_DXT1_Format, EngineFormat.RGBA_S3TC_DXT5_Format],
        priorityETC1S: 4,
        priorityUASTC: 5,
        needsPowerOfTwo: false
    }, {
        if: 'etc2Supported',
        basisFormat: [BasisFormat.ETC1S, BasisFormat.UASTC_4x4],
        transcoderFormat: [TranscoderFormat.ETC1, TranscoderFormat.ETC2],
        engineFormat: [EngineFormat.RGB_ETC2_Format, EngineFormat.RGBA_ETC2_EAC_Format],
        priorityETC1S: 1,
        priorityUASTC: 3,
        needsPowerOfTwo: false
    }, {
        if: 'etc1Supported',
        basisFormat: [BasisFormat.ETC1S, BasisFormat.UASTC_4x4],
        transcoderFormat: [TranscoderFormat.ETC1],
        engineFormat: [EngineFormat.RGB_ETC1_Format],
        priorityETC1S: 2,
        priorityUASTC: 4,
        needsPowerOfTwo: false
    }, {
        if: 'pvrtcSupported',
        basisFormat: [BasisFormat.ETC1S, BasisFormat.UASTC_4x4],
        transcoderFormat: [TranscoderFormat.PVRTC1_4_RGB, TranscoderFormat.PVRTC1_4_RGBA],
        engineFormat: [EngineFormat.RGB_PVRTC_4BPPV1_Format, EngineFormat.RGBA_PVRTC_4BPPV1_Format],
        priorityETC1S: 5,
        priorityUASTC: 6,
        needsPowerOfTwo: true
    }];
    const ETC1S_OPTIONS = FORMAT_OPTIONS.sort(function (a, b) {
        return a.priorityETC1S - b.priorityETC1S;
    });
    const UASTC_OPTIONS = FORMAT_OPTIONS.sort(function (a, b) {
        return a.priorityUASTC - b.priorityUASTC;
    });

    function getTranscoderFormat(basisFormat, width, height, hasAlpha) {
        let transcoderFormat;
        let engineFormat;
        const options = basisFormat === BasisFormat.ETC1S ? ETC1S_OPTIONS : UASTC_OPTIONS;
        for (let i = 0; i < options.length; i++) {
            const opt = options[i];
            if (!config[opt.if]) continue;
            if (!opt.basisFormat.includes(basisFormat)) continue;
            if (hasAlpha && opt.transcoderFormat.length < 2) continue;
            if (opt.needsPowerOfTwo && !(isPowerOfTwo(width) && isPowerOfTwo(height))) continue;
            transcoderFormat = opt.transcoderFormat[hasAlpha ? 1 : 0];
            engineFormat = opt.engineFormat[hasAlpha ? 1 : 0];
            return {
                transcoderFormat,
                engineFormat
            };
        }
        console.warn('KTX2TextureTranscoder: No suitable compressed texture format found. Decoding to RGBA32.');
        transcoderFormat = TranscoderFormat.RGBA32;
        engineFormat = EngineFormat.RGBAFormat;
        return {
            transcoderFormat,
            engineFormat
        };
    }

    function isPowerOfTwo(value) {
        if (value <= 2) return true;
        return (value & value - 1) === 0 && value !== 0;
    }
};

const cachedTranscoders = {};

/**
 * Returns a new {@link KTX2TextureTranscoder}.
 *
 * The ````transcoderPath```` config will be set to: "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/"
 *
 * @private
 */
function getKTX2TextureTranscoder(viewer) {
    const sceneId = viewer.scene.id;
    let transcoder = cachedTranscoders[sceneId];
    if (!transcoder) {
        transcoder = new KTX2TextureTranscoder({viewer});
        cachedTranscoders[sceneId] = transcoder;
        viewer.scene.on("destroyed", () => {
            delete cachedTranscoders[sceneId];
            transcoder.destroy();
        });
    }
    return transcoder;
}

/**
 * @author https://github.com/tmarti, with support from https://tribia.com/
 * @license MIT
 *
 * This file takes a geometry given by { positionsCompressed, indices }, and returns
 * equivalent { positionsCompressed, indices } arrays but which only contain unique
 * positionsCompressed.
 *
 * The time is O(N logN) with the number of positionsCompressed due to a pre-sorting
 * step, but is much more GC-friendly and actually faster than the classic O(N)
 * approach based in keeping a hash-based LUT to identify unique positionsCompressed.
 */
let comparePositions = null;

function compareVertex(a, b) {
    let res;
    for (let i = 0; i < 3; i++) {
        if (0 !== (res = comparePositions[a * 3 + i] - comparePositions[b * 3 + i])) {
            return res;
        }
    }
    return 0;
}

let seqInit = null;

function setMaxNumberOfPositions(maxPositions) {
    if (seqInit !== null && seqInit.length >= maxPositions) {
        return;
    }
    seqInit = new Uint32Array(maxPositions);
    for (let i = 0; i < maxPositions; i++) {
        seqInit[i] = i;
    }
}

/**
 * This function obtains unique positionsCompressed in the provided object
 * .positionsCompressed array and calculates an index mapping, which is then
 * applied to the provided object .indices and .edgeindices.
 *
 * The input object items are not modified, and instead new set
 * of positionsCompressed, indices and edgeIndices with the applied optimization
 * are returned.
 *
 * The algorithm, instead of being based in a hash-like LUT for
 * identifying unique positionsCompressed, is based in pre-sorting the input
 * positionsCompressed...
 *
 * (it's possible to define a _"consistent ordering"_ for the positionsCompressed
 *  as positionsCompressed are quantized and thus not suffer from float number
 *  comparison artifacts)
 *
 * ... so same positionsCompressed are adjacent in the sorted array, and then
 * it's easy to scan linearly the sorted array. During the linear run,
 * we will know that we found a different position because the comparison
 * function will return != 0 between current and previous element.
 *
 * During this linear traversal of the array, a `unique counter` is used
 * in order to calculate the mapping between original indices and unique
 * indices.
 *
 * @param {{positionsCompressed: number[],indices: number[], edgeIndices: number[]}} mesh The input mesh to process, with `positionsCompressed`, `indices` and `edgeIndices` keys.
 *
 * @returns {[Uint16Array, Uint32Array, Uint32Array]} An array with 3 elements: 0 => the uniquified positionsCompressed; 1 and 2 => the remapped edges and edgeIndices arrays
 */
function uniquifyPositions(mesh) {
    const _positions = mesh.positionsCompressed;
    const _indices = mesh.indices;
    const _edgeIndices = mesh.edgeIndices;

    setMaxNumberOfPositions(_positions.length / 3);

    const seq = seqInit.slice(0, _positions.length / 3);
    const remappings = seqInit.slice(0, _positions.length / 3);

    comparePositions = _positions;

    seq.sort(compareVertex);

    let uniqueIdx = 0;

    remappings[seq[0]] = 0;

    for (let i = 1, len = seq.length; i < len; i++) {
        if (0 !== compareVertex(seq[i], seq[i - 1])) {
            uniqueIdx++;
        }
        remappings[seq[i]] = uniqueIdx;
    }

    const numUniquePositions = uniqueIdx + 1;
    const newPositions = new Uint16Array(numUniquePositions * 3);

    uniqueIdx = 0;

    newPositions [uniqueIdx * 3 + 0] = _positions [seq[0] * 3 + 0];
    newPositions [uniqueIdx * 3 + 1] = _positions [seq[0] * 3 + 1];
    newPositions [uniqueIdx * 3 + 2] = _positions [seq[0] * 3 + 2];

    for (let i = 1, len = seq.length; i < len; i++) {
        if (0 !== compareVertex(seq[i], seq[i - 1])) {
            uniqueIdx++;
            newPositions [uniqueIdx * 3 + 0] = _positions [seq[i] * 3 + 0];
            newPositions [uniqueIdx * 3 + 1] = _positions [seq[i] * 3 + 1];
            newPositions [uniqueIdx * 3 + 2] = _positions [seq[i] * 3 + 2];
        }
        remappings[seq[i]] = uniqueIdx;
    }

    comparePositions = null;

    const newIndices = new Uint32Array(_indices.length);

    for (let i = 0, len = _indices.length; i < len; i++) {
        newIndices[i] = remappings [_indices[i]];
    }

    const newEdgeIndices = new Uint32Array(_edgeIndices.length);

    for (let i = 0, len = _edgeIndices.length; i < len; i++) {
        newEdgeIndices[i] = remappings [_edgeIndices[i]];
    }

    return [newPositions, newIndices, newEdgeIndices];
}

/**
 * @author https://github.com/tmarti, with support from https://tribia.com/
 * @license MIT
 **/

const MAX_RE_BUCKET_FAN_OUT = 8;

let bucketsForIndices = null;

function compareBuckets(a, b) {
    const aa = a * 3;
    const bb = b * 3;
    let aa1, aa2, aa3, bb1, bb2, bb3;
    const minBucketA = Math.min(
        aa1 = bucketsForIndices[aa],
        aa2 = bucketsForIndices[aa + 1],
        aa3 = bucketsForIndices[aa + 2]
    );
    const minBucketB = Math.min(
        bb1 = bucketsForIndices[bb],
        bb2 = bucketsForIndices[bb + 1],
        bb3 = bucketsForIndices[bb + 2]
    );
    if (minBucketA !== minBucketB) {
        return minBucketA - minBucketB;
    }
    const maxBucketA = Math.max(aa1, aa2, aa3);
    const maxBucketB = Math.max(bb1, bb2, bb3,);
    if (maxBucketA !== maxBucketB) {
        return maxBucketA - maxBucketB;
    }
    return 0;
}

function preSortIndices(indices, bitsPerBucket) {
    const seq = new Int32Array(indices.length / 3);
    for (let i = 0, len = seq.length; i < len; i++) {
        seq[i] = i;
    }
    bucketsForIndices = new Int32Array(indices.length);
    for (let i = 0, len = indices.length; i < len; i++) {
        bucketsForIndices[i] = indices[i] >> bitsPerBucket;
    }
    seq.sort(compareBuckets);
    const sortedIndices = new Int32Array(indices.length);
    for (let i = 0, len = seq.length; i < len; i++) {
        sortedIndices[i * 3 + 0] = indices[seq[i] * 3 + 0];
        sortedIndices[i * 3 + 1] = indices[seq[i] * 3 + 1];
        sortedIndices[i * 3 + 2] = indices[seq[i] * 3 + 2];
    }
    return sortedIndices;
}

let compareEdgeIndices = null;

function compareIndices(a, b) {
    let retVal = compareEdgeIndices[a * 2] - compareEdgeIndices[b * 2];
    if (retVal !== 0) {
        return retVal;
    }
    return compareEdgeIndices[a * 2 + 1] - compareEdgeIndices[b * 2 + 1];
}

function preSortEdgeIndices(edgeIndices) {
    if ((edgeIndices || []).length === 0) {
        return [];
    }
    let seq = new Int32Array(edgeIndices.length / 2);
    for (let i = 0, len = seq.length; i < len; i++) {
        seq[i] = i;
    }
    for (let i = 0, len = edgeIndices.length; i < len; i += 2) {
        if (edgeIndices[i] > edgeIndices[i + 1]) {
            let tmp = edgeIndices[i];
            edgeIndices[i] = edgeIndices[i + 1];
            edgeIndices[i + 1] = tmp;
        }
    }
    compareEdgeIndices = new Int32Array(edgeIndices);
    seq.sort(compareIndices);
    const sortedEdgeIndices = new Int32Array(edgeIndices.length);
    for (let i = 0, len = seq.length; i < len; i++) {
        sortedEdgeIndices[i * 2 + 0] = edgeIndices[seq[i] * 2 + 0];
        sortedEdgeIndices[i * 2 + 1] = edgeIndices[seq[i] * 2 + 1];
    }
    return sortedEdgeIndices;
}

/**
 * @param {{positionsCompressed: number[], indices: number[], edgeIndices: number[]}} mesh 
 * @param {number} bitsPerBucket 
 * @param {boolean} checkResult 
 * 
 * @returns {{positionsCompressed: number[], indices: number[], edgeIndices: number[]}[]}
 */
function rebucketPositions(mesh, bitsPerBucket, checkResult = false) {
    const positionsCompressed = (mesh.positionsCompressed || []);
    const indices = preSortIndices(mesh.indices || [], bitsPerBucket);
    const edgeIndices = preSortEdgeIndices(mesh.edgeIndices || []);

    function edgeSearch(el0, el1) { // Code adapted from https://stackoverflow.com/questions/22697936/binary-search-in-javascript
        if (el0 > el1) {
            let tmp = el0;
            el0 = el1;
            el1 = tmp;
        }

        function compare_fn(a, b) {
            if (a !== el0) {
                return el0 - a;
            }
            if (b !== el1) {
                return el1 - b;
            }
            return 0;
        }

        let m = 0;
        let n = (edgeIndices.length >> 1) - 1;
        while (m <= n) {
            const k = (n + m) >> 1;
            const cmp = compare_fn(edgeIndices[k * 2], edgeIndices[k * 2 + 1]);
            if (cmp > 0) {
                m = k + 1;
            } else if (cmp < 0) {
                n = k - 1;
            } else {
                return k;
            }
        }
        return -m - 1;
    }

    const alreadyOutputEdgeIndices = new Int32Array(edgeIndices.length / 2);
    alreadyOutputEdgeIndices.fill(0);

    const numPositions = positionsCompressed.length / 3;

    if (numPositions > ((1 << bitsPerBucket) * MAX_RE_BUCKET_FAN_OUT)) {
        return [mesh];
    }

    const bucketIndicesRemap = new Int32Array(numPositions);
    bucketIndicesRemap.fill(-1);

    const buckets = [];

    function addEmptyBucket() {
        bucketIndicesRemap.fill(-1);

        const newBucket = {
            positionsCompressed: [],
            indices: [],
            edgeIndices: [],
            maxNumPositions: (1 << bitsPerBucket) - bitsPerBucket,
            numPositions: 0,
            bucketNumber: buckets.length,
        };

        buckets.push(newBucket);

        return newBucket;
    }

    let currentBucket = addEmptyBucket();

    for (let i = 0, len = indices.length; i < len; i += 3) {
        let additonalPositionsInBucket = 0;

        const ii0 = indices[i];
        const ii1 = indices[i + 1];
        const ii2 = indices[i + 2];

        if (bucketIndicesRemap[ii0] === -1) {
            additonalPositionsInBucket++;
        }

        if (bucketIndicesRemap[ii1] === -1) {
            additonalPositionsInBucket++;
        }

        if (bucketIndicesRemap[ii2] === -1) {
            additonalPositionsInBucket++;
        }

        if ((additonalPositionsInBucket + currentBucket.numPositions) > currentBucket.maxNumPositions) {
            currentBucket = addEmptyBucket();
        }

        if (currentBucket.bucketNumber > MAX_RE_BUCKET_FAN_OUT) {
            return [mesh];
        }

        if (bucketIndicesRemap[ii0] === -1) {
            bucketIndicesRemap[ii0] = currentBucket.numPositions++;
            currentBucket.positionsCompressed.push(positionsCompressed[ii0 * 3]);
            currentBucket.positionsCompressed.push(positionsCompressed[ii0 * 3 + 1]);
            currentBucket.positionsCompressed.push(positionsCompressed[ii0 * 3 + 2]);
        }

        if (bucketIndicesRemap[ii1] === -1) {
            bucketIndicesRemap[ii1] = currentBucket.numPositions++;
            currentBucket.positionsCompressed.push(positionsCompressed[ii1 * 3]);
            currentBucket.positionsCompressed.push(positionsCompressed[ii1 * 3 + 1]);
            currentBucket.positionsCompressed.push(positionsCompressed[ii1 * 3 + 2]);
        }

        if (bucketIndicesRemap[ii2] === -1) {
            bucketIndicesRemap[ii2] = currentBucket.numPositions++;
            currentBucket.positionsCompressed.push(positionsCompressed[ii2 * 3]);
            currentBucket.positionsCompressed.push(positionsCompressed[ii2 * 3 + 1]);
            currentBucket.positionsCompressed.push(positionsCompressed[ii2 * 3 + 2]);
        }

        currentBucket.indices.push(bucketIndicesRemap[ii0]);
        currentBucket.indices.push(bucketIndicesRemap[ii1]);
        currentBucket.indices.push(bucketIndicesRemap[ii2]);

        // Check possible edge1
        let edgeIndex;

        if ((edgeIndex = edgeSearch(ii0, ii1)) >= 0) {
            if (alreadyOutputEdgeIndices[edgeIndex] === 0) {
                alreadyOutputEdgeIndices[edgeIndex] = 1;

                currentBucket.edgeIndices.push(bucketIndicesRemap[edgeIndices[edgeIndex * 2]]);
                currentBucket.edgeIndices.push(bucketIndicesRemap[edgeIndices[edgeIndex * 2 + 1]]);
            }
        }

        if ((edgeIndex = edgeSearch(ii0, ii2)) >= 0) {
            if (alreadyOutputEdgeIndices[edgeIndex] === 0) {
                alreadyOutputEdgeIndices[edgeIndex] = 1;

                currentBucket.edgeIndices.push(bucketIndicesRemap[edgeIndices[edgeIndex * 2]]);
                currentBucket.edgeIndices.push(bucketIndicesRemap[edgeIndices[edgeIndex * 2 + 1]]);
            }
        }

        if ((edgeIndex = edgeSearch(ii1, ii2)) >= 0) {
            if (alreadyOutputEdgeIndices[edgeIndex] === 0) {
                alreadyOutputEdgeIndices[edgeIndex] = 1;

                currentBucket.edgeIndices.push(bucketIndicesRemap[edgeIndices[edgeIndex * 2]]);
                currentBucket.edgeIndices.push(bucketIndicesRemap[edgeIndices[edgeIndex * 2 + 1]]);
            }
        }
    }

    const prevBytesPerIndex = bitsPerBucket / 8 * 2;
    const newBytesPerIndex = bitsPerBucket / 8;

    const originalSize = positionsCompressed.length * 2 + (indices.length + edgeIndices.length) * prevBytesPerIndex;

    let newSize = 0;
    let newPositions = -positionsCompressed.length / 3;

    buckets.forEach(bucket => {
        newSize += bucket.positionsCompressed.length * 2 + (bucket.indices.length + bucket.edgeIndices.length) * newBytesPerIndex;
        newPositions += bucket.positionsCompressed.length / 3;
    });
    if (newSize > originalSize) {
        return [mesh];
    }
    if (checkResult) {
        doCheckResult(buckets, mesh);
    }
    return buckets;
}

function doCheckResult(buckets, mesh) {
    const meshDict = {};

    let edgeIndicesCount = 0;

    buckets.forEach(bucket => {
        const indices = bucket.indices;
        const edgeIndices = bucket.edgeIndices;
        const positionsCompressed = bucket.positionsCompressed;

        for (let i = 0, len = indices.length; i < len; i += 3) {
            const key = positionsCompressed[indices[i] * 3] + "_" + positionsCompressed[indices[i] * 3 + 1] + "_" + positionsCompressed[indices[i] * 3 + 2] + "/" +
                positionsCompressed[indices[i + 1] * 3] + "_" + positionsCompressed[indices[i + 1] * 3 + 1] + "_" + positionsCompressed[indices[i + 1] * 3 + 2] + "/" +
                positionsCompressed[indices[i + 2] * 3] + "_" + positionsCompressed[indices[i + 2] * 3 + 1] + "_" + positionsCompressed[indices[i + 2] * 3 + 2];
            meshDict[key] = true;
        }

        edgeIndicesCount += bucket.edgeIndices.length / 2;

        for (let i = 0, len = edgeIndices.length; i < len; i += 2) {
            positionsCompressed[edgeIndices[i] * 3] + "_" + positionsCompressed[edgeIndices[i] * 3 + 1] + "_" + positionsCompressed[edgeIndices[i] * 3 + 2] + "/" +
                positionsCompressed[edgeIndices[i + 1] * 3] + "_" + positionsCompressed[edgeIndices[i + 1] * 3 + 1] + "_" + positionsCompressed[edgeIndices[i + 1] * 3 + 2] + "/";
        }
    });

    {
        const indices = mesh.indices;
        mesh.edgeIndices;
        const positionsCompressed = mesh.positionsCompressed;

        for (let i = 0, len = indices.length; i < len; i += 3) {
            const key = positionsCompressed[indices[i] * 3] + "_" + positionsCompressed[indices[i] * 3 + 1] + "_" + positionsCompressed[indices[i] * 3 + 2] + "/" +
                positionsCompressed[indices[i + 1] * 3] + "_" + positionsCompressed[indices[i + 1] * 3 + 1] + "_" + positionsCompressed[indices[i + 1] * 3 + 2] + "/" +
                positionsCompressed[indices[i + 2] * 3] + "_" + positionsCompressed[indices[i + 2] * 3 + 1] + "_" + positionsCompressed[indices[i + 2] * 3 + 2];

            if (!(key in meshDict)) {
                console.log("Not found " + key);
                throw "Ohhhh!";
            }
        }

        //  for (var i = 0, len = edgeIndices.length; i < len; i+=2)
        //  {
        //      var key = positionsCompressed[edgeIndices[i]*3] + "_" + positionsCompressed[edgeIndices[i]*3+1] + "_" + positionsCompressed[edgeIndices[i]*3+2] + "/" +
        //                positionsCompressed[edgeIndices[i+1]*3] + "_" + positionsCompressed[edgeIndices[i+1]*3+1] + "_" + positionsCompressed[edgeIndices[i+1]*3+2] + "/";

        //      if (!(key in edgesDict)) {
        //          var key2 = edgeIndices[i] + "_" + edgeIndices[i+1];

        //          console.log ("   - Not found " + key);
        //          console.log ("   - Not found " + key2);
        //         //  throw "Ohhhh2!";
        //      }
        //  }
    }
}

const tempFloatRGB = new Float32Array([0, 0, 0]);
const tempIntRGB = new Uint16Array([0, 0, 0]);

math.OBB3();

/**
 * An entity within a {@link SceneModel}
 *
 * * Created with {@link SceneModel#createEntity}
 * * Stored by ID in {@link SceneModel#entities}
 * * Has one or more {@link SceneModelMesh}es
 *
 * @implements {Entity}
 */
class SceneModelEntity {

    /**
     * @private
     */
    constructor(model, isObject, id, meshes, flags, lodCullable) {

        this._isObject = isObject;

        /**
         * The {@link Scene} to which this SceneModelEntity belongs.
         */
        this.scene = model.scene;

        /**
         * The {@link SceneModel} to which this SceneModelEntity belongs.
         */
        this.model = model;

        /**
         * Identifies if it's a SceneModelEntity
         */
        this.isSceneModelEntity = true;

        /**
         * The {@link SceneModelMesh}es belonging to this SceneModelEntity.
         *
         * * These are created with {@link SceneModel#createMesh} and registered in {@ilnk SceneModel#meshes}
         * * Each SceneModelMesh belongs to one SceneModelEntity
         */
        this.meshes = meshes;

        this._numPrimitives = 0;
        this._capMaterial = null;

        for (let i = 0, len = this.meshes.length; i < len; i++) {  // TODO: tidier way? Refactor?
            const mesh = this.meshes[i];
            mesh.parent = this;
            mesh.entity = this;
            this._numPrimitives += mesh.numPrimitives;
        }

        /**
         * The unique ID of this SceneModelEntity.
         */
        this.id = id;

        /**
         * The original system ID of this SceneModelEntity.
         */
        this.originalSystemId = math.unglobalizeObjectId(model.id, id);

        this._flags = flags;
        this._aabb = math.AABB3();
        this._aabbDirty = true;

        this._offset = math.vec3();
        this._colorizeUpdated = false;
        this._opacityUpdated = false;

        this._lodCullable = (!!lodCullable);
        this._culled = false;
        this._culledVFC = false;
        this._culledLOD = false;

        if (this._isObject) {
            model.scene._registerObject(this);
        }
    }

    _transformDirty() {
        this._aabbDirty = true;
        this.model._transformDirty();

    }

    _sceneModelDirty() { // Called by SceneModel when SceneModel's matrix is updated
        this._aabbDirty = true;
        for (let i = 0, len = this.meshes.length; i < len; i++) {
            this.meshes[i]._sceneModelDirty();
        }
    }

    /**
     * World-space 3D axis-aligned bounding box (AABB) of this SceneModelEntity.
     *
     * Represented by a six-element Float64Array containing the min/max extents of the
     * axis-aligned volume, ie. ````[xmin, ymin, zmin, xmax, ymax, zmax]````.
     *
     * @type {Float64Array}
     */
    get aabb() {
        if (this._aabbDirty) {
            math.collapseAABB3(this._aabb);
            for (let i = 0, len = this.meshes.length; i < len; i++) {
                math.expandAABB3(this._aabb, this.meshes[i].aabb);
            }
            this._aabbDirty = false;
        }
        // if (this._aabbDirty) {
        //     math.AABB3ToOBB3(this._aabb, tempOBB3a);
        //     math.transformOBB3(this.model.matrix, tempOBB3a);
        //     math.OBB3ToAABB3(tempOBB3a, this._worldAABB);
        //     this._worldAABB[0] += this._offset[0];
        //     this._worldAABB[1] += this._offset[1];
        //     this._worldAABB[2] += this._offset[2];
        //     this._worldAABB[3] += this._offset[0];
        //     this._worldAABB[4] += this._offset[1];
        //     this._worldAABB[5] += this._offset[2];
        //     this._aabbDirty = false;
        // }
        return this._aabb;
    }

    get isEntity() {
        return true;
    }

    /**
     * Returns false to indicate that this Entity subtype is not a model.
     * @returns {boolean}
     */
    get isModel() {
        return false;
    }

    /**
     * Returns ````true```` if this SceneModelEntity represents an object.
     *
     * When this is ````true````, the SceneModelEntity will be registered by {@link SceneModelEntity#id}
     * in {@link Scene#objects} and may also have a corresponding {@link MetaObject}.
     *
     * @type {Boolean}
     */
    get isObject() {
        return this._isObject;
    }

    get numPrimitives() {
        return this._numPrimitives;
    }

    /**
     * The approximate number of triangles in this SceneModelEntity.
     *
     * @type {Number}
     */
    get numTriangles() {
        return this._numPrimitives;
    }

    /**
     * Gets if this SceneModelEntity is visible.
     *
     * Only rendered when {@link SceneModelEntity#visible} is ````true````
     * and {@link SceneModelEntity#culled} is ````false````.
     *
     * When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#visible} are
     * both ````true```` the SceneModelEntity will be registered
     * by {@link SceneModelEntity#id} in {@link Scene#visibleObjects}.
     *
     * @type {Boolean}
     */
    get visible() {
        return this._getFlag(ENTITY_FLAGS.VISIBLE);
    }

    /**
     * Sets if this SceneModelEntity is visible.
     *
     * Only rendered when {@link SceneModelEntity#visible} is ````true```` and {@link SceneModelEntity#culled} is ````false````.
     *
     * When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#visible} are
     * both ````true```` the SceneModelEntity will be
     * registered by {@link SceneModelEntity#id} in {@link Scene#visibleObjects}.
     *
     * @type {Boolean}
     */
    set visible(visible) {
        if (!!(this._flags & ENTITY_FLAGS.VISIBLE) === visible) {
            return; // Redundant update
        }
        if (visible) {
            this._flags = this._flags | ENTITY_FLAGS.VISIBLE;
        } else {
            this._flags = this._flags & -2;
        }
        for (let i = 0, len = this.meshes.length; i < len; i++) {
            this.meshes[i]._setVisible(this._flags);
        }
        if (this._isObject) {
            this.model.scene._objectVisibilityUpdated(this);
        }
        this.model.glRedraw();
    }

    /**
     * Gets if this SceneModelEntity is highlighted.
     *
     * When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#highlighted} are both ````true```` the SceneModelEntity will be
     * registered by {@link SceneModelEntity#id} in {@link Scene#highlightedObjects}.
     *
     * @type {Boolean}
     */
    get highlighted() {
        return this._getFlag(ENTITY_FLAGS.HIGHLIGHTED);
    }

    /**
     * Sets if this SceneModelEntity is highlighted.
     *
     * When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#highlighted} are both ````true```` the SceneModelEntity will be
     * registered by {@link SceneModelEntity#id} in {@link Scene#highlightedObjects}.
     *
     * @type {Boolean}
     */
    set highlighted(highlighted) {
        if (!!(this._flags & ENTITY_FLAGS.HIGHLIGHTED) === highlighted) {
            return; // Redundant update
        }

        if (highlighted) {
            this._flags = this._flags | ENTITY_FLAGS.HIGHLIGHTED;
        } else {
            this._flags = this._flags & -513;
        }
        for (var i = 0, len = this.meshes.length; i < len; i++) {
            this.meshes[i]._setHighlighted(this._flags);
        }
        if (this._isObject) {
            this.model.scene._objectHighlightedUpdated(this);
        }
        this.model.glRedraw();
    }

    /**
     * Gets if this SceneModelEntity is xrayed.
     *
     * When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#xrayed} are both ````true``` the SceneModelEntity will be
     * registered by {@link SceneModelEntity#id} in {@link Scene#xrayedObjects}.
     *
     * @type {Boolean}
     */
    get xrayed() {
        return this._getFlag(ENTITY_FLAGS.XRAYED);
    }

    /**
     * Sets if this SceneModelEntity is xrayed.
     *
     * When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#xrayed} are both ````true``` the SceneModelEntity will be
     * registered by {@link SceneModelEntity#id} in {@link Scene#xrayedObjects}.
     *
     * @type {Boolean}
     */
    set xrayed(xrayed) {
        if (!!(this._flags & ENTITY_FLAGS.XRAYED) === xrayed) {
            return; // Redundant update
        }
        if (xrayed) {
            this._flags = this._flags | ENTITY_FLAGS.XRAYED;
        } else {
            this._flags = this._flags & -257;
        }
        for (let i = 0, len = this.meshes.length; i < len; i++) {
            this.meshes[i]._setXRayed(this._flags);
        }
        if (this._isObject) {
            this.model.scene._objectXRayedUpdated(this);
        }
        this.model.glRedraw();
    }

    /**
     * Gets if this SceneModelEntity is selected.
     *
     * When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#selected} are both ````true``` the SceneModelEntity will be
     * registered by {@link SceneModelEntity#id} in {@link Scene#selectedObjects}.
     *
     * @type {Boolean}
     */
    get selected() {
        return this._getFlag(ENTITY_FLAGS.SELECTED);
    }

    /**
     * Gets if this SceneModelEntity is selected.
     *
     * When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#selected} are both ````true``` the SceneModelEntity will be
     * registered by {@link SceneModelEntity#id} in {@link Scene#selectedObjects}.
     *
     * @type {Boolean}
     */
    set selected(selected) {
        if (!!(this._flags & ENTITY_FLAGS.SELECTED) === selected) {
            return; // Redundant update
        }
        if (selected) {
            this._flags = this._flags | ENTITY_FLAGS.SELECTED;
        } else {
            this._flags = this._flags & -1025;
        }
        for (let i = 0, len = this.meshes.length; i < len; i++) {
            this.meshes[i]._setSelected(this._flags);
        }
        if (this._isObject) {
            this.model.scene._objectSelectedUpdated(this);
        }
        this.model.glRedraw();
    }

    /**
     * Gets if this SceneModelEntity's edges are enhanced.
     *
     * @type {Boolean}
     */
    get edges() {
        return this._getFlag(ENTITY_FLAGS.EDGES);
    }

    /**
     * Sets if this SceneModelEntity's edges are enhanced.
     *
     * @type {Boolean}
     */
    set edges(edges) {
        if (!!(this._flags & ENTITY_FLAGS.EDGES) === edges) {
            return; // Redundant update
        }
        if (edges) {
            this._flags = this._flags | ENTITY_FLAGS.EDGES;
        } else {
            this._flags = this._flags & -2049;
        }
        for (var i = 0, len = this.meshes.length; i < len; i++) {
            this.meshes[i]._setEdges(this._flags);
        }
        this.model.glRedraw();
    }


    get culledVFC() {
        return !!(this._culledVFC);
    }

    set culledVFC(culled) {
        this._culledVFC = culled;
        this._setCulled();
    }

    get culledLOD() {
        return !!(this._culledLOD);
    }

    set culledLOD(culled) {
        this._culledLOD = culled;
        this._setCulled();
    }

    /**
     * Gets if this SceneModelEntity is culled.
     *
     * Only rendered when {@link SceneModelEntity#visible} is ````true```` and {@link SceneModelEntity#culled} is ````false````.
     *
     * @type {Boolean}
     */
    get culled() {
        return !!(this._culled);
        // return this._getFlag(ENTITY_FLAGS.CULLED);
    }

    /**
     * Sets if this SceneModelEntity is culled.
     *
     * Only rendered when {@link SceneModelEntity#visible} is ````true```` and {@link SceneModelEntity#culled} is ````false````.
     *
     * @type {Boolean}
     */
    set culled(culled) {
        this._culled = culled;
        this._setCulled();
    }

    _setCulled() {
        let culled = !!(this._culled) || !!(this._culledLOD && this._lodCullable) || !!(this._culledVFC);
        if (!!(this._flags & ENTITY_FLAGS.CULLED) === culled) {
            return; // Redundant update
        }
        if (culled) {
            this._flags = this._flags | ENTITY_FLAGS.CULLED;
        } else {
            this._flags = this._flags & -5;
        }
        for (var i = 0, len = this.meshes.length; i < len; i++) {
            this.meshes[i]._setCulled(this._flags);
        }
        this.model.glRedraw();
    }

    /**
     * Gets if this SceneModelEntity is clippable.
     *
     * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}.
     *
     * @type {Boolean}
     */
    get clippable() {
        return this._getFlag(ENTITY_FLAGS.CLIPPABLE);
    }

    /**
     * Sets if this SceneModelEntity is clippable.
     *
     * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}.
     *
     * @type {Boolean}
     */
    set clippable(clippable) {
        if ((!!(this._flags & ENTITY_FLAGS.CLIPPABLE)) === clippable) {
            return; // Redundant update
        }
        if (clippable) {
            this._flags = this._flags | ENTITY_FLAGS.CLIPPABLE;
        } else {
            this._flags = this._flags & -17;
        }
        for (var i = 0, len = this.meshes.length; i < len; i++) {
            this.meshes[i]._setClippable(this._flags);
        }
        this.model.glRedraw();
    }

    /**
     * Gets if this SceneModelEntity is included in boundary calculations.
     *
     * @type {Boolean}
     */
    get collidable() {
        return this._getFlag(ENTITY_FLAGS.COLLIDABLE);
    }

    /**
     * Sets if this SceneModelEntity is included in boundary calculations.
     *
     * @type {Boolean}
     */
    set collidable(collidable) {
        if (!!(this._flags & ENTITY_FLAGS.COLLIDABLE) === collidable) {
            return; // Redundant update
        }
        if (collidable) {
            this._flags = this._flags | ENTITY_FLAGS.COLLIDABLE;
        } else {
            this._flags = this._flags & -33;
        }
        for (var i = 0, len = this.meshes.length; i < len; i++) {
            this.meshes[i]._setCollidable(this._flags);
        }
    }

    /**
     * Gets if this SceneModelEntity is pickable.
     *
     * Picking is done via calls to {@link Scene#pick}.
     *
     * @type {Boolean}
     */
    get pickable() {
        return this._getFlag(ENTITY_FLAGS.PICKABLE);
    }

    /**
     * Sets if this SceneModelEntity is pickable.
     *
     * Picking is done via calls to {@link Scene#pick}.
     *
     * @type {Boolean}
     */
    set pickable(pickable) {
        if (!!(this._flags & ENTITY_FLAGS.PICKABLE) === pickable) {
            return; // Redundant update
        }
        if (pickable) {
            this._flags = this._flags | ENTITY_FLAGS.PICKABLE;
        } else {
            this._flags = this._flags & -9;
        }
        for (var i = 0, len = this.meshes.length; i < len; i++) {
            this.meshes[i]._setPickable(this._flags);
        }
    }

    /**
     * Gets the SceneModelEntity's RGB colorize color, multiplies by the SceneModelEntity's rendered fragment colors.
     *
     * Each element of the color is in range ````[0..1]````.
     *
     * @type {Number[]}
     */
    get colorize() { // [0..1, 0..1, 0..1]
        if (this.meshes.length === 0) {
            return null;
        }
        const colorize = this.meshes[0]._colorize;
        tempFloatRGB[0] = colorize[0] / 255.0; // Unquantize
        tempFloatRGB[1] = colorize[1] / 255.0;
        tempFloatRGB[2] = colorize[2] / 255.0;
        return tempFloatRGB;
    }

    /**
     * Sets the SceneModelEntity's RGB colorize color, multiplies by the SceneModelEntity's rendered fragment colors.
     *
     * Each element of the color is in range ````[0..1]````.
     *
     * @type {Number[]}
     */
    set colorize(color) { // [0..1, 0..1, 0..1]
        if (color) {
            tempIntRGB[0] = Math.floor(color[0] * 255.0); // Quantize
            tempIntRGB[1] = Math.floor(color[1] * 255.0);
            tempIntRGB[2] = Math.floor(color[2] * 255.0);
            for (let i = 0, len = this.meshes.length; i < len; i++) {
                this.meshes[i]._setColorize(tempIntRGB);
            }
        } else {
            for (let i = 0, len = this.meshes.length; i < len; i++) {
                this.meshes[i]._setColorize(null);
            }
        }
        if (this._isObject) {
            const colorized = (!!color);
            this.scene._objectColorizeUpdated(this, colorized);
            this._colorizeUpdated = colorized;
        }
        this.model.glRedraw();
    }

    /**
     * Gets the SceneModelEntity's opacity factor.
     *
     * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas.
     *
     * @type {Number}
     */
    get opacity() {
        if (this.meshes.length > 0) {
            return (this.meshes[0]._colorize[3] / 255.0);
        } else {
            return 1.0;
        }
    }

    /**
     * Sets the SceneModelEntity's opacity factor.
     *
     * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas.
     *
     * @type {Number}
     */
    set opacity(opacity) {
        if (this.meshes.length === 0) {
            return;
        }
        const opacityUpdated = (opacity !== null && opacity !== undefined);
        const lastOpacityQuantized = this.meshes[0]._colorize[3];
        let opacityQuantized = 255;
        if (opacityUpdated) {
            if (opacity < 0) {
                opacity = 0;
            } else if (opacity > 1) {
                opacity = 1;
            }
            opacityQuantized = Math.floor(opacity * 255.0); // Quantize
            if (lastOpacityQuantized === opacityQuantized) {
                return;
            }
        } else {
            opacityQuantized = 255.0;
            if (lastOpacityQuantized === opacityQuantized) {
                return;
            }
        }
        for (let i = 0, len = this.meshes.length; i < len; i++) {
            this.meshes[i]._setOpacity(opacityQuantized, this._flags);
        }
        if (this._isObject) {
            this.scene._objectOpacityUpdated(this, opacityUpdated);
            this._opacityUpdated = opacityUpdated;
        }
        this.model.glRedraw();
    }

    /**
     * Gets the SceneModelEntity's 3D World-space offset.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    get offset() {
        return this._offset;
    }

    /**
     * Sets the SceneModelEntity's 3D World-space offset.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    set offset(offset) {
        if (offset) {
            this._offset[0] = offset[0];
            this._offset[1] = offset[1];
            this._offset[2] = offset[2];
        } else {
            this._offset[0] = 0;
            this._offset[1] = 0;
            this._offset[2] = 0;
        }
        for (let i = 0, len = this.meshes.length; i < len; i++) {
            this.meshes[i]._setOffset(this._offset);
        }
        this._aabbDirty = true;
        this.model._aabbDirty = true;
        this.scene._aabbDirty = true;
        this.scene._objectOffsetUpdated(this, offset);
        this.model.glRedraw();
    }

    get saoEnabled() {
        return this.model.saoEnabled;
    }

    /**
     * Sets the SceneModelEntity's capMaterial that will be used on the caps generated when this entity is sliced
     *
     * Default value is ````null````.
     *
     * @type {Material}
     */
    set capMaterial(value) {
        if(!this.scene.readableGeometryEnabled) return;
        this._capMaterial = value instanceof Material ? value : null;
        this.scene._capMaterialUpdated(this.id, this.model.id);
    }

    /**
     * Gets the SceneModelEntity's capMaterial.
     *
     * Default value is ````null````.
     *
     * @type {Material}
     */
    get capMaterial() {
        return this._capMaterial;
    }

    getEachVertex(callback) {
        for (let i = 0, len = this.meshes.length; i < len; i++) {
            this.meshes[i].getEachVertex(callback);
        }
    }

    getEachIndex(callback) {
        for (let i = 0, len = this.meshes.length; i < len; i++) {
            this.meshes[i].getEachIndex(callback);
        }
    }

    getGeometryData() {
        let positionsBase = 0;
        const positions = [];
        const indices = [];

        for (let i = 0, len = this.meshes.length; i < len; i++) {
            const mesh = this.meshes[i];

            if (mesh.layer.readGeometryData) {
                const meshGeom = mesh.layer.readGeometryData(mesh.portionId);
    
                for (let j = 0, len = meshGeom.indices.length; j < len; j++) {
                    indices.push(meshGeom.indices[j]+positionsBase);
                }
    
                for (let j = 0, len = meshGeom.positions.length; j < len; j++) {
                    positions.push(meshGeom.positions[j]);
                }
    
                positionsBase += meshGeom.positions.length / 3;                
            }
        }

        return { indices, positions };
    }

    /**
     * Returns the volume of this SceneModelEntity.
     *
     * Only works when {@link Scene.readableGeometryEnabled | Scene.readableGeometryEnabled} is `true` and the
     * SceneModelEntity contains solid triangle meshes; returns `0` otherwise.
     *
     * @returns {number}
     */
    get volume() {
        let volume = 0;
        for (let i = 0, len = this.meshes.length; i < len; i++) {
            const meshVolume = this.meshes[i].volume;
            if (meshVolume < 0) {
                return -1;
            }
            volume += meshVolume;
        }
        return volume;
    }

    /**
     * Returns the surface area of this SceneModelEntity.
     *
     * Only works when {@link Scene.readableGeometryEnabled | Scene.readableGeometryEnabled} is `true` and the
     * SceneModelEntity contains triangle meshes; returns `0` otherwise.
     *
     * @returns {number}
     */
    get surfaceArea() {
        let surfaceArea = 0;
        for (let i = 0, len = this.meshes.length; i < len; i++) {
            const meshSurfaceArea = this.meshes[i].surfaceArea;
            if (meshSurfaceArea >= 0) {
                surfaceArea += meshSurfaceArea;
            }
        }
        return surfaceArea > 0 ? surfaceArea : -1;
    }

    _getFlag(flag) {
        return !!(this._flags & flag);
    }

    _finalize() {
        const scene = this.model.scene;
        if (this._isObject) {
            if (this.visible) {
                scene._objectVisibilityUpdated(this);
            }
            if (this.highlighted) {
                scene._objectHighlightedUpdated(this);
            }
            if (this.xrayed) {
                scene._objectXRayedUpdated(this);
            }
            if (this.selected) {
                scene._objectSelectedUpdated(this);
            }
        }
        for (let i = 0, len = this.meshes.length; i < len; i++) {
            this.meshes[i]._finalize(this._flags);
        }
    }

    _finalize2() {
        for (let i = 0, len = this.meshes.length; i < len; i++) {
            this.meshes[i]._finalize2();
        }
    }

    _destroy() {
        const scene = this.model.scene;
        if (this._isObject) {
            scene._deregisterObject(this);
            if (this.visible) {
                scene._deRegisterVisibleObject(this);
            }
            if (this.xrayed) {
                scene._deRegisterXRayedObject(this);
            }
            if (this.selected) {
                scene._deRegisterSelectedObject(this);
            }
            if (this.highlighted) {
                scene._deRegisterHighlightedObject(this);
            }
            if (this._colorizeUpdated) {
                this.scene._deRegisterColorizedObject(this);
            }
            if (this._opacityUpdated) {
                this.scene._deRegisterOpacityObject(this);
            }
            if (this._offset && (this._offset[0] !== 0 || this._offset[1] !== 0 || this._offset[2] !== 0)) {
                this.scene._deRegisterOffsetObject(this);
            }
        }
        for (let i = 0, len = this.meshes.length; i < len; i++) {
            this.meshes[i]._destroy();
        }
        scene._aabbDirty = true;
    }
}

const angleAxis = math.vec4(4);
const q1 = math.vec4();
const q2 = math.vec4();
const xAxis = math.vec3([1, 0, 0]);
const yAxis = math.vec3([0, 1, 0]);
const zAxis = math.vec3([0, 0, 1]);

math.vec3(3);
math.vec3(3);

const identityMat = math.identityMat4();

/**
 * A dynamically-updatable transform within a {@link SceneModel}.
 *
 * * Can be composed into hierarchies
 * * Shared by multiple {@link SceneModelMesh}es
 * * Created with {@link SceneModel#createTransform}
 * * Stored by ID in {@link SceneModel#transforms}
 * * Referenced by {@link SceneModelMesh#transform}
 */
class SceneModelTransform {

    /**
     * @private
     */
    constructor(cfg) {
        this._model = cfg.model;

        /**
         * Unique ID of this SceneModelTransform.
         *
         * The SceneModelTransform is registered against this ID in {@link SceneModel#transforms}.
         */
        this.id = cfg.id;

        this._parentTransform = cfg.parent;
        this._childTransforms = [];
        this._meshes = [];
        this._scale = new Float32Array([1,1,1]);
        this._quaternion = math.identityQuaternion(new Float32Array(4));
        this._rotation = new Float32Array(3);
        this._position = new Float32Array(3);
        this._localMatrix = math.identityMat4(new Float32Array(16));
        this._worldMatrix = math.identityMat4(new Float32Array(16));
        this._localMatrixDirty = true;
        this._worldMatrixDirty = true;

        if (cfg.matrix) {
            this.matrix = cfg.matrix;
        } else {
            this.scale = cfg.scale;
            this.position = cfg.position;
            if (cfg.quaternion) ; else {
                this.rotation = cfg.rotation;
            }
        }
        if (cfg.parent) {
            cfg.parent._addChildTransform(this);
        }
    }

    _addChildTransform(childTransform) {
        this._childTransforms.push(childTransform);
        childTransform._parentTransform = this;
        childTransform._transformDirty();
        childTransform._setSubtreeAABBsDirty(this);
    }

    _addMesh(mesh) {
        this._meshes.push(mesh);
        mesh.transform = this;
        // childTransform._setWorldMatrixDirty();
        // childTransform._setAABBDirty();
    }

    /**
     * The optional parent SceneModelTransform.
     *
     * @type {SceneModelTransform}
     */
    get parentTransform() {
        return this._parentTransform;
    }

    /**
     * The {@link SceneModelMesh}es transformed by this SceneModelTransform.
     *
     * @returns {[]}
     */
    get meshes() {
        return this._meshes;
    }

    /**
     * Sets the SceneModelTransform's local translation.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    set position(value) {
        this._position.set(value || [0, 0, 0]);
        this._setLocalMatrixDirty();
        this._model.glRedraw();
    }

    /**
     * Gets the SceneModelTransform's translation.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    get position() {
        return this._position;
    }

    /**
     * Sets the SceneModelTransform's rotation, as Euler angles given in degrees, for each of the X, Y and Z axis.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    set rotation(value) {
        this._rotation.set(value || [0, 0, 0]);
        math.eulerToQuaternion(this._rotation, "XYZ", this._quaternion);
        this._setLocalMatrixDirty();
        this._model.glRedraw();
    }

    /**
     * Gets the SceneModelTransform's rotation, as Euler angles given in degrees, for each of the X, Y and Z axis.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    get rotation() {
        return this._rotation;
    }

    /**
     * Sets the SceneModelTransform's rotation quaternion.
     *
     * Default value is ````[0,0,0,1]````.
     *
     * @type {Number[]}
     */
    set quaternion(value) {
        this._quaternion.set(value || [0, 0, 0, 1]);
        math.quaternionToEuler(this._quaternion, "XYZ", this._rotation);
        this._setLocalMatrixDirty();
        this._model.glRedraw();
    }

    /**
     * Gets the SceneModelTransform's rotation quaternion.
     *
     * Default value is ````[0,0,0,1]````.
     *
     * @type {Number[]}
     */
    get quaternion() {
        return this._quaternion;
    }

    /**
     * Sets the SceneModelTransform's scale.
     *
     * Default value is ````[1,1,1]````.
     *
     * @type {Number[]}
     */
    set scale(value) {
        this._scale.set(value || [1, 1, 1]);
        this._setLocalMatrixDirty();
        this._model.glRedraw();
    }

    /**
     * Gets the SceneModelTransform's scale.
     *
     * Default value is ````[1,1,1]````.
     *
     * @type {Number[]}
     */
    get scale() {
        return this._scale;
    }

    /**
     * Sets the SceneModelTransform's transform matrix.
     *
     * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````.
     *
     * @type {Number[]}
     */
    set matrix(value) {
        if (!this._localMatrix) {
            this._localMatrix = math.identityMat4();
        }
        this._localMatrix.set(value || identityMat);
        math.decomposeMat4(this._localMatrix, this._position, this._quaternion, this._scale);
        this._localMatrixDirty = false;
        this._transformDirty();
        this._model.glRedraw();
    }

    /**
     * Gets the SceneModelTransform's transform matrix.
     *
     * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````.
     *
     * @type {Number[]}
     */
    get matrix() {
        if (this._localMatrixDirty) {
            if (!this._localMatrix) {
                this._localMatrix = math.identityMat4();
            }
            math.composeMat4(this._position, this._quaternion, this._scale, this._localMatrix);
            this._localMatrixDirty = false;
        }
        return this._localMatrix;
    }

    /**
     * Gets the SceneModelTransform's World matrix.
     *
     * @property worldMatrix
     * @type {Number[]}
     */
    get worldMatrix() {
        if (this._worldMatrixDirty) {
            this._buildWorldMatrix();
        }
        return this._worldMatrix;
    }

    /**
     * Rotates the SceneModelTransform about the given axis by the given increment.
     *
     * @param {Number[]} axis Local axis about which to rotate.
     * @param {Number} angle Angle increment in degrees.
     */
    rotate(axis, angle) {
        angleAxis[0] = axis[0];
        angleAxis[1] = axis[1];
        angleAxis[2] = axis[2];
        angleAxis[3] = angle * math.DEGTORAD;
        math.angleAxisToQuaternion(angleAxis, q1);
        math.mulQuaternions(this.quaternion, q1, q2);
        this.quaternion = q2;
        this._setLocalMatrixDirty();
        this._model.glRedraw();
        return this;
    }

    /**
     * Rotates the SceneModelTransform about the given World-space axis by the given increment.
     *
     * @param {Number[]} axis Local axis about which to rotate.
     * @param {Number} angle Angle increment in degrees.
     */
    rotateOnWorldAxis(axis, angle) {
        angleAxis[0] = axis[0];
        angleAxis[1] = axis[1];
        angleAxis[2] = axis[2];
        angleAxis[3] = angle * math.DEGTORAD;
        math.angleAxisToQuaternion(angleAxis, q1);
        math.mulQuaternions(q1, this.quaternion, q1);
        //this.quaternion.premultiply(q1);
        return this;
    }

    /**
     * Rotates the SceneModelTransform about the local X-axis by the given increment.
     *
     * @param {Number} angle Angle increment in degrees.
     */
    rotateX(angle) {
        return this.rotate(xAxis, angle);
    }

    /**
     * Rotates the SceneModelTransform about the local Y-axis by the given increment.
     *
     * @param {Number} angle Angle increment in degrees.
     */
    rotateY(angle) {
        return this.rotate(yAxis, angle);
    }

    /**
     * Rotates the SceneModelTransform about the local Z-axis by the given increment.
     *
     * @param {Number} angle Angle increment in degrees.
     */
    rotateZ(angle) {
        return this.rotate(zAxis, angle);
    }

    /**
     * Translates the SceneModelTransform along the local axis by the given increment.
     *
     * @param {Number[]} axis Normalized local space 3D vector along which to translate.
     * @param {Number} distance Distance to translate along  the vector.
     */
    translate(axis) {
        this._position[0] += axis[0];
        this._position[1] += axis[1];
        this._position[2] += axis[2];
        this._setLocalMatrixDirty();
        this._model.glRedraw();
        return this;
    }

    /**
     * Translates the SceneModelTransform along the local X-axis by the given increment.
     *
     * @param {Number} distance Distance to translate along  the X-axis.
     */
    translateX(distance) {
        this._position[0] += distance;
        this._setLocalMatrixDirty();
        this._model.glRedraw();
        return this;
    }

    /**
     * Translates the SceneModelTransform along the local Y-axis by the given increment.
     *
     * @param {Number} distance Distance to translate along  the Y-axis.
     */
    translateY(distance) {
        this._position[1] += distance;
        this._setLocalMatrixDirty();
        this._model.glRedraw();
        return this;
    }

    /**
     * Translates the SceneModelTransform along the local Z-axis by the given increment.
     *
     * @param {Number} distance Distance to translate along  the Z-axis.
     */
    translateZ(distance) {
        this._position[2] += distance;
        this._setLocalMatrixDirty();
        this._model.glRedraw();
        return this;
    }

    _setLocalMatrixDirty() {
        this._localMatrixDirty = true;
        this._transformDirty();
    }

    _transformDirty() {
        this._worldMatrixDirty = true;
        for (let i = 0, len = this._childTransforms.length; i < len; i++) {
            const childTransform = this._childTransforms[i];
            childTransform._transformDirty();
            if (childTransform._meshes && childTransform._meshes.length > 0) {
               const meshes = childTransform._meshes;
               for (let j =0, lenj = meshes.length; j < lenj; j++) {
                 meshes[j]._transformDirty();
               }
            }
        }
        if (this._meshes && this._meshes.length > 0) {
            const meshes = this._meshes;
            for (let j =0, lenj = meshes.length; j < lenj; j++) {
                meshes[j]._transformDirty();
            }
        }
    }

    _buildWorldMatrix() {
        const localMatrix = this.matrix;
        if (!this._parentTransform) {
            for (let i = 0, len = localMatrix.length; i < len; i++) {
                this._worldMatrix[i] = localMatrix[i];
            }
        } else {
            math.mulMat4(this._parentTransform.worldMatrix, localMatrix, this._worldMatrix);
        }
        this._worldMatrixDirty = false;
    }

    _setSubtreeAABBsDirty(sceneTransform) {
        sceneTransform._aabbDirty = true;
        if (sceneTransform._childTransforms) {
            for (let i = 0, len = sceneTransform._childTransforms.length; i < len; i++) {
                this._setSubtreeAABBsDirty(sceneTransform._childTransforms[i]);
            }
        }
    }
}

const tempVec3a$d = math.vec3();

const tempOBB3 = math.OBB3();
const tempQuaternion = math.vec4();

const DEFAULT_SCALE = math.vec3([1, 1, 1]);
const DEFAULT_POSITION = math.vec3([0, 0, 0]);
math.vec3([0, 0, 0]);
const DEFAULT_QUATERNION = math.identityQuaternion();
const DEFAULT_MATRIX = math.identityMat4();

const DEFAULT_COLOR_TEXTURE_ID = "defaultColorTexture";
const DEFAULT_METAL_ROUGH_TEXTURE_ID = "defaultMetalRoughTexture";
const DEFAULT_NORMALS_TEXTURE_ID = "defaultNormalsTexture";
const DEFAULT_EMISSIVE_TEXTURE_ID = "defaultEmissiveTexture";
const DEFAULT_OCCLUSION_TEXTURE_ID = "defaultOcclusionTexture";
const DEFAULT_TEXTURE_SET_ID = "defaultTextureSet";

const defaultCompressedColor = new Uint8Array([255, 255, 255]);

const VBO_INSTANCED = 0;
const VBO_BATCHED = 1;
const DTX = 2;

/**
 * @desc A high-performance model representation for efficient rendering and low memory usage.
 *
 * # Examples
 *
 * Internally, SceneModel uses a combination of several different techniques to render and represent
 * the different parts of a typical model. Each of the live examples at these links is designed to "unit test" one of these
 * techniques, in isolation. If some bug occurs in SceneModel, we use these tests to debug, but they also
 * serve to demonstrate how to use the capabilities of SceneModel programmatically.
 *
 * * [Loading building models into SceneModels](/examples/buildings)
 * * [Loading city models into SceneModels](/examples/cities)
 * * [Loading LiDAR scans into SceneModels](/examples/lidar)
 * * [Loading CAD models into SceneModels](/examples/cad)
 * * [SceneModel feature tests](/examples/scenemodel)
 *
 * # Overview
 *
 * While xeokit's standard [scene graph](https://github.com/xeokit/xeokit-sdk/wiki/Scene-Graphs) is great for gizmos and medium-sized models, it doesn't scale up to millions of objects in terms of memory and rendering efficiency.
 *
 * For huge models, we have the ````SceneModel```` representation, which is optimized to pack large amounts of geometry into memory and render it efficiently using WebGL.
 *
 * ````SceneModel```` is the default model representation loaded by  (at least) {@link GLTFLoaderPlugin}, {@link XKTLoaderPlugin} and  {@link WebIFCLoaderPlugin}.
 *
 * In this tutorial you'll learn how to use ````SceneModel```` to create high-detail content programmatically. Ordinarily you'd be learning about ````SceneModel```` if you were writing your own model loader plugins.
 *
 * # Contents
 *
 * - [SceneModel](#DataTextureSceneModel)
 * - [GPU-Resident Geometry](#gpu-resident-geometry)
 * - [Picking](#picking)
 * - [Example 1: Geometry Instancing](#example-1--geometry-instancing)
 * - [Finalizing a SceneModel](#finalizing-a-DataTextureSceneModel)
 * - [Finding Entities](#finding-entities)
 * - [Example 2: Geometry Batching](#example-2--geometry-batching)
 * - [Classifying with Metadata](#classifying-with-metadata)
 * - [Querying Metadata](#querying-metadata)
 * - [Metadata Structure](#metadata-structure)
 * - [RTC Coordinates](#rtc-coordinates-for-double-precision)
 *   - [Example 3: RTC Coordinates with Geometry Instancing](#example-2--rtc-coordinates-with-geometry-instancing)
 *   - [Example 4: RTC Coordinates with Geometry Batching](#example-2--rtc-coordinates-with-geometry-batching)
 *
 * ## SceneModel
 *
 * ````SceneModel```` uses two rendering techniques internally:
 *
 * 1. ***Geometry batching*** for unique geometries, combining those into a single WebGL geometry buffer, to render in one draw call, and
 * 2. ***geometry instancing*** for geometries that are shared by multiple meshes, rendering all instances of each shared geometry in one draw call.
 *
 * <br>
 * These techniques come with certain limitations:
 *
 * * Non-realistic rendering - while scene graphs can use xeokit's full set of material workflows, ````SceneModel```` uses simple Lambertian shading without textures.
 * * Static transforms - transforms within a ````SceneModel```` are static and cannot be dynamically translated, rotated and scaled the way {@link Node}s and {@link Mesh}es in scene graphs can.
 * * Immutable model representation - while scene graph {@link Node}s and
 * {@link Mesh}es can be dynamically plugged together, ````SceneModel```` is immutable,
 * since it packs its geometries into buffers and instanced arrays.
 *
 * ````SceneModel````'s API allows us to exploit batching and instancing, while exposing its elements as
 * abstract {@link Entity} types.
 *
 * {@link Entity} is the abstract base class for
 * the various xeokit components that represent models, objects, or anonymous visible elements. An Entity has a unique ID and can be
 * individually shown, hidden, selected, highlighted, ghosted, culled, picked and clipped, and has its own World-space boundary.
 *
 * * A ````SceneModel```` is an {@link Entity} that represents a model.
 * * A ````SceneModel```` represents each of its objects with an {@link Entity}.
 * * Each {@link Entity} has one or more meshes that define its shape.
 * * Each mesh has either its own unique geometry, or shares a geometry with other meshes.
 *
 * ## GPU-Resident Geometry
 *
 * For a low memory footprint, ````SceneModel```` stores its geometries in GPU memory only, compressed (quantized) as integers. Unfortunately, GPU-resident geometry is
 * not readable by JavaScript.
 *
 *
 * ## Example 1: Geometry Instancing
 *
 * In the example below, we'll use a ````SceneModel````
 * to build a simple table model using geometry instancing.
 *
 * We'll start by adding a reusable box-shaped geometry to our ````SceneModel````.
 *
 * Then, for each object in our model we'll add an {@link Entity}
 * that has a mesh that instances our box geometry, transforming and coloring the instance.
 *
 * [![](http://xeokit.io/img/docs/sceneGraph.png)](https://xeokit.github.io/xeokit-sdk/examples/index.html#sceneRepresentation_SceneModel_instancing)
 *
 * ````javascript
 * import {Viewer, SceneModel} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.scene.camera.eye = [-21.80, 4.01, 6.56];
 * viewer.scene.camera.look = [0, -5.75, 0];
 * viewer.scene.camera.up = [0.37, 0.91, -0.11];
 *
 * // Build a SceneModel representing a table
 * // with four legs, using geometry instancing
 *
 * const sceneModel = new SceneModel(viewer.scene, {
 *     id: "table",
 *     isModel: true, // <--- Registers SceneModel in viewer.scene.models
 *     position: [0, 0, 0],
 *     scale: [1, 1, 1],
 *     rotation: [0, 0, 0]
 * });
 *
 * // Create a reusable geometry within the SceneModel
 * // We'll instance this geometry by five meshes
 *
 * sceneModel.createGeometry({
 *
 *     id: "myBoxGeometry",
 *
 *     // The primitive type - allowed values are "points", "lines" and "triangles".
 *     // See the OpenGL/WebGL specification docs
 *     // for how the coordinate arrays are supposed to be laid out.
 *     primitive: "triangles",
 *
 *     // The vertices - eight for our cube, each
 *     // one spanning three array elements for X,Y and Z
 *     positions: [
 *          1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, // v0-v1-v2-v3 front
 *          1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, // v0-v3-v4-v1 right
 *          1, 1, 1, 1, 1, -1, -1, 1, -1, -1, 1, 1, // v0-v1-v6-v1 top
 *          -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, // v1-v6-v7-v2 left
 *          -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, // v7-v4-v3-v2 bottom
 *          1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, -1 // v4-v7-v6-v1 back
 *     ],
 *
 *     // Normal vectors, one for each vertex
 *     normals: [
 *         0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, // v0-v1-v2-v3 front
 *         1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v0-v3-v4-v5 right
 *         0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, // v0-v5-v6-v1 top
 *         -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, // v1-v6-v7-v2 left
 *         0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, // v7-v4-v3-v2 bottom
 *         0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1 // v4-v7-v6-v5 back
 *     ],
 *
 *     // Indices - these organise the positions and and normals
 *     // into geometric primitives in accordance with the "primitive" parameter,
 *     // in this case a set of three indices for each triangle.
 *     //
 *     // Note that each triangle is specified in counter-clockwise winding order.
 *     //
 *     indices: [
 *         0, 1, 2, 0, 2, 3, // front
 *         4, 5, 6, 4, 6, 7, // right
 *         8, 9, 10, 8, 10, 11, // top
 *         12, 13, 14, 12, 14, 15, // left
 *         16, 17, 18, 16, 18, 19, // bottom
 *         20, 21, 22, 20, 22, 23
 *     ]
 * });
 *
 * // Red table leg
 *
 * sceneModel.createMesh({
 *     id: "redLegMesh",
 *     geometryId: "myBoxGeometry",
 *     position: [-4, -6, -4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [1, 0.3, 0.3]
 * });
 *
 * sceneModel.createEntity({
 *     id: "redLeg",
 *     meshIds: ["redLegMesh"],
 *     isObject: true // <---- Registers Entity by ID on viewer.scene.objects
 * });
 *
 * // Green table leg
 *
 * sceneModel.createMesh({
 *     id: "greenLegMesh",
 *     geometryId: "myBoxGeometry",
 *     position: [4, -6, -4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [0.3, 1.0, 0.3]
 * });
 *
 * sceneModel.createEntity({
 *     id: "greenLeg",
 *     meshIds: ["greenLegMesh"],
 *     isObject: true // <---- Registers Entity by ID on viewer.scene.objects
 * });
 *
 * // Blue table leg
 *
 * sceneModel.createMesh({
 *     id: "blueLegMesh",
 *     geometryId: "myBoxGeometry",
 *     position: [4, -6, 4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [0.3, 0.3, 1.0]
 * });
 *
 * sceneModel.createEntity({
 *     id: "blueLeg",
 *     meshIds: ["blueLegMesh"],
 *     isObject: true // <---- Registers Entity by ID on viewer.scene.objects
 * });
 *
 * // Yellow table leg
 *
 * sceneModel.createMesh({
 *      id: "yellowLegMesh",
 *      geometryId: "myBoxGeometry",
 *      position: [-4, -6, 4],
 *      scale: [1, 3, 1],
 *      rotation: [0, 0, 0],
 *      color: [1.0, 1.0, 0.0]
 * });
 *
 * sceneModel.createEntity({
 *     id: "yellowLeg",
 *     meshIds: ["yellowLegMesh"],
 *     isObject: true // <---- Registers Entity by ID on viewer.scene.objects
 * });
 *
 * // Purple table top
 *
 * sceneModel.createMesh({
 *     id: "purpleTableTopMesh",
 *     geometryId: "myBoxGeometry",
 *     position: [0, -3, 0],
 *     scale: [6, 0.5, 6],
 *     rotation: [0, 0, 0],
 *     color: [1.0, 0.3, 1.0]
 * });
 *
 * sceneModel.createEntity({
 *     id: "purpleTableTop",
 *     meshIds: ["purpleTableTopMesh"],
 *     isObject: true // <---- Registers Entity by ID on viewer.scene.objects
 * });
 *  ````
 *
 * ## Finalizing a SceneModel
 *
 * Before we can view and interact with our ````SceneModel````, we need to **finalize** it. Internally, this causes the ````SceneModel```` to build the
 * vertex buffer objects (VBOs) that support our geometry instances. When using geometry batching (see next example),
 * this causes ````SceneModel```` to build the VBOs that combine the batched geometries. Note that you can do both instancing and
 * batching within the same ````SceneModel````.
 *
 * Once finalized, we can't add anything more to our ````SceneModel````.
 *
 * ```` javascript
 * SceneModel.finalize();
 * ````
 *
 * ## Finding Entities
 *
 * As mentioned earlier, {@link Entity} is
 * the abstract base class for components that represent models, objects, or just
 * anonymous visible elements.
 *
 * Since we created configured our ````SceneModel```` with ````isModel: true````,
 * we're able to find it as an Entity by ID in ````viewer.scene.models````. Likewise, since
 * we configured each of its Entities with ````isObject: true````, we're able to
 * find them in  ````viewer.scene.objects````.
 *
 *
 * ````javascript
 * // Get the whole table model Entity
 * const table = viewer.scene.models["table"];
 *
 *  // Get some leg object Entities
 * const redLeg = viewer.scene.objects["redLeg"];
 * const greenLeg = viewer.scene.objects["greenLeg"];
 * const blueLeg = viewer.scene.objects["blueLeg"];
 * ````
 *
 * ## Example 2: Geometry Batching
 *
 * Let's once more use a ````SceneModel````
 * to build the simple table model, this time exploiting geometry batching.
 *
 *  [![](http://xeokit.io/img/docs/sceneGraph.png)](https://xeokit.github.io/xeokit-sdk/examples/index.html#sceneRepresentation_SceneModel_batching)
 *
 * ````javascript
 * import {Viewer, SceneModel} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.scene.camera.eye = [-21.80, 4.01, 6.56];
 * viewer.scene.camera.look = [0, -5.75, 0];
 * viewer.scene.camera.up = [0.37, 0.91, -0.11];
 *
 * // Create a SceneModel representing a table with four legs, using geometry batching
 * const sceneModel = new SceneModel(viewer.scene, {
 *     id: "table",
 *     isModel: true,  // <--- Registers SceneModel in viewer.scene.models
 *     position: [0, 0, 0],
 *     scale: [1, 1, 1],
 *     rotation: [0, 0, 0]
 * });
 *
 * // Red table leg
 *
 * sceneModel.createMesh({
 *     id: "redLegMesh",
 *
 *     // Geometry arrays are same as for the earlier batching example
 *     primitive: "triangles",
 *     positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
 *     normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
 *     indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
 *     position: [-4, -6, -4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [1, 0.3, 0.3]
 * });
 *
 * sceneModel.createEntity({
 *     id: "redLeg",
 *     meshIds: ["redLegMesh"],
 *     isObject: true // <---- Registers Entity by ID on viewer.scene.objects
 * });
 *
 * // Green table leg
 *
 * sceneModel.createMesh({
 *     id: "greenLegMesh",
 *     primitive: "triangles",
 *     primitive: "triangles",
 *     positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
 *     normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
 *     indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
 *     position: [4, -6, -4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [0.3, 1.0, 0.3]
 * });
 *
 * sceneModel.createEntity({
 *     id: "greenLeg",
 *     meshIds: ["greenLegMesh"],
 *     isObject: true // <---- Registers Entity by ID on viewer.scene.objects
 * });
 *
 * // Blue table leg
 *
 * sceneModel.createMesh({
 *     id: "blueLegMesh",
 *     primitive: "triangles",
 *     positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
 *     normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
 *     indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
 *     position: [4, -6, 4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [0.3, 0.3, 1.0]
 * });
 *
 * sceneModel.createEntity({
 *     id: "blueLeg",
 *     meshIds: ["blueLegMesh"],
 *     isObject: true // <---- Registers Entity by ID on viewer.scene.objects
 * });
 *
 * // Yellow table leg object
 *
 * sceneModel.createMesh({
 *     id: "yellowLegMesh",
 *     positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
 *     normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
 *     indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
 *     position: [-4, -6, 4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [1.0, 1.0, 0.0]
 * });
 *
 * sceneModel.createEntity({
 *     id: "yellowLeg",
 *     meshIds: ["yellowLegMesh"],
 *     isObject: true // <---- Registers Entity by ID on viewer.scene.objects
 * });
 *
 * // Purple table top
 *
 * sceneModel.createMesh({
 *     id: "purpleTableTopMesh",
 *     positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
 *     normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
 *     indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
 *     position: [0, -3, 0],
 *     scale: [6, 0.5, 6],
 *     rotation: [0, 0, 0],
 *     color: [1.0, 0.3, 1.0]
 * });
 *
 * sceneModel.createEntity({
 *     id: "purpleTableTop",
 *     meshIds: ["purpleTableTopMesh"],
 *     isObject: true // <---- Registers Entity by ID on viewer.scene.objects
 * });
 *
 * // Finalize the SceneModel.
 *
 * SceneModel.finalize();
 *
 * // Find BigModelNodes by their model and object IDs
 *
 * // Get the whole table model
 * const table = viewer.scene.models["table"];
 *
 * // Get some leg objects
 * const redLeg = viewer.scene.objects["redLeg"];
 * const greenLeg = viewer.scene.objects["greenLeg"];
 * const blueLeg = viewer.scene.objects["blueLeg"];
 * ````
 *
 * ## Classifying with Metadata
 *
 * In the previous examples, we used ````SceneModel```` to build
 * two versions of the same table model, to demonstrate geometry batching and geometry instancing.
 *
 * We'll now classify our {@link Entity}s with metadata. This metadata
 * will work the same for both our examples, since they create the exact same structure of {@link Entity}s
 * to represent their models and objects. The abstract Entity type is, after all, intended to provide an abstract interface through which differently-implemented scene content can be accessed uniformly.
 *
 * To create the metadata, we'll create a {@link MetaModel} for our model,
 * with a {@link MetaObject} for each of it's objects. The MetaModel and MetaObjects
 * get the same IDs as the {@link Entity}s that represent their model and objects within our scene.
 *
 * ```` javascript
 * const furnitureMetaModel = viewer.metaScene.createMetaModel("furniture", {         // Creates a MetaModel in the MetaScene
 *
 *      "projectId": "myTableProject",
 *      "revisionId": "V1.0",
 *
 *      "metaObjects": [
 *          {                               // Creates a MetaObject in the MetaModel
 *              "id": "table",
 *              "name": "Table",            // Same ID as an object Entity
 *              "type": "furniture",        // Arbitrary type, could be IFC type
 *              "properties": {             // Arbitrary properties, could be IfcPropertySet
 *                  "cost": "200"
 *              }
 *          },
 *          {
 *              "id": "redLeg",
 *              "name": "Red table Leg",
 *              "type": "leg",
 *              "parent": "table",           // References first MetaObject as parent
 *              "properties": {
 *                  "material": "wood"
 *              }
 *          },
 *          {
 *              "id": "greenLeg",           // Node with corresponding id does not need to exist
 *              "name": "Green table leg",  // and MetaObject does not need to exist for Node with an id
 *              "type": "leg",
 *              "parent": "table",
 *              "properties": {
 *                  "material": "wood"
 *              }
 *          },
 *          {
 *              "id": "blueLeg",
 *              "name": "Blue table leg",
 *              "type": "leg",
 *              "parent": "table",
 *              "properties": {
 *                  "material": "wood"
 *              }
 *          },
 *          {
 *              "id": "yellowLeg",
 *              "name": "Yellow table leg",
 *              "type": "leg",
 *              "parent": "table",
 *              "properties": {
 *                  "material": "wood"
 *              }
 *          },
 *          {
 *              "id": "tableTop",
 *              "name": "Purple table top",
 *              "type": "surface",
 *              "parent": "table",
 *              "properties": {
 *                  "material": "formica",
 *                  "width": "60",
 *                  "depth": "60",
 *                  "thickness": "5"
 *              }
 *          }
 *      ]
 *  });
 * ````
 *
 * ## Querying Metadata
 *
 * Having created and classified our model (either the instancing or batching example), we can now find the {@link MetaModel}
 * and {@link MetaObject}s using the IDs of their
 * corresponding {@link Entity}s.
 *
 * ````JavaScript
 * const furnitureMetaModel = scene.metaScene.metaModels["furniture"];
 *
 * const redLegMetaObject = scene.metaScene.metaObjects["redLeg"];
 * ````
 *
 * In the snippet below, we'll log metadata on each {@link Entity} we click on:
 *
 * ````JavaScript
 * viewer.scene.input.on("mouseclicked", function (coords) {
 *
 *      const hit = viewer.scene.pick({
 *          canvasPos: coords
 *      });
 *
 *      if (hit) {
 *          const entity = hit.entity;
 *          const metaObject = viewer.metaScene.metaObjects[entity.id];
 *          if (metaObject) {
 *              console.log(JSON.stringify(metaObject.getJSON(), null, "\t"));
 *          }
 *      }
 *  });
 * ````
 *
 * ## Metadata Structure
 *
 * The {@link MetaModel}
 * organizes its {@link MetaObject}s in
 * a tree that describes their structural composition:
 *
 * ````JavaScript
 * // Get metadata on the root object
 * const tableMetaObject = furnitureMetaModel.rootMetaObject;
 *
 * // Get metadata on the leg objects
 * const redLegMetaObject = tableMetaObject.children[0];
 * const greenLegMetaObject = tableMetaObject.children[1];
 * const blueLegMetaObject = tableMetaObject.children[2];
 * const yellowLegMetaObject = tableMetaObject.children[3];
 * ````
 *
 * Given an {@link Entity}, we can find the object or model of which it is a part, or the objects that comprise it. We can also generate UI
 * components from the metadata, such as the tree view demonstrated in [this demo](https://xeokit.github.io/xeokit-sdk/examples/index.html#BIMOffline_glTF_OTCConferenceCenter).
 *
 * This hierarchy allows us to express the hierarchical structure of a model while representing it in
 * various ways in the 3D scene (such as with ````SceneModel````, which
 * has a non-hierarchical scene representation).
 *
 * Note also that a {@link MetaObject} does not need to have a corresponding
 * {@link Entity} and vice-versa.
 *
 * # RTC Coordinates for Double Precision
 *
 * ````SceneModel```` can emulate 64-bit precision on GPUs using relative-to-center (RTC) coordinates.
 *
 * Consider a model that contains many small objects, but with such large spatial extents that 32 bits of GPU precision (accurate to ~7 digits) will not be sufficient to render all of the the objects without jittering.
 *
 * To prevent jittering, we could spatially subdivide the objects into "tiles". Each tile would have a center position, and the positions of the objects within the tile would be relative to that center ("RTC coordinates").
 *
 * While the center positions of the tiles would be 64-bit values, the object positions only need to be 32-bit.
 *
 * Internally, when rendering an object with RTC coordinates, xeokit first temporarily translates the camera viewing matrix by the object's tile's RTC center, on the CPU, using 64-bit math.
 *
 * Then xeokit loads the viewing matrix into its WebGL shaders, where math happens at 32-bit precision. Within the shaders, the matrix is effectively down-cast to 32-bit precision, and the object's 32-bit vertex positions are transformed by the matrix.
 *
 * We see no jittering, because with RTC a detectable loss of GPU accuracy only starts happening to objects as they become very distant from the camera viewpoint, at which point they are too small to be discernible anyway.
 *
 * ## RTC Coordinates with Geometry Instancing
 *
 * To use RTC with ````SceneModel```` geometry instancing, we specify an RTC center for the geometry via its ````origin```` parameter. Then ````SceneModel```` assumes that all meshes that instance that geometry are within the same RTC coordinate system, ie. the meshes ````position```` and ````rotation```` properties are assumed to be relative to the geometry's ````origin````.
 *
 * For simplicity, our example's meshes all instance the same geometry. Therefore, our example model has only one RTC center.
 *
 * Note that the axis-aligned World-space boundary (AABB) of our model is ````[ -6, -9, -6, 1000000006, -2.5, 1000000006]````.
 *
 * [![](http://xeokit.io/img/docs/sceneGraph.png)](https://xeokit.github.io/xeokit-sdk/examples/index.html#sceneRepresentation_SceneModel_batching)
 *
 * ````javascript
 * const origin = [100000000, 0, 100000000];
 *
 * sceneModel.createGeometry({
 *     id: "box",
 *     primitive: "triangles",
 *     positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
 *     normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
 *     indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
 * });
 *
 * sceneModel.createMesh({
 *     id: "leg1",
 *     geometryId: "box",
 *     position: [-4, -6, -4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [1, 0.3, 0.3],
 *     origin: origin
 * });
 *
 * sceneModel.createEntity({
 *     meshIds: ["leg1"],
 *     isObject: true
 * });
 *
 * sceneModel.createMesh({
 *     id: "leg2",
 *     geometryId: "box",
 *     position: [4, -6, -4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [0.3, 1.0, 0.3],
 *     origin: origin
 * });
 *
 * sceneModel.createEntity({
 *     meshIds: ["leg2"],
 *     isObject: true
 * });
 *
 * sceneModel.createMesh({
 *     id: "leg3",
 *     geometryId: "box",
 *     position: [4, -6, 4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [0.3, 0.3, 1.0],
 *     origin: origin
 * });
 *
 * sceneModel.createEntity({
 *     meshIds: ["leg3"],
 *     isObject: true
 * });
 *
 * sceneModel.createMesh({
 *     id: "leg4",
 *     geometryId: "box",
 *     position: [-4, -6, 4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [1.0, 1.0, 0.0],
 *     origin: origin
 * });
 *
 * sceneModel.createEntity({
 *     meshIds: ["leg4"],
 *     isObject: true
 * });
 *
 * sceneModel.createMesh({
 *     id: "top",
 *     geometryId: "box",
 *     position: [0, -3, 0],
 *     scale: [6, 0.5, 6],
 *     rotation: [0, 0, 0],
 *     color: [1.0, 0.3, 1.0],
 *     origin: origin
 * });
 *
 * sceneModel.createEntity({
 *     meshIds: ["top"],
 *     isObject: true
 * });
 * ````
 *
 * ## RTC Coordinates with Geometry Batching
 *
 * To use RTC with ````SceneModel```` geometry batching, we specify an RTC center (````origin````) for each mesh. For performance, we try to have as many meshes share the same value for ````origin```` as possible. Each mesh's ````positions````, ````position```` and ````rotation```` properties are assumed to be relative to ````origin````.
 *
 * For simplicity, the meshes in our example all share the same RTC center.
 *
 * The axis-aligned World-space boundary (AABB) of our model is ````[ -6, -9, -6, 1000000006, -2.5, 1000000006]````.
 *
 * [![](http://xeokit.io/img/docs/sceneGraph.png)](https://xeokit.github.io/xeokit-sdk/examples/index.html#sceneRepresentation_SceneModel_batching)
 *
 * ````javascript
 * const origin = [100000000, 0, 100000000];
 *
 * sceneModel.createMesh({
 *     id: "leg1",
 *     origin: origin, // This mesh's positions and transforms are relative to the RTC center
 *     primitive: "triangles",
 *     positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
 *     normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
 *     indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
 *     position: [-4, -6, -4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [1, 0.3, 0.3]
 * });
 *
 * sceneModel.createEntity({
 *     meshIds: ["leg1"],
 *     isObject: true
 * });
 *
 * sceneModel.createMesh({
 *     id: "leg2",
 *     origin: origin, // This mesh's positions and transforms are relative to the RTC center
 *     primitive: "triangles",
 *     positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
 *     normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
 *     indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
 *     position: [4, -6, -4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [0.3, 1.0, 0.3]
 * });
 *
 * sceneModel.createEntity({
 *     meshIds: ["leg2"],
 *     isObject: true
 * });
 *
 * sceneModel.createMesh({
 *     id: "leg3",
 *     origin: origin, // This mesh's positions and transforms are relative to the RTC center
 *     primitive: "triangles",
 *     positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
 *     normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
 *     indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
 *     position: [4, -6, 4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [0.3, 0.3, 1.0]
 * });
 *
 * sceneModel.createEntity({
 *     meshIds: ["leg3"],
 *     isObject: true
 * });
 *
 * sceneModel.createMesh({
 *     id: "leg4",
 *     origin: origin, // This mesh's positions and transforms are relative to the RTC center
 *     primitive: "triangles",
 *     positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
 *     normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
 *     indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
 *     position: [-4, -6, 4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [1.0, 1.0, 0.0]
 * });
 *
 * sceneModel.createEntity({
 *     meshIds: ["leg4"],
 *     isObject: true
 * });
 *
 * sceneModel.createMesh({
 *     id: "top",
 *     origin: origin, // This mesh's positions and transforms are relative to the RTC center
 *     primitive: "triangles",
 *     positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
 *     normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
 *     indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
 *     position: [0, -3, 0],
 *     scale: [6, 0.5, 6],
 *     rotation: [0, 0, 0],
 *     color: [1.0, 0.3, 1.0]
 * });
 *
 * sceneModel.createEntity({
 *     meshIds: ["top"],
 *     isObject: true
 * });
 * ````
 *
 * ## Positioning at World-space coordinates
 *
 * To position a SceneModel at given double-precision World coordinates, we can
 * configure the ````origin```` of the SceneModel itself. The ````origin```` is a double-precision
 * 3D World-space position at which the SceneModel will be located.
 *
 * Note that ````position```` is a single-precision offset relative to ````origin````.
 *
 * ````javascript
 * const origin = [100000000, 0, 100000000];
 *
 * const sceneModel = new SceneModel(viewer.scene, {
 *     id: "table",
 *     isModel: true,
 *     origin: origin, // Everything in this SceneModel is relative to this RTC center
 *     position: [0, 0, 0],
 *     scale: [1, 1, 1],
 *     rotation: [0, 0, 0]
 * });
 *
 * sceneModel.createGeometry({
 *     id: "box",
 *     primitive: "triangles",
 *     positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ],
 *     normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ],
 *     indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ],
 * });
 *
 * sceneModel.createMesh({
 *     id: "leg1",
 *     geometryId: "box",
 *     position: [-4, -6, -4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [1, 0.3, 0.3]
 * });
 *
 * sceneModel.createEntity({
 *     meshIds: ["leg1"],
 *     isObject: true
 * });
 *
 * sceneModel.createMesh({
 *     id: "leg2",
 *     geometryId: "box",
 *     position: [4, -6, -4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [0.3, 1.0, 0.3]
 * });
 *
 * sceneModel.createEntity({
 *     meshIds: ["leg2"],
 *     isObject: true
 * });
 *
 * sceneModel.createMesh({
 *     id: "leg3",
 *     geometryId: "box",
 *     position: [4, -6, 4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [0.3, 0.3, 1.0]
 * });
 *
 * sceneModel.createEntity({
 *     meshIds: ["leg3"],
 *     isObject: true
 * });
 *
 * sceneModel.createMesh({
 *     id: "leg4",
 *     geometryId: "box",
 *     position: [-4, -6, 4],
 *     scale: [1, 3, 1],
 *     rotation: [0, 0, 0],
 *     color: [1.0, 1.0, 0.0]
 * });
 *
 * sceneModel.createEntity({
 *     meshIds: ["leg4"],
 *     isObject: true
 * });
 *
 * sceneModel.createMesh({
 *     id: "top",
 *     geometryId: "box",
 *     position: [0, -3, 0],
 *     scale: [6, 0.5, 6],
 *     rotation: [0, 0, 0],
 *     color: [1.0, 0.3, 1.0]
 * });
 *
 * sceneModel.createEntity({
 *     meshIds: ["top"],
 *     isObject: true
 * });
 * ````
 *
 * # Textures
 *
 * ## Loading KTX2 Texture Files into a SceneModel
 *
 * A {@link SceneModel} that is configured with a {@link KTX2TextureTranscoder} will
 * allow us to load textures into it from KTX2 buffers or files.
 *
 * In the example below, we'll create a {@link Viewer}, containing a {@link SceneModel} configured with a
 * {@link KTX2TextureTranscoder}. We'll then programmatically create a simple object within the SceneModel, consisting of
 * a single mesh with a texture loaded from a KTX2 file, which our SceneModel internally transcodes, using
 * its {@link KTX2TextureTranscoder}. Note how we configure our {@link KTX2TextureTranscoder} with a path to the Basis Universal
 * transcoder WASM module.
 *
 * ````javascript
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.scene.camera.eye = [-21.80, 4.01, 6.56];
 * viewer.scene.camera.look = [0, -5.75, 0];
 * viewer.scene.camera.up = [0.37, 0.91, -0.11];
 *
 * const textureTranscoder = new KTX2TextureTranscoder({
 *     viewer,
 *     transcoderPath: "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/" // <------ Path to BasisU transcoder module
 * });
 *
 * const sceneModel = new SceneModel(viewer.scene, {
 *      id: "myModel",
 *      textureTranscoder // <<-------------------- Configure model with our transcoder
 *  });
 *
 * sceneModel.createTexture({
 *      id: "myColorTexture",
 *      src: "../assets/textures/compressed/sample_uastc_zstd.ktx2" // <<----- KTX2 texture asset
 * });
 *
 * sceneModel.createTexture({
 *      id: "myMetallicRoughnessTexture",
 *      src: "../assets/textures/alpha/crosshatchAlphaMap.jpg" // <<----- JPEG texture asset
 * });
 *
 * sceneModel.createTextureSet({
 *      id: "myTextureSet",
 *      colorTextureId: "myColorTexture",
 *      metallicRoughnessTextureId: "myMetallicRoughnessTexture"
 *  });
 *
 * sceneModel.createMesh({
 *      id: "myMesh",
 *      textureSetId: "myTextureSet",
 *      primitive: "triangles",
 *      positions: [1, 1, 1, ...],
 *      normals: [0, 0, 1, 0, ...],
 *      uv: [1, 0, 0, ...],
 *      indices: [0, 1, 2, ...],
 *  });
 *
 * sceneModel.createEntity({
 *      id: "myEntity",
 *      meshIds: ["myMesh"]
 *  });
 *
 * sceneModel.finalize();
 * ````
 *
 * ## Loading KTX2 Textures from ArrayBuffers into a SceneModel
 *
 * A SceneModel that is configured with a {@link KTX2TextureTranscoder} will allow us to load textures into
 * it from KTX2 ArrayBuffers.
 *
 * In the example below, we'll create a {@link Viewer}, containing a {@link SceneModel} configured with a
 * {@link KTX2TextureTranscoder}. We'll then programmatically create a simple object within the SceneModel, consisting of
 * a single mesh with a texture loaded from a KTX2 ArrayBuffer, which our SceneModel internally transcodes, using
 * its {@link KTX2TextureTranscoder}.
 *
 * ````javascript
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.scene.camera.eye = [-21.80, 4.01, 6.56];
 * viewer.scene.camera.look = [0, -5.75, 0];
 * viewer.scene.camera.up = [0.37, 0.91, -0.11];
 *
 * const textureTranscoder = new KTX2TextureTranscoder({
 *     viewer,
 *     transcoderPath: "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/" // <------ Path to BasisU transcoder module
 * });
 *
 * const sceneModel = new SceneModel(viewer.scene, {
 *      id: "myModel",
 *      textureTranscoder // <<-------------------- Configure model with our transcoder
 * });
 *
 * utils.loadArraybuffer("../assets/textures/compressed/sample_uastc_zstd.ktx2",(arrayBuffer) => {
 *
 *     sceneModel.createTexture({
 *         id: "myColorTexture",
 *         buffers: [arrayBuffer] // <<----- KTX2 texture asset
 *     });
 *
 *     sceneModel.createTexture({
 *         id: "myMetallicRoughnessTexture",
 *         src: "../assets/textures/alpha/crosshatchAlphaMap.jpg" // <<----- JPEG texture asset
 *     });
 *
 *     sceneModel.createTextureSet({
 *        id: "myTextureSet",
 *        colorTextureId: "myColorTexture",
 *        metallicRoughnessTextureId: "myMetallicRoughnessTexture"
 *     });
 *
 *     sceneModel.createMesh({
 *          id: "myMesh",
 *          textureSetId: "myTextureSet",
 *          primitive: "triangles",
 *          positions: [1, 1, 1, ...],
 *          normals: [0, 0, 1, 0, ...],
 *          uv: [1, 0, 0, ...],
 *          indices: [0, 1, 2, ...],
 *     });
 *
 *     sceneModel.createEntity({
 *         id: "myEntity",
 *         meshIds: ["myMesh"]
 *     });
 *
 *     sceneModel.finalize();
 * });
 * ````
 *
 * @implements {Entity}
 */
class SceneModel extends Component {

    /**
     * @constructor
     * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well.
     * @param {*} [cfg] Configs
     * @param {String} [cfg.id] Optional ID, unique among all components in the parent scene, generated automatically when omitted.
     * @param {Boolean} [cfg.isModel] Specify ````true```` if this SceneModel represents a model, in which case the SceneModel will be registered by {@link SceneModel#id} in {@link Scene#models} and may also have a corresponding {@link MetaModel} with matching {@link MetaModel#id}, registered by that ID in {@link MetaScene#metaModels}.
     * @param {Number[]} [cfg.origin=[0,0,0]] World-space double-precision 3D origin.
     * @param {Number[]} [cfg.position=[0,0,0]] Local, single-precision 3D position, relative to the origin parameter.
     * @param {Number[]} [cfg.scale=[1,1,1]] Local scale.
     * @param {Number[]} [cfg.rotation=[0,0,0]] Local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis.
     * @param {Number[]} [cfg.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1] Local modelling transform matrix. Overrides the position, scale and rotation parameters.
     * @param {Boolean} [cfg.visible=true] Indicates if the SceneModel is initially visible.
     * @param {Boolean} [cfg.culled=false] Indicates if the SceneModel is initially culled from view.
     * @param {Boolean} [cfg.pickable=true] Indicates if the SceneModel is initially pickable.
     * @param {Boolean} [cfg.clippable=true] Indicates if the SceneModel is initially clippable.
     * @param {Boolean} [cfg.collidable=true] Indicates if the SceneModel is initially included in boundary calculations.
     * @param {Boolean} [cfg.xrayed=false] Indicates if the SceneModel is initially xrayed.
     * @param {Boolean} [cfg.highlighted=false] Indicates if the SceneModel is initially highlighted.
     * @param {Boolean} [cfg.selected=false] Indicates if the SceneModel is initially selected.
     * @param {Boolean} [cfg.edges=false] Indicates if the SceneModel's edges are initially emphasized.
     * @param {Number[]} [cfg.colorize=[1.0,1.0,1.0]] SceneModel's initial RGB colorize color, multiplies by the rendered fragment colors.
     * @param {Number} [cfg.opacity=1.0] SceneModel's initial opacity factor, multiplies by the rendered fragment alpha.
     * @param {Number} [cfg.backfaces=false] When we set this ````true````, then we force rendering of backfaces for this SceneModel. When
     * we leave this ````false````, then we allow the Viewer to decide when to render backfaces. In that case, the
     * Viewer will hide backfaces on watertight meshes, show backfaces on open meshes, and always show backfaces on meshes when we slice them open with {@link SectionPlane}s.
     * @param {Boolean} [cfg.saoEnabled=true] Indicates if Scalable Ambient Obscurance (SAO) will apply to this SceneModel. SAO is configured by the Scene's {@link SAO} component.
     * @param {Boolean} [cfg.pbrEnabled=true] Indicates if physically-based rendering (PBR) will apply to the SceneModel when {@link Scene#pbrEnabled} is ````true````.
     * @param {Boolean} [cfg.colorTextureEnabled=true] Indicates if base color textures will be rendered for the SceneModel when {@link Scene#colorTextureEnabled} is ````true````.
     * @param {Number} [cfg.edgeThreshold=10] When xraying, highlighting, selecting or edging, this is the threshold angle between normals of adjacent triangles, below which their shared wireframe edge is not drawn.
     * @param {Number} [cfg.maxGeometryBatchSize=50000000] Maximum geometry batch size, as number of vertices. This is optionally supplied
     * to limit the size of the batched geometry arrays that SceneModel internally creates for batched geometries.
     * A lower value means less heap allocation/de-allocation while creating/loading batched geometries, but more draw calls and
     * slower rendering speed. A high value means larger heap allocation/de-allocation while creating/loading, but less draw calls
     * and faster rendering speed. It's recommended to keep this somewhere roughly between ````50000```` and ````50000000```.
     * @param {TextureTranscoder} [cfg.textureTranscoder] Transcoder that will be used internally by {@link SceneModel#createTexture}
     * to convert transcoded texture data. Only required when we'll be providing transcoded data
     * to {@link SceneModel#createTexture}. We assume that all transcoded texture data added to a  ````SceneModel````
     * will then in a format supported by this transcoder.
     * @param {Boolean} [cfg.dtxEnabled=true] When ````true```` (default) use data textures (DTX), where appropriate, to
     * represent the returned model. Set false to always use vertex buffer objects (VBOs). Note that DTX is only applicable
     * to non-textured triangle meshes, and that VBOs are always used for meshes that have textures, line segments, or point
     * primitives. Only works while {@link DTX#enabled} is also ````true````.
     * @param {Number} [cfg.renderOrder=0] Specifies the rendering order for this SceneModel. This is used to control the order in which
     * SceneModels are drawn when they have transparent objects, to give control over the order in which those objects are blended within the transparent
     * render pass.
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this.renderOrder = cfg.renderOrder || 0;

        this._dtxEnabled = this.scene.dtxEnabled && (cfg.dtxEnabled !== false);

        this._enableVertexWelding = false; // Not needed for most objects, and very expensive, so disabled
        this._enableIndexBucketing = false; // Until fixed: https://github.com/xeokit/xeokit-sdk/issues/1204

        this._vboBatchingLayerScratchMemory = getScratchMemory();
        this._textureTranscoder = cfg.textureTranscoder || getKTX2TextureTranscoder(this.scene.viewer);

        this._maxGeometryBatchSize = cfg.maxGeometryBatchSize;

        this._aabb = math.collapseAABB3();
        this._aabbDirty = true;

        this._quantizationRanges = {};

        this._vboInstancingLayers = {};
        this._vboBatchingLayers = {};
        this._dtxLayers = {};

        this._meshList = [];

        this.layerList = []; // For GL state efficiency when drawing, InstancingLayers are in first part, BatchingLayers are in second
        this._layersToFinalize = [];

        this._entityList = [];
        this._entitiesToFinalize = [];

        this._geometries = {};
        this._dtxBuckets = {}; // Geometries with optimizations used for data texture representation
        this._textures = {};
        this._textureSets = {};
        this._transforms = {};
        this._meshes = {};
        this._unusedMeshes = {};
        this._entities = {};

        /** @private **/
        this.renderFlags = new RenderFlags();

        /**
         * @private
         */
        this.numGeometries = 0; // Number of geometries created with createGeometry()

        // These counts are used to avoid unnecessary render passes
        // They are incremented or decremented exclusively by BatchingLayer and InstancingLayer

        /**
         * @private
         */
        this.numPortions = 0;

        /**
         * @private
         */
        this.numVisibleLayerPortions = 0;

        /**
         * @private
         */
        this.numTransparentLayerPortions = 0;

        /**
         * @private
         */
        this.numXRayedLayerPortions = 0;

        /**
         * @private
         */
        this.numHighlightedLayerPortions = 0;

        /**
         * @private
         */
        this.numSelectedLayerPortions = 0;

        /**
         * @private
         */
        this.numEdgesLayerPortions = 0;

        /**
         * @private
         */
        this.numPickableLayerPortions = 0;

        /**
         * @private
         */
        this.numClippableLayerPortions = 0;

        /**
         * @private
         */
        this.numCulledLayerPortions = 0;

        this.numEntities = 0;
        this._numTriangles = 0;
        this._numLines = 0;
        this._numPoints = 0;
        this._layersFinalized = false;

        this._edgeThreshold = cfg.edgeThreshold || 10;

        // Build static matrix

        this._origin = math.vec3(cfg.origin || [0, 0, 0]);
        this._position = math.vec3(cfg.position || [0, 0, 0]);
        this._rotation = math.vec3(cfg.rotation || [0, 0, 0]);
        this._quaternion = math.vec4(cfg.quaternion || [0, 0, 0, 1]);
        this._conjugateQuaternion = math.vec4(cfg.quaternion || [0, 0, 0, 1]);

        if (cfg.rotation) {
            math.eulerToQuaternion(this._rotation, "XYZ", this._quaternion);
        }
        this._scale = math.vec3(cfg.scale || [1, 1, 1]);

        this._worldRotationMatrix = math.mat4();
        this._worldRotationMatrixConjugate = math.mat4();
        this._matrix = math.mat4();
        this._matrixDirty = true;

        this._rebuildMatrices();

        this._worldNormalMatrix = math.mat4();
        math.inverseMat4(this._matrix, this._worldNormalMatrix);
        math.transposeMat4(this._worldNormalMatrix);

        if (cfg.matrix || cfg.position || cfg.rotation || cfg.scale || cfg.quaternion) {
            this._viewMatrix = math.mat4();
            this._viewNormalMatrix = math.mat4();
            this._viewMatrixDirty = true;
            this._matrixNonIdentity = true;
        }

        this._opacity = 1.0;
        this._colorize = [1, 1, 1];

        this._saoEnabled = (cfg.saoEnabled !== false);
        this._pbrEnabled = (cfg.pbrEnabled !== false);
        this._colorTextureEnabled = (cfg.colorTextureEnabled !== false);

        this._isModel = cfg.isModel;
        if (this._isModel) {
            this.scene._registerModel(this);
        }

        this._onCameraViewMatrix = this.scene.camera.on("matrix", () => {
            this._viewMatrixDirty = true;
        });

        this._meshesWithDirtyMatrices = [];
        this._numMeshesWithDirtyMatrices = 0;

        this._onTick = this.scene.on("tick", () => {
            while (this._numMeshesWithDirtyMatrices > 0) {
                this._meshesWithDirtyMatrices[--this._numMeshesWithDirtyMatrices]._updateMatrix();
            }
        });

        this._createDefaultTextureSet();

        this.visible = cfg.visible;
        this.culled = cfg.culled;
        this.pickable = cfg.pickable;
        this.clippable = cfg.clippable;
        this.collidable = cfg.collidable;
        this.castsShadow = cfg.castsShadow;
        this.receivesShadow = cfg.receivesShadow;
        this.xrayed = cfg.xrayed;
        this.highlighted = cfg.highlighted;
        this.selected = cfg.selected;
        this.edges = cfg.edges;
        this.colorize = cfg.colorize;
        this.opacity = cfg.opacity;
        this.backfaces = cfg.backfaces;
    }

    _meshMatrixDirty(mesh) {
        this._meshesWithDirtyMatrices[this._numMeshesWithDirtyMatrices++] = mesh;
    }

    _createDefaultTextureSet() {
        // Every SceneModelMesh gets at least the default TextureSet,
        // which contains empty default textures filled with color
        const defaultColorTexture = new SceneModelTexture({
            id: DEFAULT_COLOR_TEXTURE_ID,
            texture: new Texture2D({
                gl: this.scene.canvas.gl,
                preloadColor: [1, 1, 1, 1] // [r, g, b, a]})
            })
        });
        const defaultMetalRoughTexture = new SceneModelTexture({
            id: DEFAULT_METAL_ROUGH_TEXTURE_ID,
            texture: new Texture2D({
                gl: this.scene.canvas.gl,
                preloadColor: [0, 1, 1, 1] // [unused, roughness, metalness, unused]
            })
        });
        const defaultNormalsTexture = new SceneModelTexture({
            id: DEFAULT_NORMALS_TEXTURE_ID,
            texture: new Texture2D({
                gl: this.scene.canvas.gl,
                preloadColor: [0, 0, 0, 0] // [x, y, z, unused] - these must be zeros
            })
        });
        const defaultEmissiveTexture = new SceneModelTexture({
            id: DEFAULT_EMISSIVE_TEXTURE_ID,
            texture: new Texture2D({
                gl: this.scene.canvas.gl,
                preloadColor: [0, 0, 0, 1] // [x, y, z, unused]
            })
        });
        const defaultOcclusionTexture = new SceneModelTexture({
            id: DEFAULT_OCCLUSION_TEXTURE_ID,
            texture: new Texture2D({
                gl: this.scene.canvas.gl,
                preloadColor: [1, 1, 1, 1] // [x, y, z, unused]
            })
        });
        this._textures[DEFAULT_COLOR_TEXTURE_ID] = defaultColorTexture;
        this._textures[DEFAULT_METAL_ROUGH_TEXTURE_ID] = defaultMetalRoughTexture;
        this._textures[DEFAULT_NORMALS_TEXTURE_ID] = defaultNormalsTexture;
        this._textures[DEFAULT_EMISSIVE_TEXTURE_ID] = defaultEmissiveTexture;
        this._textures[DEFAULT_OCCLUSION_TEXTURE_ID] = defaultOcclusionTexture;
        this._textureSets[DEFAULT_TEXTURE_SET_ID] = new SceneModelTextureSet({
            id: DEFAULT_TEXTURE_SET_ID,
            model: this,
            colorTexture: defaultColorTexture,
            metallicRoughnessTexture: defaultMetalRoughTexture,
            normalsTexture: defaultNormalsTexture,
            emissiveTexture: defaultEmissiveTexture,
            occlusionTexture: defaultOcclusionTexture
        });
    }

    //------------------------------------------------------------------------------------------------------------------
    // SceneModel members
    //------------------------------------------------------------------------------------------------------------------

    /**
     * Returns true to indicate that this Component is a SceneModel.
     * @type {Boolean}
     */
    get isPerformanceModel() {
        return true;
    }

    /**
     * The {@link SceneModelTransform}s in this SceneModel.
     *
     * Each {#link SceneModelTransform} is stored here against its {@link SceneModelTransform.id}.
     *
     * @returns {*|{}}
     */
    get transforms() {
        return this._transforms;
    }

    /**
     * The {@link SceneModelTexture}s in this SceneModel.
     *
     * * Each {@link SceneModelTexture} is created with {@link SceneModel.createTexture}.
     * * Each {@link SceneModelTexture} is stored here against its {@link SceneModelTexture.id}.
     *
     * @returns {*|{}}
     */
    get textures() {
        return this._textures;
    }

    /**
     * The {@link SceneModelTextureSet}s in this SceneModel.
     *
     * Each {@link SceneModelTextureSet} is stored here against its {@link SceneModelTextureSet.id}.
     *
     * @returns {*|{}}
     */
    get textureSets() {
        return this._textureSets;
    }

    /**
     * The {@link SceneModelMesh}es in this SceneModel.
     *
     * Each {@SceneModelMesh} is stored here against its {@link SceneModelMesh.id}.
     *
     * @returns {*|{}}
     */
    get meshes() {
        return this._meshes;
    }

    /**
     * The {@link SceneModelEntity}s in this SceneModel.
     *
     * Each {#link SceneModelEntity} in this SceneModel that represents an object is
     * stored here against its {@link SceneModelTransform.id}.
     *
     * @returns {*|{}}
     */
    get objects() {
        return this._entities;
    }

    /**
     * Gets the 3D World-space origin for this SceneModel.
     *
     * Each {@link SceneModelMesh.origin}, if supplied, is relative to this origin.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Float64Array}
     */
    get origin() {
        return this._origin;
    }

    /**
     * Sets the SceneModel's local translation.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    set position(value) {
        this._position.set(value || [0, 0, 0]);
        this._setWorldMatrixDirty();
        this._sceneModelDirty();
        this.glRedraw();
    }

    /**
     * Gets the SceneModel's local translation.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    get position() {
        return this._position;
    }

    /**
     * Sets the SceneModel's local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    set rotation(value) {
        this._rotation.set(value || [0, 0, 0]);
        math.eulerToQuaternion(this._rotation, "XYZ", this._quaternion);
        this._setWorldMatrixDirty();
        this._sceneModelDirty();
        this.glRedraw();
    }

    /**
     * Gets the SceneModel's local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis.
     *
     * Default value is ````[0,0,0]````.
     *
     * @type {Number[]}
     */
    get rotation() {
        return this._rotation;
    }

    /**
     * Sets the SceneModel's local rotation quaternion.
     *
     * Default value is ````[0,0,0,1]````.
     *
     * @type {Number[]}
     */
    set quaternion(value) {
        this._quaternion.set(value || [0, 0, 0, 1]);
        math.quaternionToEuler(this._quaternion, "XYZ", this._rotation);
        this._setWorldMatrixDirty();
        this._sceneModelDirty();
        this.glRedraw();
    }

    /**
     * Gets the SceneModel's local rotation quaternion.
     *
     * Default value is ````[0,0,0,1]````.
     *
     * @type {Number[]}
     */
    get quaternion() {
        return this._quaternion;
    }

    /**
     * Sets the SceneModel's local scale.
     *
     * Default value is ````[1,1,1]````.
     *
     * @type {Number[]}
     * @deprecated
     */
    set scale(value) {
        // NOP - deprecated
    }

    /**
     * Gets the SceneModel's local scale.
     *
     * Default value is ````[1,1,1]````.
     *
     * @type {Number[]}
     * @deprecated
     */
    get scale() {
        return this._scale;
    }

    /**
     * Sets the SceneModel's local modeling transform matrix.
     *
     * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````.
     *
     * @type {Number[]}
     */
    set matrix(value) {
        this._matrix.set(value || DEFAULT_MATRIX);
        math.decomposeMat4(this._matrix, this._position, this._quaternion, this._scale);
        math.quaternionToEuler(this._quaternion, "XYZ", this._rotation);
        this._matrixDirty = false;
        this._setWorldMatrixDirty();
        this._sceneModelDirty();
        this.glRedraw();
    }

    /**
     * Gets the SceneModel's local modeling transform matrix.
     *
     * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````.
     *
     * @type {Number[]}
     */
    get matrix() {
        this._rebuildMatrices();
        return this._matrix;
    }

    /**
     * Gets the SceneModel's local modeling rotation transform matrix.
     *
     * @type {Number[]}
     */
    get rotationMatrix() {
        this._rebuildMatrices();
        return this._worldRotationMatrix;
    }

    _rebuildMatrices() {
        if (this._matrixDirty) {
            math.quaternionToRotationMat4(this._quaternion, this._worldRotationMatrix);
            math.conjugateQuaternion(this._quaternion, this._conjugateQuaternion);
            math.quaternionToRotationMat4(this._conjugateQuaternion, this._worldRotationMatrixConjugate);
            math.scaleMat4v(this._scale, this._worldRotationMatrix);
            math.scaleMat4v(this._scale, this._worldRotationMatrixConjugate);
            this._matrix.set(this._worldRotationMatrix);
            math.translateMat4v(this._position, this._matrix);
            this._matrixDirty = false;
            this._viewMatrixDirty = true;
        }
    }

    /**
     * Gets the conjugate of the SceneModel's local modeling rotation transform matrix.
     *
     * This is used for RTC view matrix management in renderers.
     *
     * @type {Number[]}
     */
    get rotationMatrixConjugate() {
        this._rebuildMatrices();
        return this._worldRotationMatrixConjugate;
    }

    _setWorldMatrixDirty() {
        this._matrixDirty = true;
        this._aabbDirty = true;
    }

    _transformDirty() {
        this._matrixDirty = true;
        this._aabbDirty = true;
        this.scene._aabbDirty = true;
    }

    _sceneModelDirty() {
        this.scene._aabbDirty = true;
        this._aabbDirty = true;
        this.scene._aabbDirty = true;
        this._matrixDirty = true;
        for (let i = 0, len = this._entityList.length; i < len; i++) {
            this._entityList[i]._sceneModelDirty(); // Entities need to retransform their World AABBs by SceneModel's worldMatrix
        }
    }

    /**
     * Gets the SceneModel's World matrix.
     *
     * @property worldMatrix
     * @type {Number[]}
     */
    get worldMatrix() {
        return this.matrix;
    }

    /**
     * Gets the SceneModel's World normal matrix.
     *
     * @type {Number[]}
     */
    get worldNormalMatrix() {
        return this._worldNormalMatrix;
    }

    _rebuildViewMatrices() {
        this._rebuildMatrices();
        if (this._viewMatrixDirty) {
            math.mulMat4(this.scene.camera.viewMatrix, this._matrix, this._viewMatrix);
            math.inverseMat4(this._viewMatrix, this._viewNormalMatrix);
            math.transposeMat4(this._viewNormalMatrix);
            this._viewMatrixDirty = false;
        }
    }

    /**
     * Called by private renderers in ./lib, returns the view matrix with which to
     * render this SceneModel. The view matrix is the concatenation of the
     * Camera view matrix with the Performance model's world (modeling) matrix.
     *
     * @private
     */
    get viewMatrix() {
        if (!this._viewMatrix) {
            return this.scene.camera.viewMatrix;
        }
        this._rebuildViewMatrices();
        return this._viewMatrix;
    }

    /**
     * Called by private renderers in ./lib, returns the view normal matrix with which to render this SceneModel.
     *
     * @private
     */
    get viewNormalMatrix() {
        if (!this._viewNormalMatrix) {
            return this.scene.camera.viewNormalMatrix;
        }
        this._rebuildViewMatrices();
        return this._viewNormalMatrix;
    }

    /**
     * Sets if backfaces are rendered for this SceneModel.
     *
     * Default is ````false````.
     *
     * @type {Boolean}
     */
    get backfaces() {
        return this._backfaces;
    }

    /**
     * Sets if backfaces are rendered for this SceneModel.
     *
     * Default is ````false````.
     *
     * When we set this ````true````, then backfaces are always rendered for this SceneModel.
     *
     * When we set this ````false````, then we allow the Viewer to decide whether to render backfaces. In this case,
     * the Viewer will:
     *
     *  * hide backfaces on watertight meshes,
     *  * show backfaces on open meshes, and
     *  * always show backfaces on meshes when we slice them open with {@link SectionPlane}s.
     *
     * @type {Boolean}
     */
    set backfaces(backfaces) {
        backfaces = !!backfaces;
        this._backfaces = backfaces;
        this.glRedraw();
    }

    /**
     * Gets the list of {@link SceneModelEntity}s within this SceneModel.
     *
     * @returns {SceneModelEntity[]}
     */
    get entityList() {
        return this._entityList;
    }

    /**
     * Returns true to indicate that SceneModel is an {@link Entity}.
     * @type {Boolean}
     */
    get isEntity() {
        return true;
    }

    /**
     * Returns ````true```` if this SceneModel represents a model.
     *
     * When ````true```` the SceneModel will be registered by {@link SceneModel#id} in
     * {@link Scene#models} and may also have a {@link MetaObject} with matching {@link MetaObject#id}.
     *
     * @type {Boolean}
     */
    get isModel() {
        return this._isModel;
    }

    //------------------------------------------------------------------------------------------------------------------
    // SceneModel members
    //------------------------------------------------------------------------------------------------------------------

    /**
     * Returns ````false```` to indicate that SceneModel never represents an object.
     *
     * @type {Boolean}
     */
    get isObject() {
        return false;
    }

    /**
     * Gets the SceneModel's World-space 3D axis-aligned bounding box.
     *
     * Represented by a six-element Float64Array containing the min/max extents of the
     * axis-aligned volume, ie. ````[xmin, ymin,zmin,xmax,ymax, zmax]````.
     *
     * @type {Number[]}
     */
    get aabb() {
        if (this._aabbDirty) {
            math.collapseAABB3(this._aabb);
            for (let i = 0, len = this._entityList.length; i < len; i++) {
                math.expandAABB3(this._aabb, this._entityList[i].aabb);
            }
            this._aabbDirty = false;
        }
        return this._aabb;
    }

    /**
     * The approximate number of triangle primitives in this SceneModel.
     *
     * @type {Number}
     */
    get numTriangles() {
        return this._numTriangles;
    }

    //------------------------------------------------------------------------------------------------------------------
    // Entity members
    //------------------------------------------------------------------------------------------------------------------

    /**
     * The approximate number of line primitives in this SceneModel.
     *
     * @type {Number}
     */
    get numLines() {
        return this._numLines;
    }

    /**
     * The approximate number of point primitives in this SceneModel.
     *
     * @type {Number}
     */
    get numPoints() {
        return this._numPoints;
    }

    /**
     * Gets if any {@link SceneModelEntity}s in this SceneModel are visible.
     *
     * The SceneModel is only rendered when {@link SceneModel#visible} is ````true```` and {@link SceneModel#culled} is ````false````.
     *
     * @type {Boolean}
     */
    get visible() {
        return (this.numVisibleLayerPortions > 0);
    }

    /**
     * Sets if this SceneModel is visible.
     *
     * The SceneModel is only rendered when {@link SceneModel#visible} is ````true```` and {@link SceneModel#culled} is ````false````.
     **
     * @type {Boolean}
     */
    set visible(visible) {
        visible = visible !== false;
        this._visible = visible;
        for (let i = 0, len = this._entityList.length; i < len; i++) {
            this._entityList[i].visible = visible;
        }
        this.glRedraw();
    }

    /**
     * Gets if any {@link SceneModelEntity}s in this SceneModel are xrayed.
     *
     * @type {Boolean}
     */
    get xrayed() {
        return (this.numXRayedLayerPortions > 0);
    }

    /**
     * Sets if all {@link SceneModelEntity}s in this SceneModel are xrayed.
     *
     * @type {Boolean}
     */
    set xrayed(xrayed) {
        xrayed = !!xrayed;
        this._xrayed = xrayed;
        for (let i = 0, len = this._entityList.length; i < len; i++) {
            this._entityList[i].xrayed = xrayed;
        }
        this.glRedraw();
    }

    /**
     * Gets if any {@link SceneModelEntity}s in this SceneModel are highlighted.
     *
     * @type {Boolean}
     */
    get highlighted() {
        return (this.numHighlightedLayerPortions > 0);
    }

    /**
     * Sets if all {@link SceneModelEntity}s in this SceneModel are highlighted.
     *
     * @type {Boolean}
     */
    set highlighted(highlighted) {
        highlighted = !!highlighted;
        this._highlighted = highlighted;
        for (let i = 0, len = this._entityList.length; i < len; i++) {
            this._entityList[i].highlighted = highlighted;
        }
        this.glRedraw();
    }

    /**
     * Gets if any {@link SceneModelEntity}s in this SceneModel are selected.
     *
     * @type {Boolean}
     */
    get selected() {
        return (this.numSelectedLayerPortions > 0);
    }

    /**
     * Sets if all {@link SceneModelEntity}s in this SceneModel are selected.
     *
     * @type {Boolean}
     */
    set selected(selected) {
        selected = !!selected;
        this._selected = selected;
        for (let i = 0, len = this._entityList.length; i < len; i++) {
            this._entityList[i].selected = selected;
        }
        this.glRedraw();
    }

    /**
     * Gets if any {@link SceneModelEntity}s in this SceneModel have edges emphasised.
     *
     * @type {Boolean}
     */
    get edges() {
        return (this.numEdgesLayerPortions > 0);
    }

    /**
     * Sets if all {@link SceneModelEntity}s in this SceneModel have edges emphasised.
     *
     * @type {Boolean}
     */
    set edges(edges) {
        edges = !!edges;
        this._edges = edges;
        for (let i = 0, len = this._entityList.length; i < len; i++) {
            this._entityList[i].edges = edges;
        }
        this.glRedraw();
    }

    /**
     * Gets if this SceneModel is culled from view.
     *
     * The SceneModel is only rendered when {@link SceneModel#visible} is true and {@link SceneModel#culled} is false.
     *
     * @type {Boolean}
     */
    get culled() {
        return this._culled;
    }

    /**
     * Sets if this SceneModel is culled from view.
     *
     * The SceneModel is only rendered when {@link SceneModel#visible} is true and {@link SceneModel#culled} is false.
     *
     * @type {Boolean}
     */
    set culled(culled) {
        culled = !!culled;
        this._culled = culled;
        for (let i = 0, len = this._entityList.length; i < len; i++) {
            this._entityList[i].culled = culled;
        }
        this.glRedraw();
    }

    /**
     * Gets if {@link SceneModelEntity}s in this SceneModel are clippable.
     *
     * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}.
     *
     * @type {Boolean}
     */
    get clippable() {
        return this._clippable;
    }

    /**
     * Sets if {@link SceneModelEntity}s in this SceneModel are clippable.
     *
     * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}.
     *
     * @type {Boolean}
     */
    set clippable(clippable) {
        clippable = clippable !== false;
        this._clippable = clippable;
        for (let i = 0, len = this._entityList.length; i < len; i++) {
            this._entityList[i].clippable = clippable;
        }
        this.glRedraw();
    }

    /**
     * Gets if this SceneModel is collidable.
     *
     * @type {Boolean}
     */
    get collidable() {
        return this._collidable;
    }

    /**
     * Sets if {@link SceneModelEntity}s in this SceneModel are collidable.
     *
     * @type {Boolean}
     */
    set collidable(collidable) {
        collidable = collidable !== false;
        this._collidable = collidable;
        for (let i = 0, len = this._entityList.length; i < len; i++) {
            this._entityList[i].collidable = collidable;
        }
    }

    /**
     * Gets if this SceneModel is pickable.
     *
     * Picking is done via calls to {@link Scene#pick}.
     *
     * @type {Boolean}
     */
    get pickable() {
        return (this.numPickableLayerPortions > 0);
    }

    /**
     * Sets if {@link SceneModelEntity}s in this SceneModel are pickable.
     *
     * Picking is done via calls to {@link Scene#pick}.
     *
     * @type {Boolean}
     */
    set pickable(pickable) {
        pickable = pickable !== false;
        this._pickable = pickable;
        for (let i = 0, len = this._entityList.length; i < len; i++) {
            this._entityList[i].pickable = pickable;
        }
    }

    /**
     * Gets the RGB colorize color for this SceneModel.
     *
     * Each element of the color is in range ````[0..1]````.
     *
     * @type {Number[]}
     */
    get colorize() {
        return this._colorize;
    }

    /**
     * Sets the RGB colorize color for this SceneModel.
     *
     * Multiplies by rendered fragment colors.
     *
     * Each element of the color is in range ````[0..1]````.
     *
     * @type {Number[]}
     */
    set colorize(colorize) {
        this._colorize = colorize;
        for (let i = 0, len = this._entityList.length; i < len; i++) {
            this._entityList[i].colorize = colorize;
        }
    }

    /**
     * Gets this SceneModel's opacity factor.
     *
     * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas.
     *
     * @type {Number}
     */
    get opacity() {
        return this._opacity;
    }

    /**
     * Sets the opacity factor for this SceneModel.
     *
     * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas.
     *
     * @type {Number}
     */
    set opacity(opacity) {
        this._opacity = opacity;
        for (let i = 0, len = this._entityList.length; i < len; i++) {
            this._entityList[i].opacity = opacity;
        }
    }

    /**
     * Gets if this SceneModel casts a shadow.
     *
     * @type {Boolean}
     */
    get castsShadow() {
        return this._castsShadow;
    }

    /**
     * Sets if this SceneModel casts a shadow.
     *
     * @type {Boolean}
     */
    set castsShadow(castsShadow) {
        castsShadow = (castsShadow !== false);
        if (castsShadow !== this._castsShadow) {
            this._castsShadow = castsShadow;
            this.glRedraw();
        }
    }

    /**
     * Sets if this SceneModel can have shadow cast upon it.
     *
     * @type {Boolean}
     */
    get receivesShadow() {
        return this._receivesShadow;
    }

    /**
     * Sets if this SceneModel can have shadow cast upon it.
     *
     * @type {Boolean}
     */
    set receivesShadow(receivesShadow) {
        receivesShadow = (receivesShadow !== false);
        if (receivesShadow !== this._receivesShadow) {
            this._receivesShadow = receivesShadow;
            this.glRedraw();
        }
    }

    /**
     * Gets if Scalable Ambient Obscurance (SAO) will apply to this SceneModel.
     *
     * SAO is configured by the Scene's {@link SAO} component.
     *
     *  Only works when {@link SAO#enabled} is also true.
     *
     * @type {Boolean}
     */
    get saoEnabled() {
        return this._saoEnabled;
    }

    /**
     * Gets if physically-based rendering (PBR) is enabled for this SceneModel.
     *
     * Only works when {@link Scene#pbrEnabled} is also true.
     *
     * @type {Boolean}
     */
    get pbrEnabled() {
        return this._pbrEnabled;
    }

    /**
     * Gets if color textures are enabled for this SceneModel.
     *
     * Only works when {@link Scene#colorTextureEnabled} is also true.
     *
     * @type {Boolean}
     */
    get colorTextureEnabled() {
        return this._colorTextureEnabled;
    }

    /**
     * Returns true to indicate that SceneModel is implements {@link Drawable}.
     *
     * @type {Boolean}
     */
    get isDrawable() {
        return true;
    }

    /** @private */
    get isStateSortable() {
        return false
    }

    /**
     * Configures the appearance of xrayed {@link SceneModelEntity}s within this SceneModel.
     *
     * This is the {@link Scene#xrayMaterial}.
     *
     * @type {EmphasisMaterial}
     */
    get xrayMaterial() {
        return this.scene.xrayMaterial;
    }

    /**
     * Configures the appearance of highlighted {@link SceneModelEntity}s within this SceneModel.
     *
     * This is the {@link Scene#highlightMaterial}.
     *
     * @type {EmphasisMaterial}
     */
    get highlightMaterial() {
        return this.scene.highlightMaterial;
    }

    /**
     * Configures the appearance of selected {@link SceneModelEntity}s within this SceneModel.
     *
     * This is the {@link Scene#selectedMaterial}.
     *
     * @type {EmphasisMaterial}
     */
    get selectedMaterial() {
        return this.scene.selectedMaterial;
    }

    /**
     * Configures the appearance of edges of {@link SceneModelEntity}s within this SceneModel.
     *
     * This is the {@link Scene#edgeMaterial}.
     *
     * @type {EdgeMaterial}
     */
    get edgeMaterial() {
        return this.scene.edgeMaterial;
    }

    //------------------------------------------------------------------------------------------------------------------
    // Drawable members
    //------------------------------------------------------------------------------------------------------------------

    /**
     * Called by private renderers in ./lib, returns the picking view matrix with which to
     * ray-pick on this SceneModel.
     *
     * @private
     */
    getPickViewMatrix(pickViewMatrix) {
        if (!this._viewMatrix) {
            return pickViewMatrix;
        }
        return this._viewMatrix;
    }

    /**
     *
     * @param cfg
     */
    createQuantizationRange(cfg) {
        if (cfg.id === undefined || cfg.id === null) {
            this.error("[createQuantizationRange] Config missing: id");
            return;
        }
        if (cfg.aabb) {
            this.error("[createQuantizationRange] Config missing: aabb");
            return;
        }
        if (this._quantizationRanges[cfg.id]) {
            this.error("[createQuantizationRange] QuantizationRange already created: " + cfg.id);
            return;
        }
        this._quantizationRanges[cfg.id] = {
            id: cfg.id,
            aabb: cfg.aabb,
            matrix: createPositionsDecodeMatrix(cfg.aabb, math.mat4())
        };
    }

    /**
     * Creates a reusable geometry within this SceneModel.
     *
     * We can then supply the geometry ID to {@link SceneModel#createMesh} when we want to create meshes that
     * instance the geometry.
     *
     * @param {*} cfg Geometry properties.
     * @param {String|Number} cfg.id Mandatory ID for the geometry, to refer to with {@link SceneModel#createMesh}.
     * @param {String} cfg.primitive The primitive type. Accepted values are 'points', 'lines', 'triangles', 'solid' and 'surface'.
     * @param {Number[]} [cfg.positions] Flat array of uncompressed 3D vertex positions positions. Required for all primitive types. Overridden by ````positionsCompressed````.
     * @param {Number[]} [cfg.positionsCompressed] Flat array of quantized 3D vertex positions. Overrides ````positions````, and must be accompanied by ````positionsDecodeMatrix````.
     * @param {Number[]} [cfg.positionsDecodeMatrix] A 4x4 matrix for decompressing ````positionsCompressed````. Must be accompanied by ````positionsCompressed````.
     * @param {Number[]} [cfg.normals] Flat array of normal vectors. Only used with "triangles", "solid" and "surface" primitives. When no normals are given, the geometry will be flat shaded using auto-generated face-aligned normals.
     * @param {Number[]} [cfg.normalsCompressed] Flat array of oct-encoded normal vectors. Overrides ````normals````. Only used with "triangles", "solid" and "surface" primitives. When no normals are given, the geometry will be flat shaded using auto-generated face-aligned normals.
     * @param {Number[]} [cfg.colors] Flat array of uncompressed RGBA vertex colors, as float values in range ````[0..1]````. Ignored when ````geometryId```` is given. Overridden by ````color```` and ````colorsCompressed````.
     * @param {Number[]} [cfg.colorsCompressed] Flat array of compressed RGBA vertex colors, as unsigned short integers in range ````[0..255]````. Ignored when ````geometryId```` is given. Overrides ````colors```` and is overridden by ````color````.
     * @param {Number[]} [cfg.uv] Flat array of uncompressed vertex UV coordinates. Only used with "triangles", "solid" and "surface" primitives. Required for textured rendering.
     * @param {Number[]} [cfg.uvCompressed] Flat array of compressed vertex UV coordinates. Only used with "triangles", "solid" and "surface" primitives. Overrides ````uv````. Must be accompanied by ````uvDecodeMatrix````. Only used with "triangles", "solid" and "surface" primitives. Required for textured rendering.
     * @param {Number[]} [cfg.uvDecodeMatrix] A 3x3 matrix for decompressing ````uvCompressed````.
     * @param {Number[]} [cfg.indices] Array of primitive connectivity indices. Not required for `points` primitives.
     * @param {Number[]} [cfg.edgeIndices] Array of edge line indices. Used only with 'triangles', 'solid' and 'surface' primitives. Automatically generated internally if not supplied, using the optional ````edgeThreshold```` given to the ````SceneModel```` constructor.
     */
    createGeometry(cfg) {
        if (cfg.id === undefined || cfg.id === null) {
            this.error("[createGeometry] Config missing: id");
            return;
        }
        if (this._geometries[cfg.id]) {
            this.error("[createGeometry] Geometry already created: " + cfg.id);
            return;
        }
        if (cfg.primitive === undefined || cfg.primitive === null) {
            cfg.primitive = "triangles";
        }
        if (cfg.primitive !== "points" && cfg.primitive !== "lines" && cfg.primitive !== "triangles" && cfg.primitive !== "solid" && cfg.primitive !== "surface") {
            this.error(`[createGeometry] Unsupported value for 'primitive': '${cfg.primitive}' - supported values are 'points', 'lines', 'triangles', 'solid' and 'surface'. Defaulting to 'triangles'.`);
            return;
        }
        if (!cfg.positions && !cfg.positionsCompressed && !cfg.buckets) {
            this.error("[createGeometry] Param expected: `positions`,  `positionsCompressed' or 'buckets");
            return null;
        }
        if (cfg.positionsCompressed && !cfg.positionsDecodeMatrix && !cfg.positionsDecodeBoundary) {
            this.error("[createGeometry] Param expected: `positionsDecodeMatrix` or 'positionsDecodeBoundary' (required for `positionsCompressed')");
            return null;
        }
        if (cfg.positionsDecodeMatrix && cfg.positionsDecodeBoundary) {
            this.error("[createGeometry] Only one of these params expected: `positionsDecodeMatrix` or 'positionsDecodeBoundary' (required for `positionsCompressed')");
            return null;
        }
        if (cfg.uvCompressed && !cfg.uvDecodeMatrix) {
            this.error("[createGeometry] Param expected: `uvDecodeMatrix` (required for `uvCompressed')");
            return null;
        }
        if (!cfg.buckets && !cfg.indices && (cfg.primitive === "triangles" || cfg.primitive === "solid" || cfg.primitive === "surface")) {
            const numPositions = (cfg.positions || cfg.positionsCompressed).length / 3;
            cfg.indices = this._createDefaultIndices(numPositions);
        }
        if (!cfg.buckets && !cfg.indices && cfg.primitive !== "points") {
            this.error(`[createGeometry] Param expected: indices (required for '${cfg.primitive}' primitive type)`);
            return null;
        }
        if (cfg.positionsDecodeBoundary) {
            cfg.positionsDecodeMatrix = createPositionsDecodeMatrix(cfg.positionsDecodeBoundary, math.mat4());
        }
        if (cfg.positions) {
            const aabb = math.collapseAABB3();
            cfg.positionsDecodeMatrix = math.mat4();
            math.expandAABB3Points3(aabb, cfg.positions);
            cfg.positionsCompressed = quantizePositions(cfg.positions, aabb, cfg.positionsDecodeMatrix);
            cfg.aabb = aabb;
        } else if (cfg.positionsCompressed) {
            const aabb = math.collapseAABB3();
            cfg.positionsDecodeMatrix = new Float64Array(cfg.positionsDecodeMatrix);
            cfg.positionsCompressed = new Uint16Array(cfg.positionsCompressed);
            math.expandAABB3Points3(aabb, cfg.positionsCompressed);
            geometryCompressionUtils.decompressAABB(aabb, cfg.positionsDecodeMatrix);
            cfg.aabb = aabb;
        } else if (cfg.buckets) {
            const aabb = math.collapseAABB3();
            this._dtxBuckets[cfg.id] = cfg.buckets;
            for (let i = 0, len = cfg.buckets.length; i < len; i++) {
                const bucket = cfg.buckets[i];
                if (bucket.positions) {
                    math.expandAABB3Points3(aabb, bucket.positions);
                } else if (bucket.positionsCompressed) {
                    math.expandAABB3Points3(aabb, bucket.positionsCompressed);
                }
            }
            if (cfg.positionsDecodeMatrix) {
                geometryCompressionUtils.decompressAABB(aabb, cfg.positionsDecodeMatrix);
            }
            cfg.aabb = aabb;
        }
        if (cfg.colorsCompressed && cfg.colorsCompressed.length > 0) {
            cfg.colorsCompressed = new Uint8Array(cfg.colorsCompressed);
        } else if (cfg.colors && cfg.colors.length > 0) {
            const colors = cfg.colors;
            const colorsCompressed = new Uint8Array(colors.length);
            for (let i = 0, len = colors.length; i < len; i++) {
                colorsCompressed[i] = colors[i] * 255;
            }
            cfg.colorsCompressed = colorsCompressed;
        }
        if (!cfg.buckets && !cfg.edgeIndices && (cfg.primitive === "triangles" || cfg.primitive === "solid" || cfg.primitive === "surface")) {
            if (cfg.positions) {
                cfg.edgeIndices = buildEdgeIndices(cfg.positions, cfg.indices, null, 5.0);
            } else {
                cfg.edgeIndices = buildEdgeIndices(cfg.positionsCompressed, cfg.indices, cfg.positionsDecodeMatrix, 2.0);
            }
        }
        if (cfg.uv) {
            const bounds = geometryCompressionUtils.getUVBounds(cfg.uv);
            const result = geometryCompressionUtils.compressUVs(cfg.uv, bounds.min, bounds.max);
            cfg.uvCompressed = result.quantized;
            cfg.uvDecodeMatrix = result.decodeMatrix;
        } else if (cfg.uvCompressed) {
            cfg.uvCompressed = new Uint16Array(cfg.uvCompressed);
            cfg.uvDecodeMatrix = new Float64Array(cfg.uvDecodeMatrix);
        }
        if (cfg.normals) { // HACK
            cfg.normals = null;
        }
        this._geometries [cfg.id] = cfg;
        this._numTriangles += (cfg.indices ? Math.round(cfg.indices.length / 3) : 0);
        this.numGeometries++;
    }

    /**
     * Creates a texture within this SceneModel.
     *
     * We can then supply the texture ID to {@link SceneModel#createTextureSet} when we want to create texture sets that use the texture.
     *
     * @param {*} cfg Texture properties.
     * @param {String|Number} cfg.id Mandatory ID for the texture, to refer to with {@link SceneModel#createTextureSet}.
     * @param {String} [cfg.src] Image file for the texture. Assumed to be transcoded if not having a recognized image file
     * extension (jpg, jpeg, png etc.). If transcoded, then assumes ````SceneModel```` is configured with a {@link TextureTranscoder}.
     * @param {ArrayBuffer[]} [cfg.buffers] Transcoded texture data. Assumes ````SceneModel```` is
     * configured with a {@link TextureTranscoder}. This parameter is given as an array of buffers so we can potentially support multi-image textures, such as cube maps.
     * @param {HTMLImageElement} [cfg.image] HTML Image object to load into this texture. Overrides ````src```` and ````buffers````. Never transcoded.
     * @param {Number} [cfg.minFilter=LinearMipmapLinearFilter] How the texture is sampled when a texel covers less than one pixel.
     * Supported values are {@link LinearMipmapLinearFilter}, {@link LinearMipMapNearestFilter}, {@link NearestMipMapNearestFilter}, {@link NearestMipMapLinearFilter} and {@link LinearMipMapLinearFilter}.
     * @param {Number} [cfg.magFilter=LinearFilter] How the texture is sampled when a texel covers more than one pixel. Supported values are {@link LinearFilter} and {@link NearestFilter}.
     * @param {Number} [cfg.wrapS=RepeatWrapping] Wrap parameter for texture coordinate *S*. Supported values are {@link ClampToEdgeWrapping}, {@link MirroredRepeatWrapping} and {@link RepeatWrapping}.
     * @param {Number} [cfg.wrapT=RepeatWrapping] Wrap parameter for texture coordinate *T*. Supported values are {@link ClampToEdgeWrapping}, {@link MirroredRepeatWrapping} and {@link RepeatWrapping}..
     * @param {Number} [cfg.wrapR=RepeatWrapping] Wrap parameter for texture coordinate *R*. Supported values are {@link ClampToEdgeWrapping}, {@link MirroredRepeatWrapping} and {@link RepeatWrapping}.
     * @param {Boolean} [cfg.flipY=false] Flips this Texture's source data along its vertical axis when ````true````.
     * @param  {Number} [cfg.encoding=LinearEncoding] Encoding format. Supported values are {@link LinearEncoding} and {@link sRGBEncoding}.
     */
    createTexture(cfg) {
        const textureId = cfg.id;
        if (textureId === undefined || textureId === null) {
            this.error("[createTexture] Config missing: id");
            return;
        }
        if (this._textures[textureId]) {
            this.error("[createTexture] Texture already created: " + textureId);
            return;
        }
        if (!cfg.src && !cfg.image && !cfg.buffers) {
            this.error("[createTexture] Param expected: `src`, `image' or 'buffers'");
            return null;
        }
        let minFilter = cfg.minFilter || LinearMipmapLinearFilter;
        if (minFilter !== LinearFilter &&
            minFilter !== LinearMipMapNearestFilter &&
            minFilter !== LinearMipmapLinearFilter &&
            minFilter !== NearestMipMapLinearFilter &&
            minFilter !== NearestMipMapNearestFilter) {
            this.error(`[createTexture] Unsupported value for 'minFilter' - 
            supported values are LinearFilter, LinearMipMapNearestFilter, NearestMipMapNearestFilter, 
            NearestMipMapLinearFilter and LinearMipmapLinearFilter. Defaulting to LinearMipmapLinearFilter.`);
            minFilter = LinearMipmapLinearFilter;
        }
        let magFilter = cfg.magFilter || LinearFilter;
        if (magFilter !== LinearFilter && magFilter !== NearestFilter) {
            this.error(`[createTexture] Unsupported value for 'magFilter' - supported values are LinearFilter and NearestFilter. Defaulting to LinearFilter.`);
            magFilter = LinearFilter;
        }
        let wrapS = cfg.wrapS || RepeatWrapping;
        if (wrapS !== ClampToEdgeWrapping && wrapS !== MirroredRepeatWrapping && wrapS !== RepeatWrapping) {
            this.error(`[createTexture] Unsupported value for 'wrapS' - supported values are ClampToEdgeWrapping, MirroredRepeatWrapping and RepeatWrapping. Defaulting to RepeatWrapping.`);
            wrapS = RepeatWrapping;
        }
        let wrapT = cfg.wrapT || RepeatWrapping;
        if (wrapT !== ClampToEdgeWrapping && wrapT !== MirroredRepeatWrapping && wrapT !== RepeatWrapping) {
            this.error(`[createTexture] Unsupported value for 'wrapT' - supported values are ClampToEdgeWrapping, MirroredRepeatWrapping and RepeatWrapping. Defaulting to RepeatWrapping.`);
            wrapT = RepeatWrapping;
        }
        let wrapR = cfg.wrapR || RepeatWrapping;
        if (wrapR !== ClampToEdgeWrapping && wrapR !== MirroredRepeatWrapping && wrapR !== RepeatWrapping) {
            this.error(`[createTexture] Unsupported value for 'wrapR' - supported values are ClampToEdgeWrapping, MirroredRepeatWrapping and RepeatWrapping. Defaulting to RepeatWrapping.`);
            wrapR = RepeatWrapping;
        }
        let encoding = cfg.encoding || LinearEncoding;
        if (encoding !== LinearEncoding && encoding !== sRGBEncoding) {
            this.error("[createTexture] Unsupported value for 'encoding' - supported values are LinearEncoding and sRGBEncoding. Defaulting to LinearEncoding.");
            encoding = LinearEncoding;
        }
        const texture = new Texture2D({
            gl: this.scene.canvas.gl,
            minFilter,
            magFilter,
            wrapS,
            wrapT,
            wrapR,
            // flipY: cfg.flipY,
            encoding
        });
        if (cfg.preloadColor) {
            texture.setPreloadColor(cfg.preloadColor);
        }
        if (cfg.image) { // Ignore transcoder for Images
            const image = cfg.image;
            image.crossOrigin = "Anonymous";
            if (image.compressed) {
                // see `parsedImage` in @loaders.gl/gltf/src/lib/parsers/parse-gltf.ts
                // NOTE: @loaders.gl in its current version discards potential mipmaps, leaving only a single one
                const data = image.data;
                texture.setCompressedData({
                    mipmaps: data,
                    props: {
                        format: data[0].format,
                        minFilter: minFilter,
                        magFilter: magFilter
                    }
                });
            } else {
                texture.setImage(image, {minFilter, magFilter, wrapS, wrapT, wrapR, flipY: cfg.flipY, encoding});
            }
        } else if (cfg.src) {
            const ext = cfg.src.split('.').pop();
            switch (ext) { // Don't transcode recognized image file types
                case "jpeg":
                case "jpg":
                case "png":
                case "gif":
                    const image = new Image();
                    image.onload = () => {
                        texture.setImage(image, {
                            minFilter,
                            magFilter,
                            wrapS,
                            wrapT,
                            wrapR,
                            flipY: cfg.flipY,
                            encoding
                        });
                        this.glRedraw();
                    };
                    image.src = cfg.src; // URL or Base64 string
                    break;
                default: // Assume other file types need transcoding
                    if (!this._textureTranscoder) {
                        this.error(`[createTexture] Can't create texture from 'src' - SceneModel needs to be configured with a TextureTranscoder for this file type ('${ext}')`);
                    } else {
                        utils.loadArraybuffer(cfg.src, (arrayBuffer) => {
                                if (!arrayBuffer.byteLength) {
                                    this.error(`[createTexture] Can't create texture from 'src': file data is zero length`);
                                    return;
                                }
                                this._textureTranscoder.transcode([arrayBuffer], texture).then(() => {
                                    this.glRedraw();
                                });
                            },
                            function (errMsg) {
                                this.error(`[createTexture] Can't create texture from 'src': ${errMsg}`);
                            });
                    }
                    break;
            }
        } else if (cfg.buffers) { // Buffers implicitly require transcoding
            if (!this._textureTranscoder) {
                this.error(`[createTexture] Can't create texture from 'buffers' - SceneModel needs to be configured with a TextureTranscoder for this option`);
            } else {
                this._textureTranscoder.transcode(cfg.buffers, texture).then(() => {
                    this.glRedraw();
                });
            }
        }
        this._textures[textureId] = new SceneModelTexture({id: textureId, texture});
    }

    /**
     * Creates a texture set within this SceneModel.
     *
     * * Stores the new {@link SceneModelTextureSet} in {@link SceneModel#textureSets}.
     *
     * A texture set is a collection of textures that can be shared among meshes. We can then supply the texture set
     * ID to {@link SceneModel#createMesh} when we want to create meshes that use the texture set.
     *
     * The textures can work as a texture atlas, where each mesh can have geometry UVs that index
     * a different part of the textures. This allows us to minimize the number of textures in our models, which
     * means faster rendering.
     *
     * @param {*} cfg Texture set properties.
     * @param {String|Number} cfg.id Mandatory ID for the texture set, to refer to with {@link SceneModel#createMesh}.
     * @param {*} [cfg.colorTextureId] ID of *RGBA* base color texture, with color in *RGB* and alpha in *A*.
     * @param {*} [cfg.metallicRoughnessTextureId] ID of *RGBA* metal-roughness texture, with the metallic factor in *R*, and roughness factor in *G*.
     * @param {*} [cfg.normalsTextureId] ID of *RGBA* normal map texture, with normal map vectors in *RGB*.
     * @param {*} [cfg.emissiveTextureId] ID of *RGBA* emissive map texture, with emissive color in *RGB*.
     * @param {*} [cfg.occlusionTextureId] ID of *RGBA* occlusion map texture, with occlusion factor in *R*.
     * @returns {SceneModelTransform} The new texture set.
     */
    createTextureSet(cfg) {
        const textureSetId = cfg.id;
        if (textureSetId === undefined || textureSetId === null) {
            this.error("[createTextureSet] Config missing: id");
            return;
        }
        if (this._textureSets[textureSetId]) {
            this.error(`[createTextureSet] Texture set already created: ${textureSetId}`);
            return;
        }
        let colorTexture;
        if (cfg.colorTextureId !== undefined && cfg.colorTextureId !== null) {
            colorTexture = this._textures[cfg.colorTextureId];
            if (!colorTexture) {
                this.error(`[createTextureSet] Texture not found: ${cfg.colorTextureId} - ensure that you create it first with createTexture()`);
                return;
            }
        } else {
            colorTexture = this._textures[DEFAULT_COLOR_TEXTURE_ID];
        }
        let metallicRoughnessTexture;
        if (cfg.metallicRoughnessTextureId !== undefined && cfg.metallicRoughnessTextureId !== null) {
            metallicRoughnessTexture = this._textures[cfg.metallicRoughnessTextureId];
            if (!metallicRoughnessTexture) {
                this.error(`[createTextureSet] Texture not found: ${cfg.metallicRoughnessTextureId} - ensure that you create it first with createTexture()`);
                return;
            }
        } else {
            metallicRoughnessTexture = this._textures[DEFAULT_METAL_ROUGH_TEXTURE_ID];
        }
        let normalsTexture;
        if (cfg.normalsTextureId !== undefined && cfg.normalsTextureId !== null) {
            normalsTexture = this._textures[cfg.normalsTextureId];
            if (!normalsTexture) {
                this.error(`[createTextureSet] Texture not found: ${cfg.normalsTextureId} - ensure that you create it first with createTexture()`);
                return;
            }
        } else {
            normalsTexture = this._textures[DEFAULT_NORMALS_TEXTURE_ID];
        }
        let emissiveTexture;
        if (cfg.emissiveTextureId !== undefined && cfg.emissiveTextureId !== null) {
            emissiveTexture = this._textures[cfg.emissiveTextureId];
            if (!emissiveTexture) {
                this.error(`[createTextureSet] Texture not found: ${cfg.emissiveTextureId} - ensure that you create it first with createTexture()`);
                return;
            }
        } else {
            emissiveTexture = this._textures[DEFAULT_EMISSIVE_TEXTURE_ID];
        }
        let occlusionTexture;
        if (cfg.occlusionTextureId !== undefined && cfg.occlusionTextureId !== null) {
            occlusionTexture = this._textures[cfg.occlusionTextureId];
            if (!occlusionTexture) {
                this.error(`[createTextureSet] Texture not found: ${cfg.occlusionTextureId} - ensure that you create it first with createTexture()`);
                return;
            }
        } else {
            occlusionTexture = this._textures[DEFAULT_OCCLUSION_TEXTURE_ID];
        }
        const textureSet = new SceneModelTextureSet({
            id: textureSetId,
            model: this,
            colorTexture,
            alphaCutoff: cfg.alphaCutoff,
            metallicRoughnessTexture,
            normalsTexture,
            emissiveTexture,
            occlusionTexture
        });
        this._textureSets[textureSetId] = textureSet;
        return textureSet;
    }

    /**
     * Creates a new {@link SceneModelTransform} within this SceneModel.
     *
     * * Stores the new {@link SceneModelTransform} in {@link SceneModel#transforms}.
     * * Can be connected into hierarchies
     * * Each {@link SceneModelTransform} can be used by unlimited {@link SceneModelMesh}es
     *
     * @param {*} cfg Transform creation parameters.
     * @param {String} cfg.id Mandatory ID for the new transform. Must not clash with any existing components within the {@link Scene}.
     * @param {String} [cfg.parentTransformId] ID of a parent transform, previously created with {@link SceneModel#createTextureSet}.
     * @param {Number[]} [cfg.position=[0,0,0]] Local 3D position of the mesh. Overridden by ````transformId````.
     * @param {Number[]} [cfg.scale=[1,1,1]] Scale of the transform.
     * @param {Number[]} [cfg.rotation=[0,0,0]] Rotation of the transform as Euler angles given in degrees, for each of the X, Y and Z axis.
     * @param {Number[]} [cfg.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]] Modelling transform matrix. Overrides the ````position````, ````scale```` and ````rotation```` parameters.
     * @returns {SceneModelTransform} The new transform.
     */
    createTransform(cfg) {
        if (cfg.id === undefined || cfg.id === null) {
            this.error("[createTransform] SceneModel.createTransform() config missing: id");
            return;
        }
        if (this._transforms[cfg.id]) {
            this.error(`[createTransform] SceneModel already has a transform with this ID: ${cfg.id}`);
            return;
        }
        let parentTransform;
        if (cfg.parentTransformId) {
            parentTransform = this._transforms[cfg.parentTransformId];
            if (!parentTransform) {
                this.error("[createTransform] SceneModel.createTransform() config missing: id");
                return;
            }
        }
        const transform = new SceneModelTransform({
            id: cfg.id,
            model: this,
            parent: parentTransform,
            matrix: cfg.matrix,
            position: cfg.position,
            scale: cfg.scale,
            rotation: cfg.rotation,
            quaternion: cfg.quaternion
        });
        this._transforms[transform.id] = transform;
        return transform;
    }

    /**
     * Creates a new {@link SceneModelMesh} within this SceneModel.
     *
     * * It prepares and saves data for a SceneModelMesh {@link SceneModel#meshes} creation. SceneModelMesh will be created only once the SceneModelEntity (which references this particular SceneModelMesh) will be created.
     * * The SceneModelMesh can either define its own geometry or share it with other SceneModelMeshes. To define own geometry, provide the
     * various geometry arrays to this method. To share a geometry, provide the ID of a geometry created earlier
     * with {@link SceneModel#createGeometry}.
     * * If you accompany the arrays with an  ````origin````, then ````createMesh()```` will assume
     * that the geometry ````positions```` are in relative-to-center (RTC) coordinates, with ````origin```` being the
     * origin of their RTC coordinate system.
     *
     * @param {object} cfg Object properties.
     * @param {String} cfg.id Mandatory ID for the new mesh. Must not clash with any existing components within the {@link Scene}.
     * @param {String|Number} [cfg.textureSetId] ID of a {@link SceneModelTextureSet} previously created with {@link SceneModel#createTextureSet}.
     * @param {String|Number} [cfg.transformId] ID of a {@link SceneModelTransform} to instance, previously created with {@link SceneModel#createTransform}. Overrides all other transform parameters given to this method.
     * @param {String|Number} [cfg.geometryId] ID of a geometry to instance, previously created with {@link SceneModel#createGeometry}. Overrides all other geometry parameters given to this method.
     * @param {String} cfg.primitive The primitive type. Accepted values are 'points', 'lines', 'triangles', 'solid' and 'surface'.
     * @param {Number[]} [cfg.positions] Flat array of uncompressed 3D vertex positions positions. Required for all primitive types. Overridden by ````positionsCompressed````.
     * @param {Number[]} [cfg.positionsCompressed] Flat array of quantized 3D vertex positions. Overrides ````positions````, and must be accompanied by ````positionsDecodeMatrix````.
     * @param {Number[]} [cfg.positionsDecodeMatrix] A 4x4 matrix for decompressing ````positionsCompressed````. Must be accompanied by ````positionsCompressed````.
     * @param {Number[]} [cfg.normals] Flat array of normal vectors. Only used with "triangles", "solid" and "surface" primitives. When no normals are given, the geometry will be flat shaded using auto-generated face-aligned normals.
     * @param {Number[]} [cfg.normalsCompressed] Flat array of oct-encoded normal vectors. Overrides ````normals````. Only used with "triangles", "solid" and "surface" primitives. When no normals are given, the geometry will be flat shaded using auto-generated face-aligned normals.
     * @param {Number[]} [cfg.colors] Flat array of uncompressed RGBA vertex colors, as float values in range ````[0..1]````. Ignored when ````geometryId```` is given. Overridden by ````color```` and ````colorsCompressed````.
     * @param {Number[]} [cfg.colorsCompressed] Flat array of compressed RGBA vertex colors, as unsigned short integers in range ````[0..255]````. Ignored when ````geometryId```` is given. Overrides ````colors```` and is overridden by ````color````.
     * @param {Number[]} [cfg.uv] Flat array of uncompressed vertex UV coordinates. Only used with "triangles", "solid" and "surface" primitives. Required for textured rendering.
     * @param {Number[]} [cfg.uvCompressed] Flat array of compressed vertex UV coordinates. Only used with "triangles", "solid" and "surface" primitives. Overrides ````uv````. Must be accompanied by ````uvDecodeMatrix````. Only used with "triangles", "solid" and "surface" primitives. Required for textured rendering.
     * @param {Number[]} [cfg.uvDecodeMatrix] A 3x3 matrix for decompressing ````uvCompressed````.
     * @param {Number[]} [cfg.indices] Array of primitive connectivity indices. Not required for `points` primitives.
     * @param {Number[]} [cfg.edgeIndices] Array of edge line indices. Used only with 'triangles', 'solid' and 'surface' primitives. Automatically generated internally if not supplied, using the optional ````edgeThreshold```` given to the ````SceneModel```` constructor.
     * @param {Number[]} [cfg.origin] Optional geometry origin, relative to {@link SceneModel#origin}. When this is given, then ````positions```` are assumed to be relative to this.
     * @param {Number[]} [cfg.position=[0,0,0]] Local 3D position of the mesh. Overridden by ````transformId````.
     * @param {Number[]} [cfg.scale=[1,1,1]] Scale of the mesh.  Overridden by ````transformId````.
     * @param {Number[]} [cfg.rotation=[0,0,0]] Rotation of the mesh as Euler angles given in degrees, for each of the X, Y and Z axis.  Overridden by ````transformId````.
     * @param {Number[]} [cfg.quaternion] Rotation of the mesh as a quaternion.  Overridden by ````rotation````.
     * @param {Number[]} [cfg.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]] Mesh modelling transform matrix. Overrides the ````position````, ````scale```` and ````rotation```` parameters. Also  overridden by ````transformId````.
     * @param {Number[]} [cfg.color=[1,1,1]] RGB color in range ````[0..1, 0..1, 0..1]````. Overridden by texture set ````colorTexture````. Overrides ````colors```` and ````colorsCompressed````.
     * @param {Number} [cfg.opacity=1] Opacity in range ````[0..1]````. Overridden by texture set ````colorTexture````.
     * @param {Number} [cfg.metallic=0] Metallic factor in range ````[0..1]````. Overridden by texture set ````metallicRoughnessTexture````.
     * @param {Number} [cfg.roughness=1] Roughness factor in range ````[0..1]````. Overridden by texture set ````metallicRoughnessTexture````.
     * @returns {SceneModelMesh} The new mesh.
     */
    createMesh(cfg) {

        if (cfg.id === undefined || cfg.id === null) {
            this.error("[createMesh] SceneModel.createMesh() config missing: id");
            return false;
        }

        if (this._meshes[cfg.id]) {
            this.error(`[createMesh] SceneModel already has a mesh with this ID: ${cfg.id}`);
            return false;
        }

        const instancing = (cfg.geometryId !== undefined);
        const batching = !instancing;

        if (batching) {

            // Batched geometry

            if (cfg.primitive === undefined || cfg.primitive === null) {
                cfg.primitive = "triangles";
            }
            if (cfg.primitive !== "points" && cfg.primitive !== "lines" && cfg.primitive !== "triangles" && cfg.primitive !== "solid" && cfg.primitive !== "surface") {
                this.error(`Unsupported value for 'primitive': '${primitive}'  ('geometryId' is absent) - supported values are 'points', 'lines', 'triangles', 'solid' and 'surface'.`);
                return false;
            }
            if (!cfg.positions && !cfg.positionsCompressed && !cfg.buckets) {
                this.error("Param expected: 'positions',  'positionsCompressed' or `buckets`  ('geometryId' is absent)");
                return false;
            }
            if (cfg.positions && (cfg.positionsDecodeMatrix || cfg.positionsDecodeBoundary)) {
                this.error("Illegal params: 'positions' not expected with 'positionsDecodeMatrix'/'positionsDecodeBoundary' ('geometryId' is absent)");
                return false;
            }
            if (cfg.positionsCompressed && !cfg.positionsDecodeMatrix && !cfg.positionsDecodeBoundary) {
                this.error("Param expected: 'positionsCompressed' should be accompanied by 'positionsDecodeMatrix'/'positionsDecodeBoundary' ('geometryId' is absent)");
                return false;
            }
            if (cfg.uvCompressed && !cfg.uvDecodeMatrix) {
                this.error("Param expected: 'uvCompressed' should be accompanied by `uvDecodeMatrix` ('geometryId' is absent)");
                return false;
            }
            if (!cfg.buckets && !cfg.indices && (cfg.primitive === "triangles" || cfg.primitive === "solid" || cfg.primitive === "surface")) {
                const numPositions = (cfg.positions || cfg.positionsCompressed).length / 3;
                cfg.indices = this._createDefaultIndices(numPositions);
            }
            if (!cfg.buckets && !cfg.indices && cfg.primitive !== "points") {
                cfg.indices = this._createDefaultIndices(numIndices);
                this.error(`Param expected: indices (required for '${cfg.primitive}' primitive type)`);
                return false;
            }
            if ((cfg.matrix || cfg.position || cfg.rotation || cfg.scale) && (cfg.positionsCompressed || cfg.positionsDecodeBoundary)) {
                this.error("Unexpected params: 'matrix', 'rotation', 'scale', 'position' not allowed with 'positionsCompressed'");
                return false;
            }

            const useDTX = (!!this._dtxEnabled && (cfg.primitive === "triangles"
                    || cfg.primitive === "solid"
                    || cfg.primitive === "surface"))
                && (!cfg.textureSetId);

            cfg.origin = cfg.origin ? math.addVec3(this._origin, cfg.origin, math.vec3()) : this._origin;

            // MATRIX - optional for batching

            if (cfg.matrix) {
                cfg.meshMatrix = cfg.matrix;
            } else if (cfg.scale || cfg.rotation || cfg.position || cfg.quaternion) {
                const scale = cfg.scale || DEFAULT_SCALE;
                const position = cfg.position || DEFAULT_POSITION;
                if (cfg.rotation) {
                    math.eulerToQuaternion(cfg.rotation, "XYZ", tempQuaternion);
                    cfg.meshMatrix = math.composeMat4(position, tempQuaternion, scale, math.mat4());
                } else {
                    cfg.meshMatrix = math.composeMat4(position, cfg.quaternion || DEFAULT_QUATERNION, scale, math.mat4());
                }
            }

            if (cfg.positionsDecodeBoundary) {
                cfg.positionsDecodeMatrix = createPositionsDecodeMatrix(cfg.positionsDecodeBoundary, math.mat4());
            }

            if (useDTX) {

                // DTX

                cfg.type = DTX;

                // NPR

                cfg.color = (cfg.color) ? new Uint8Array([Math.floor(cfg.color[0] * 255), Math.floor(cfg.color[1] * 255), Math.floor(cfg.color[2] * 255)]) : defaultCompressedColor;
                cfg.opacity = (cfg.opacity !== undefined && cfg.opacity !== null) ? Math.floor(cfg.opacity * 255) : 255;

                // RTC

                if (cfg.positions) {
                    const rtcCenter = math.vec3();
                    const rtcPositions = [];
                    const rtcNeeded = worldToRTCPositions(cfg.positions, rtcPositions, rtcCenter);
                    if (rtcNeeded) {
                        cfg.positions = rtcPositions;
                        cfg.origin = math.addVec3(cfg.origin, rtcCenter, rtcCenter);
                    }
                }

                // COMPRESSION

                if (cfg.positions) {
                    const aabb = math.collapseAABB3();
                    cfg.positionsDecodeMatrix = math.mat4();
                    math.expandAABB3Points3(aabb, cfg.positions);
                    cfg.positionsCompressed = quantizePositions(cfg.positions, aabb, cfg.positionsDecodeMatrix);
                    cfg.aabb = aabb;

                } else if (cfg.positionsCompressed) {
                    const aabb = math.collapseAABB3();
                    math.expandAABB3Points3(aabb, cfg.positionsCompressed);
                    geometryCompressionUtils.decompressAABB(aabb, cfg.positionsDecodeMatrix);
                    cfg.aabb = aabb;

                }
                if (cfg.buckets) {
                    const aabb = math.collapseAABB3();
                    for (let i = 0, len = cfg.buckets.length; i < len; i++) {
                        const bucket = cfg.buckets[i];
                        if (bucket.positions) {
                            math.expandAABB3Points3(aabb, bucket.positions);
                        } else if (bucket.positionsCompressed) {
                            math.expandAABB3Points3(aabb, bucket.positionsCompressed);
                        }
                    }
                    if (cfg.positionsDecodeMatrix) {
                        geometryCompressionUtils.decompressAABB(aabb, cfg.positionsDecodeMatrix);
                    }
                    cfg.aabb = aabb;
                }

                if (cfg.meshMatrix) {
                    math.AABB3ToOBB3(cfg.aabb, tempOBB3);
                    math.transformOBB3(cfg.meshMatrix, tempOBB3);
                    math.OBB3ToAABB3(tempOBB3, cfg.aabb);
                }

                // EDGES

                if (!cfg.buckets && !cfg.edgeIndices && (cfg.primitive === "triangles" || cfg.primitive === "solid" || cfg.primitive === "surface")) {
                    if (cfg.positions) { // Faster
                        cfg.edgeIndices = buildEdgeIndices(cfg.positions, cfg.indices, null, 2.0);
                    } else {
                        cfg.edgeIndices = buildEdgeIndices(cfg.positionsCompressed, cfg.indices, cfg.positionsDecodeMatrix, 2.0);
                    }
                }

                // BUCKETING

                if (!cfg.buckets) {
                    cfg.buckets = createDTXBuckets(cfg, this._enableVertexWelding && this._enableIndexBucketing);
                }

            } else {

                // VBO

                cfg.type = VBO_BATCHED;

                // PBR

                cfg.color = (cfg.color) ? new Uint8Array([Math.floor(cfg.color[0] * 255), Math.floor(cfg.color[1] * 255), Math.floor(cfg.color[2] * 255)]) : [255, 255, 255];
                cfg.opacity = (cfg.opacity !== undefined && cfg.opacity !== null) ? Math.floor(cfg.opacity * 255) : 255;
                cfg.metallic = (cfg.metallic !== undefined && cfg.metallic !== null) ? Math.floor(cfg.metallic * 255) : 0;
                cfg.roughness = (cfg.roughness !== undefined && cfg.roughness !== null) ? Math.floor(cfg.roughness * 255) : 255;

                // RTC

                if (cfg.positions) {
                    const rtcPositions = [];
                    const rtcNeeded = worldToRTCPositions(cfg.positions, rtcPositions, tempVec3a$d);
                    if (rtcNeeded) {
                        cfg.positions = rtcPositions;
                        cfg.origin = math.addVec3(cfg.origin, tempVec3a$d, math.vec3());
                    }
                }

                if (cfg.positions) {
                    const aabb = math.collapseAABB3();
                    if (cfg.meshMatrix) {
                        math.transformPositions3(cfg.meshMatrix, cfg.positions, cfg.positions);
                        cfg.meshMatrix = null; // Positions now baked, don't need any more
                    }
                    math.expandAABB3Points3(aabb, cfg.positions);
                    cfg.aabb = aabb;

                } else {
                    const aabb = math.collapseAABB3();
                    math.expandAABB3Points3(aabb, cfg.positionsCompressed);
                    geometryCompressionUtils.decompressAABB(aabb, cfg.positionsDecodeMatrix);
                    cfg.aabb = aabb;
                }

                if (cfg.meshMatrix) {
                    math.AABB3ToOBB3(cfg.aabb, tempOBB3);
                    math.transformOBB3(cfg.meshMatrix, tempOBB3);
                    math.OBB3ToAABB3(tempOBB3, cfg.aabb);
                }

                // EDGES

                if (!cfg.buckets && !cfg.edgeIndices && (cfg.primitive === "triangles" || cfg.primitive === "solid" || cfg.primitive === "surface")) {
                    if (cfg.positions) {
                        cfg.edgeIndices = buildEdgeIndices(cfg.positions, cfg.indices, null, 2.0);
                    } else {
                        cfg.edgeIndices = buildEdgeIndices(cfg.positionsCompressed, cfg.indices, cfg.positionsDecodeMatrix, 2.0);
                    }
                }

                // TEXTURE

                // cfg.textureSetId = cfg.textureSetId || DEFAULT_TEXTURE_SET_ID;
                if (cfg.textureSetId) {
                    cfg.textureSet = this._textureSets[cfg.textureSetId];
                    if (!cfg.textureSet) {
                        this.error(`[createMesh] Texture set not found: ${cfg.textureSetId} - ensure that you create it first with createTextureSet()`);
                        return false;
                    }
                }
            }

        } else {

            // INSTANCING

            if (cfg.positions || cfg.positionsCompressed || cfg.indices || cfg.edgeIndices || cfg.normals || cfg.normalsCompressed || cfg.uv || cfg.uvCompressed || cfg.positionsDecodeMatrix) {
                this.error(`Mesh geometry parameters not expected when instancing a geometry (not expected: positions, positionsCompressed, indices, edgeIndices, normals, normalsCompressed, uv, uvCompressed, positionsDecodeMatrix)`);
                return false;
            }

            cfg.geometry = this._geometries[cfg.geometryId];
            if (!cfg.geometry) {
                this.error(`[createMesh] Geometry not found: ${cfg.geometryId} - ensure that you create it first with createGeometry()`);
                return false;
            }

            cfg.origin = cfg.origin ? math.addVec3(this._origin, cfg.origin, math.vec3()) : this._origin;
            cfg.positionsDecodeMatrix = cfg.geometry.positionsDecodeMatrix;

            if (cfg.transformId) {

                // TRANSFORM

                cfg.transform = this._transforms[cfg.transformId];

                if (!cfg.transform) {
                    this.error(`[createMesh] Transform not found: ${cfg.transformId} - ensure that you create it first with createTransform()`);
                    return false;
                }

                cfg.aabb = cfg.geometry.aabb;

            } else {

                // MATRIX

                if (cfg.matrix) {
                    cfg.meshMatrix = cfg.matrix;
                } else if (cfg.scale || cfg.rotation || cfg.position || cfg.quaternion) {
                    const scale = cfg.scale || DEFAULT_SCALE;
                    const position = cfg.position || DEFAULT_POSITION;
                    if (cfg.rotation) {
                        math.eulerToQuaternion(cfg.rotation, "XYZ", tempQuaternion);
                        cfg.meshMatrix = math.composeMat4(position, tempQuaternion, scale, math.mat4());
                    } else {
                        cfg.meshMatrix = math.composeMat4(position, cfg.quaternion || DEFAULT_QUATERNION, scale, math.mat4());
                    }
                }

                math.AABB3ToOBB3(cfg.geometry.aabb, tempOBB3);
                math.transformOBB3(cfg.meshMatrix, tempOBB3);
                cfg.aabb = math.OBB3ToAABB3(tempOBB3, math.AABB3());
            }

            const useDTX = (!!this._dtxEnabled
                    && (cfg.geometry.primitive === "triangles"
                        || cfg.geometry.primitive === "solid"
                        || cfg.geometry.primitive === "surface"))
                && (!cfg.textureSetId);

            if (useDTX) {

                // DTX

                cfg.type = DTX;

                // NPR

                cfg.color = (cfg.color) ? new Uint8Array([Math.floor(cfg.color[0] * 255), Math.floor(cfg.color[1] * 255), Math.floor(cfg.color[2] * 255)]) : defaultCompressedColor;
                cfg.opacity = (cfg.opacity !== undefined && cfg.opacity !== null) ? Math.floor(cfg.opacity * 255) : 255;

                // BUCKETING - lazy generated, reused

                let buckets = this._dtxBuckets[cfg.geometryId];
                if (!buckets) {
                    buckets = createDTXBuckets(cfg.geometry, this._enableVertexWelding, this._enableIndexBucketing);
                    this._dtxBuckets[cfg.geometryId] = buckets;
                }
                cfg.buckets = buckets;

            } else {

                // VBO

                cfg.type = VBO_INSTANCED;

                // PBR

                cfg.color = (cfg.color) ? new Uint8Array([Math.floor(cfg.color[0] * 255), Math.floor(cfg.color[1] * 255), Math.floor(cfg.color[2] * 255)]) : defaultCompressedColor;
                cfg.opacity = (cfg.opacity !== undefined && cfg.opacity !== null) ? Math.floor(cfg.opacity * 255) : 255;
                cfg.metallic = (cfg.metallic !== undefined && cfg.metallic !== null) ? Math.floor(cfg.metallic * 255) : 0;
                cfg.roughness = (cfg.roughness !== undefined && cfg.roughness !== null) ? Math.floor(cfg.roughness * 255) : 255;

                // TEXTURE

                if (cfg.textureSetId) {
                    cfg.textureSet = this._textureSets[cfg.textureSetId];
                    // if (!cfg.textureSet) {
                    //     this.error(`[createMesh] Texture set not found: ${cfg.textureSetId} - ensure that you create it first with createTextureSet()`);
                    //     return false;
                    // }
                }
            }
        }

        cfg.numPrimitives = this._getNumPrimitives(cfg);

        return this._createMesh(cfg);
    }

    _createDefaultIndices(numIndices) {
        const indices = [];
        for (let i = 0; i < numIndices; i++) {
            indices.push(i);
        }
        return indices;
    }

    _createMesh(cfg) {
        const mesh = new SceneModelMesh(this, cfg.id, cfg.color, cfg.opacity, cfg.transform, cfg.textureSet);
        mesh.pickId = this.scene._renderer.getPickID(mesh);
        const pickId = mesh.pickId;
        const a = pickId >> 24 & 0xFF;
        const b = pickId >> 16 & 0xFF;
        const g = pickId >> 8 & 0xFF;
        const r = pickId & 0xFF;
        cfg.pickColor = new Uint8Array([r, g, b, a]); // Quantized pick color
        cfg.solid = (cfg.primitive === "solid");
        mesh.origin = math.vec3(cfg.origin);
        switch (cfg.type) {
            case DTX:
                mesh.layer = this._getDTXLayer(cfg);
                mesh.aabb = cfg.aabb;
                break;
            case VBO_BATCHED:
                mesh.layer = this._getVBOBatchingLayer(cfg);
                mesh.aabb = cfg.aabb;
                break;
            case VBO_INSTANCED:
                mesh.layer = this._getVBOInstancingLayer(cfg);
                mesh.aabb = cfg.aabb;
                break;
        }
        if (cfg.transform) {
            cfg.meshMatrix = cfg.transform.worldMatrix;
        }
        mesh.portionId = mesh.layer.createPortion(mesh, cfg);
        mesh.numPrimitives = cfg.numPrimitives;
        this._meshes[cfg.id] = mesh;
        this._unusedMeshes[cfg.id] = mesh;
        this._meshList.push(mesh);
        return mesh;
    }

    _getNumPrimitives(cfg) {
        let countIndices = 0;
        const primitive = cfg.geometry ? cfg.geometry.primitive : cfg.primitive;
        switch (primitive) {
            case "triangles":
            case "solid":
            case "surface":
                switch (cfg.type) {
                    case DTX:
                        for (let i = 0, len = cfg.buckets.length; i < len; i++) {
                            countIndices += cfg.buckets[i].indices.length;
                        }
                        break;
                    case VBO_BATCHED:
                        countIndices += cfg.indices.length;
                        break;
                    case VBO_INSTANCED:
                        countIndices += cfg.geometry.indices.length;
                        break;
                }
                return Math.round(countIndices / 3);
            case "points":
                switch (cfg.type) {
                    case DTX:
                        for (let i = 0, len = cfg.buckets.length; i < len; i++) {
                            countIndices += cfg.buckets[i].positionsCompressed.length;
                        }
                        break;
                    case VBO_BATCHED:
                        countIndices += cfg.positions ? cfg.positions.length : cfg.positionsCompressed.length;
                        break;
                    case VBO_INSTANCED:
                        const geometry = cfg.geometry;
                        countIndices += geometry.positions ? geometry.positions.length : geometry.positionsCompressed.length;
                        break;
                }
                return Math.round(countIndices);
            case "lines":
            case "line-strip":
                switch (cfg.type) {
                    case DTX:
                        for (let i = 0, len = cfg.buckets.length; i < len; i++) {
                            countIndices += cfg.buckets[i].indices.length;
                        }
                        break;
                    case VBO_BATCHED:
                        countIndices += cfg.indices.length;
                        break;
                    case VBO_INSTANCED:
                        countIndices += cfg.geometry.indices.length;
                        break;
                }
                return Math.round(countIndices / 2);
        }
        return 0;
    }

    _getDTXLayer(cfg) {
        const origin = cfg.origin;
        const primitive = cfg.geometry ? cfg.geometry.primitive : cfg.primitive;
        const layerId = `.${primitive}.${Math.round(origin[0])}.${Math.round(origin[1])}.${Math.round(origin[2])}`;
        let dtxLayer = this._dtxLayers[layerId];
        if (dtxLayer) {
            if (!dtxLayer.canCreatePortion(cfg)) {
                //  dtxLayer.finalize();
                delete this._dtxLayers[layerId];
                dtxLayer = null;
            } else {
                return dtxLayer;
            }
        }
        switch (primitive) {
            case "triangles":
            case "solid":
            case "surface":
                dtxLayer = new DTXTrianglesLayer(this, {layerIndex: 0, origin, primitive}); // layerIndex is set in #finalize()
                break;
            case "lines":
                dtxLayer = new DTXLinesLayer(this, {layerIndex: 0, origin, primitive}); // layerIndex is set in #finalize()
                break;
            default:
                return;
        }
        this._dtxLayers[layerId] = dtxLayer;
        this.layerList.push(dtxLayer);
        this._layersToFinalize.push(dtxLayer);
        return dtxLayer;
    }

    _getVBOBatchingLayer(cfg) {
        const model = this;
        const origin = cfg.origin;
        cfg.renderLayer || 0;
        const positionsDecodeHash = cfg.positionsDecodeMatrix || cfg.positionsDecodeBoundary ?
            this._createHashStringFromMatrix(cfg.positionsDecodeMatrix || cfg.positionsDecodeBoundary)
            : "-";
        const textureSetId = cfg.textureSetId || "-";
        const layerId = `${Math.round(origin[0])}.${Math.round(origin[1])}.${Math.round(origin[2])}.${cfg.primitive}.${positionsDecodeHash}.${textureSetId}`;
        let vboBatchingLayer = this._vboBatchingLayers[layerId];
        if (vboBatchingLayer) {
            return vboBatchingLayer;
        }
        let textureSet = cfg.textureSet;
        while (!vboBatchingLayer) {
            switch (cfg.primitive) {
                case "triangles":
                    // console.info(`[SceneModel ${this.id}]: creating TrianglesBatchingLayer`);
                    vboBatchingLayer = new VBOBatchingTrianglesLayer({
                        model,
                        textureSet,
                        layerIndex: 0, // This is set in #finalize()
                        scratchMemory: this._vboBatchingLayerScratchMemory,
                        positionsDecodeMatrix: cfg.positionsDecodeMatrix,  // Can be undefined
                        uvDecodeMatrix: cfg.uvDecodeMatrix, // Can be undefined
                        origin,
                        maxGeometryBatchSize: this._maxGeometryBatchSize,
                        solid: (cfg.primitive === "solid"),
                        autoNormals: true,
                        primitive: cfg.primitive
                    });
                    break;
                case "solid":
                    // console.info(`[SceneModel ${this.id}]: creating TrianglesBatchingLayer`);
                    vboBatchingLayer = new VBOBatchingTrianglesLayer({
                        model,
                        textureSet,
                        layerIndex: 0, // This is set in #finalize()
                        scratchMemory: this._vboBatchingLayerScratchMemory,
                        positionsDecodeMatrix: cfg.positionsDecodeMatrix,  // Can be undefined
                        uvDecodeMatrix: cfg.uvDecodeMatrix, // Can be undefined
                        origin,
                        maxGeometryBatchSize: this._maxGeometryBatchSize,
                        solid: (cfg.primitive === "solid"),
                        autoNormals: true,
                        primitive: cfg.primitive
                    });
                    break;
                case "surface":
                    // console.info(`[SceneModel ${this.id}]: creating TrianglesBatchingLayer`);
                    vboBatchingLayer = new VBOBatchingTrianglesLayer({
                        model,
                        textureSet,
                        layerIndex: 0, // This is set in #finalize()
                        scratchMemory: this._vboBatchingLayerScratchMemory,
                        positionsDecodeMatrix: cfg.positionsDecodeMatrix,  // Can be undefined
                        uvDecodeMatrix: cfg.uvDecodeMatrix, // Can be undefined
                        origin,
                        maxGeometryBatchSize: this._maxGeometryBatchSize,
                        solid: (cfg.primitive === "solid"),
                        autoNormals: true,
                        primitive: cfg.primitive
                    });
                    break;
                case "lines":
                    // console.info(`[SceneModel ${this.id}]: creating VBOBatchingLinesLayer`);
                    vboBatchingLayer = new VBOBatchingLinesLayer({
                        model,
                        layerIndex: 0, // This is set in #finalize()
                        scratchMemory: this._vboBatchingLayerScratchMemory,
                        positionsDecodeMatrix: cfg.positionsDecodeMatrix,  // Can be undefined
                        uvDecodeMatrix: cfg.uvDecodeMatrix, // Can be undefined
                        origin,
                        maxGeometryBatchSize: this._maxGeometryBatchSize,
                        primitive: cfg.primitive
                    });
                    break;
                case "points":
                    // console.info(`[SceneModel ${this.id}]: creating VBOBatchingPointsLayer`);
                    vboBatchingLayer = new VBOBatchingPointsLayer({
                        model,
                        layerIndex: 0, // This is set in #finalize()
                        scratchMemory: this._vboBatchingLayerScratchMemory,
                        positionsDecodeMatrix: cfg.positionsDecodeMatrix,  // Can be undefined
                        uvDecodeMatrix: cfg.uvDecodeMatrix, // Can be undefined
                        origin,
                        maxGeometryBatchSize: this._maxGeometryBatchSize,
                        primitive: cfg.primitive
                    });
                    break;
            }
            const lenPositions = cfg.positionsCompressed ? cfg.positionsCompressed.length : cfg.positions.length;
            const canCreatePortion = (cfg.primitive === "points")
                ? vboBatchingLayer.canCreatePortion(lenPositions)
                : vboBatchingLayer.canCreatePortion(lenPositions, cfg.indices.length);
            if (!canCreatePortion) {
                vboBatchingLayer.finalize();
                delete this._vboBatchingLayers[layerId];
                vboBatchingLayer = null;
            }
        }
        this._vboBatchingLayers[layerId] = vboBatchingLayer;
        this.layerList.push(vboBatchingLayer);
        this._layersToFinalize.push(vboBatchingLayer);
        return vboBatchingLayer;
    }

    _createHashStringFromMatrix(matrix) {
        const matrixString = matrix.join('');
        let hash = 0;
        for (let i = 0; i < matrixString.length; i++) {
            const char = matrixString.charCodeAt(i);
            hash = (hash << 5) - hash + char;
            hash |= 0; // Convert to 32-bit integer
        }
        const hashString = (hash >>> 0).toString(16);
        return hashString;
    }

    _getVBOInstancingLayer(cfg) {
        const model = this;
        const origin = cfg.origin;
        const textureSetId = cfg.textureSetId || "-";
        const geometryId = cfg.geometryId;
        const layerId = `${Math.round(origin[0])}.${Math.round(origin[1])}.${Math.round(origin[2])}.${textureSetId}.${geometryId}`;
        let vboInstancingLayer = this._vboInstancingLayers[layerId];
        if (vboInstancingLayer) {
            return vboInstancingLayer;
        }
        let textureSet = cfg.textureSet;
        const geometry = cfg.geometry;
        while (!vboInstancingLayer) {
            switch (geometry.primitive) {
                case "triangles":
                    // console.info(`[SceneModel ${this.id}]: creating TrianglesInstancingLayer`);
                    vboInstancingLayer = new VBOInstancingTrianglesLayer({
                        model,
                        textureSet,
                        geometry,
                        origin,
                        layerIndex: 0,
                        solid: false
                    });
                    break;
                case "solid":
                    // console.info(`[SceneModel ${this.id}]: creating TrianglesInstancingLayer`);
                    vboInstancingLayer = new VBOInstancingTrianglesLayer({
                        model,
                        textureSet,
                        geometry,
                        origin,
                        layerIndex: 0,
                        solid: true
                    });
                    break;
                case "surface":
                    // console.info(`[SceneModel ${this.id}]: creating TrianglesInstancingLayer`);
                    vboInstancingLayer = new VBOInstancingTrianglesLayer({
                        model,
                        textureSet,
                        geometry,
                        origin,
                        layerIndex: 0,
                        solid: false
                    });
                    break;
                case "lines":
                    // console.info(`[SceneModel ${this.id}]: creating VBOInstancingLinesLayer`);
                    vboInstancingLayer = new VBOInstancingLinesLayer({
                        model,
                        textureSet,
                        geometry,
                        origin,
                        layerIndex: 0
                    });
                    break;
                case "points":
                    // console.info(`[SceneModel ${this.id}]: creating PointsInstancingLayer`);
                    vboInstancingLayer = new VBOInstancingPointsLayer({
                        model,
                        textureSet,
                        geometry,
                        origin,
                        layerIndex: 0
                    });
                    break;
            }
            // const lenPositions = geometry.positionsCompressed.length;
            // if (!vboInstancingLayer.canCreatePortion(lenPositions, geometry.indices.length)) { // FIXME: indices should be optional
            //     vboInstancingLayer.finalize();
            //     delete this._vboInstancingLayers[layerId];
            //     vboInstancingLayer = null;
            // }
        }
        this._vboInstancingLayers[layerId] = vboInstancingLayer;
        this.layerList.push(vboInstancingLayer);
        this._layersToFinalize.push(vboInstancingLayer);
        return vboInstancingLayer;
    }

    /**
     * Creates a {@link SceneModelEntity} within this SceneModel.
     *
     * * Gives the SceneModelEntity one or more {@link SceneModelMesh}es previously created with
     * {@link SceneModel#createMesh}. A SceneModelMesh can only belong to one SceneModelEntity, so you'll get an
     * error if you try to reuse a mesh among multiple SceneModelEntitys.
     * * The SceneModelEntity can have a {@link SceneModelTextureSet}, previously created with
     * {@link SceneModel#createTextureSet}. A SceneModelTextureSet can belong to multiple SceneModelEntitys.
     * * The SceneModelEntity can have a geometry, previously created with
     * {@link SceneModel#createTextureSet}. A geometry is a "virtual component" and can belong to multiple SceneModelEntitys.
     *
     * @param {Object} cfg SceneModelEntity configuration.
     * @param {String} cfg.id Optional ID for the new SceneModelEntity. Must not clash with any existing components within the {@link Scene}.
     * @param {String[]} cfg.meshIds IDs of one or more meshes created previously with {@link SceneModel@createMesh}.
     * @param {Boolean} [cfg.isObject] Set ````true```` if the {@link SceneModelEntity} represents an object, in which case it will be registered by {@link SceneModelEntity#id} in {@link Scene#objects} and can also have a corresponding {@link MetaObject} with matching {@link MetaObject#id}, registered by that ID in {@link MetaScene#metaObjects}.
     * @param {Boolean} [cfg.visible=true] Indicates if the SceneModelEntity is initially visible.
     * @param {Boolean} [cfg.culled=false] Indicates if the SceneModelEntity is initially culled from view.
     * @param {Boolean} [cfg.pickable=true] Indicates if the SceneModelEntity is initially pickable.
     * @param {Boolean} [cfg.clippable=true] Indicates if the SceneModelEntity is initially clippable.
     * @param {Boolean} [cfg.collidable=true] Indicates if the SceneModelEntity is initially included in boundary calculations.
     * @param {Boolean} [cfg.castsShadow=true] Indicates if the SceneModelEntity initially casts shadows.
     * @param {Boolean} [cfg.receivesShadow=true]  Indicates if the SceneModelEntity initially receives shadows.
     * @param {Boolean} [cfg.xrayed=false] Indicates if the SceneModelEntity is initially xrayed. XRayed appearance is configured by {@link SceneModel#xrayMaterial}.
     * @param {Boolean} [cfg.highlighted=false] Indicates if the SceneModelEntity is initially highlighted. Highlighted appearance is configured by {@link SceneModel#highlightMaterial}.
     * @param {Boolean} [cfg.selected=false] Indicates if the SceneModelEntity is initially selected. Selected appearance is configured by {@link SceneModel#selectedMaterial}.
     * @param {Boolean} [cfg.edges=false] Indicates if the SceneModelEntity's edges are initially emphasized. Edges appearance is configured by {@link SceneModel#edgeMaterial}.
     * @returns {SceneModelEntity} The new SceneModelEntity.
     */
    createEntity(cfg) {
        if (cfg.id === undefined) {
            cfg.id = math.createUUID();
        } else if (this.scene.components[cfg.id]) {
            this.error(`Scene already has a Component with this ID: ${cfg.id} - will assign random ID`);
            cfg.id = math.createUUID();
        }
        if (cfg.meshIds === undefined) {
            this.error("Config missing: meshIds");
            return;
        }
        let flags = 0;
        if (this._visible && cfg.visible !== false) {
            flags = flags | ENTITY_FLAGS.VISIBLE;
        }
        if (this._pickable && cfg.pickable !== false) {
            flags = flags | ENTITY_FLAGS.PICKABLE;
        }
        if (this._culled && cfg.culled !== false) {
            flags = flags | ENTITY_FLAGS.CULLED;
        }
        if (this._clippable && cfg.clippable !== false) {
            flags = flags | ENTITY_FLAGS.CLIPPABLE;
        }
        if (this._collidable && cfg.collidable !== false) {
            flags = flags | ENTITY_FLAGS.COLLIDABLE;
        }
        if (this._edges && cfg.edges !== false) {
            flags = flags | ENTITY_FLAGS.EDGES;
        }
        if (this._xrayed && cfg.xrayed !== false) {
            flags = flags | ENTITY_FLAGS.XRAYED;
        }
        if (this._highlighted && cfg.highlighted !== false) {
            flags = flags | ENTITY_FLAGS.HIGHLIGHTED;
        }
        if (this._selected && cfg.selected !== false) {
            flags = flags | ENTITY_FLAGS.SELECTED;
        }
        cfg.flags = flags;
        return this._createEntity(cfg);
    }

    _createEntity(cfg) {
        let meshes = [];
        for (let i = 0, len = cfg.meshIds.length; i < len; i++) {
            const meshId = cfg.meshIds[i];
            let mesh = this._meshes[meshId]; // Trying to get already created mesh
            if (!mesh) { // Checks if there is already created mesh for this meshId
                this.error(`Mesh with this ID not found: "${meshId}" - ignoring this mesh`); // There is no such cfg
                continue;
            }
            if (mesh.parent) {
                this.error(`Mesh with ID "${meshId}" already belongs to object with ID "${mesh.parent.id}" - ignoring this mesh`);
                continue;
            }
            meshes.push(mesh);
            delete this._unusedMeshes[meshId];
        }
        const lodCullable = true;
        const entity = new SceneModelEntity(
            this,
            cfg.isObject,
            cfg.id,
            meshes,
            cfg.flags,
            lodCullable); // Internally sets SceneModelEntity#parent to this SceneModel
        this._entityList.push(entity);
        this._entities[cfg.id] = entity;
        this._entitiesToFinalize.push(entity);
        this.numEntities++;
        return entity;
    }

    /**
     * Pre-renders all meshes that have been added, even if the SceneModel has not bee finalized yet.
     * This is use for progressively showing the SceneModel while it is being loaded or constructed.
     * @returns {boolean}
     */
    preFinalize() {
        if (this.destroyed) {
            return false;
        }
        if (this._layersToFinalize.length === 0) {
            return false;
        }
        this._createDummyEntityForUnusedMeshes();
        for (let i = 0, len = this._layersToFinalize.length; i < len; i++) {
            const layer = this._layersToFinalize[i];
            layer.finalize();
        }
        this._vboBatchingLayers = {};
        this._vboInstancingLayers = {};
        this._dtxLayers = {};
        this._layersToFinalize = [];
        for (let i = 0, len = this._entitiesToFinalize.length; i < len; i++) {
            const entity = this._entitiesToFinalize[i];
            entity._finalize();
        }
        for (let i = 0, len = this._entitiesToFinalize.length; i < len; i++) {
            const entity = this._entitiesToFinalize[i];
            entity._finalize2();
        }
        this._entitiesToFinalize = [];
        this.scene._aabbDirty = true;
        this._viewMatrixDirty = true;
        this._matrixDirty = true;
        this._aabbDirty = true;
        this._setWorldMatrixDirty();
        this._sceneModelDirty();
        this.position = this._position;
        // Sort layers to reduce WebGL shader switching when rendering them
        this.layerList.sort((a, b) => {
            if (a.sortId < b.sortId) {
                return -1;
            }
            if (a.sortId > b.sortId) {
                return 1;
            }
            return 0;
        });
        for (let i = 0, len = this.layerList.length; i < len; i++) {
            const layer = this.layerList[i];
            layer.layerIndex = i;
        }
        this.glRedraw();
        this._layersFinalized = true;
    }

    /**
     * Finalizes this SceneModel.
     *
     * Once finalized, you can't add anything more to this SceneModel.
     */
    finalize() {
        if (this.destroyed) {
            return;
        }
        this.preFinalize();
        this._geometries = {};
        this._dtxBuckets = {};
        this._textures = {};
        this._textureSets = {};
    }

    /** @private */
    stateSortCompare(drawable1, drawable2) {
    }

    /** @private */
    rebuildRenderFlags() {
        this.renderFlags.reset();
        this._updateRenderFlagsVisibleLayers();
        if (this.renderFlags.numLayers > 0 && this.renderFlags.numVisibleLayers === 0) {
            this.renderFlags.culled = true;
            return;
        }
        this._updateRenderFlags();
    }

    /**
     * @private
     */
    _updateRenderFlagsVisibleLayers() {
        const renderFlags = this.renderFlags;
        renderFlags.numLayers = this.layerList.length;
        renderFlags.numVisibleLayers = 0;
        for (let layerIndex = 0, len = this.layerList.length; layerIndex < len; layerIndex++) {
            const layer = this.layerList[layerIndex];
            const layerVisible = this._getActiveSectionPlanesForLayer(layer);
            if (layerVisible) {
                renderFlags.visibleLayers[renderFlags.numVisibleLayers++] = layerIndex;
            }
        }
    }

    /** @private */
    _createDummyEntityForUnusedMeshes() {
        const unusedMeshIds = Object.keys(this._unusedMeshes);
        if (unusedMeshIds.length > 0) {
            const entityId = `${this.id}-${math.createUUID()}`;
            this.warn(`Creating dummy SceneModelEntity "${entityId}" for unused SceneMeshes: [${unusedMeshIds.join(",")}]`);
            this.createEntity({
                id: entityId,
                meshIds: unusedMeshIds,
                isObject: true
            });
        }
        this._unusedMeshes = {};
    }

    _getActiveSectionPlanesForLayer(layer) {
        const renderFlags = this.renderFlags;
        const sectionPlanes = this.scene._sectionPlanesState.sectionPlanes;
        const numSectionPlanes = sectionPlanes.length;
        const baseIndex = layer.layerIndex * numSectionPlanes;
        if (numSectionPlanes > 0) {
            for (let i = 0; i < numSectionPlanes; i++) {
                const sectionPlane = sectionPlanes[i];
                if (!sectionPlane.active) {
                    renderFlags.sectionPlanesActivePerLayer[baseIndex + i] = false;
                } else {
                    renderFlags.sectionPlanesActivePerLayer[baseIndex + i] = true;
                    renderFlags.sectioned = true;
                }
            }
        }
        return true;
    }

    _updateRenderFlags() {
        if (this.numVisibleLayerPortions === 0) {
            return;
        }
        if (this.numCulledLayerPortions === this.numPortions) {
            return;
        }
        const renderFlags = this.renderFlags;
        renderFlags.colorOpaque = (this.numTransparentLayerPortions < this.numPortions);
        if (this.numTransparentLayerPortions > 0) {
            renderFlags.colorTransparent = true;
        }
        if (this.numXRayedLayerPortions > 0) {
            const xrayMaterial = this.scene.xrayMaterial._state;
            if (xrayMaterial.fill) {
                if (xrayMaterial.fillAlpha < 1.0) {
                    renderFlags.xrayedSilhouetteTransparent = true;
                } else {
                    renderFlags.xrayedSilhouetteOpaque = true;
                }
            }
            if (xrayMaterial.edges) {
                if (xrayMaterial.edgeAlpha < 1.0) {
                    renderFlags.xrayedEdgesTransparent = true;
                } else {
                    renderFlags.xrayedEdgesOpaque = true;
                }
            }
        }
        if (this.numEdgesLayerPortions > 0) {
            const edgeMaterial = this.scene.edgeMaterial._state;
            if (edgeMaterial.edges) {
                renderFlags.edgesOpaque = (this.numTransparentLayerPortions < this.numPortions);
                if (this.numTransparentLayerPortions > 0) {
                    renderFlags.edgesTransparent = true;
                }
            }
        }
        if (this.numSelectedLayerPortions > 0) {
            const selectedMaterial = this.scene.selectedMaterial._state;
            if (selectedMaterial.fill) {
                if (selectedMaterial.fillAlpha < 1.0) {
                    renderFlags.selectedSilhouetteTransparent = true;
                } else {
                    renderFlags.selectedSilhouetteOpaque = true;
                }
            }
            if (selectedMaterial.edges) {
                if (selectedMaterial.edgeAlpha < 1.0) {
                    renderFlags.selectedEdgesTransparent = true;
                } else {
                    renderFlags.selectedEdgesOpaque = true;
                }
            }
        }
        if (this.numHighlightedLayerPortions > 0) {
            const highlightMaterial = this.scene.highlightMaterial._state;
            if (highlightMaterial.fill) {
                if (highlightMaterial.fillAlpha < 1.0) {
                    renderFlags.highlightedSilhouetteTransparent = true;
                } else {
                    renderFlags.highlightedSilhouetteOpaque = true;
                }
            }
            if (highlightMaterial.edges) {
                if (highlightMaterial.edgeAlpha < 1.0) {
                    renderFlags.highlightedEdgesTransparent = true;
                } else {
                    renderFlags.highlightedEdgesOpaque = true;
                }
            }
        }
    }

    // -------------- RENDERING ---------------------------------------------------------------------------------------

    /** @private */
    drawColorOpaque(frameCtx, layerList) {
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            this.layerList[layerIndex].drawColorOpaque(renderFlags, frameCtx);
        }
    }

    /** @private */
    drawColorTransparent(frameCtx) {
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            this.layerList[layerIndex].drawColorTransparent(renderFlags, frameCtx);
        }
    }

    /** @private */
    drawDepth(frameCtx) { // Dedicated to SAO because it skips transparent objects
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            this.layerList[layerIndex].drawDepth(renderFlags, frameCtx);
        }
    }

    /** @private */
    drawNormals(frameCtx) { // Dedicated to SAO because it skips transparent objects
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            this.layerList[layerIndex].drawNormals(renderFlags, frameCtx);
        }
    }

    /** @private */
    drawSilhouetteXRayed(frameCtx) {
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            this.layerList[layerIndex].drawSilhouetteXRayed(renderFlags, frameCtx);
        }
    }

    /** @private */
    drawSilhouetteHighlighted(frameCtx) {
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            this.layerList[layerIndex].drawSilhouetteHighlighted(renderFlags, frameCtx);
        }
    }

    /** @private */
    drawSilhouetteSelected(frameCtx) {
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            this.layerList[layerIndex].drawSilhouetteSelected(renderFlags, frameCtx);
        }
    }

    /** @private */
    drawEdgesColorOpaque(frameCtx) {
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            this.layerList[layerIndex].drawEdgesColorOpaque(renderFlags, frameCtx);
        }
    }

    /** @private */
    drawEdgesColorTransparent(frameCtx) {
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            this.layerList[layerIndex].drawEdgesColorTransparent(renderFlags, frameCtx);
        }
    }

    /** @private */
    drawEdgesXRayed(frameCtx) {
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            this.layerList[layerIndex].drawEdgesXRayed(renderFlags, frameCtx);
        }
    }

    /** @private */
    drawEdgesHighlighted(frameCtx) {
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            this.layerList[layerIndex].drawEdgesHighlighted(renderFlags, frameCtx);
        }
    }

    /** @private */
    drawEdgesSelected(frameCtx) {
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            this.layerList[layerIndex].drawEdgesSelected(renderFlags, frameCtx);
        }
    }

    /**
     * @private
     */
    drawOcclusion(frameCtx) {
        if (this.numVisibleLayerPortions === 0) {
            return;
        }
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            this.layerList[layerIndex].drawOcclusion(renderFlags, frameCtx);
        }
    }

    /**
     * @private
     */
    drawShadow(frameCtx) {
        if (this.numVisibleLayerPortions === 0) {
            return;
        }
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            this.layerList[layerIndex].drawShadow(renderFlags, frameCtx);
        }
    }

    /** @private */
    setPickMatrices(pickViewMatrix, pickProjMatrix) {
        if (this._numVisibleLayerPortions === 0) {
            return;
        }
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            const layer = this.layerList[layerIndex];
            if (layer.setPickMatrices) {
                layer.setPickMatrices(pickViewMatrix, pickProjMatrix);
            }
        }
    }

    /** @private */
    drawPickMesh(frameCtx) {
        if (this.numVisibleLayerPortions === 0) {
            return;
        }
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            this.layerList[layerIndex].drawPickMesh(renderFlags, frameCtx);
        }
    }

    /**
     * Called by SceneModelMesh.drawPickDepths()
     * @private
     */
    drawPickDepths(frameCtx) {
        if (this.numVisibleLayerPortions === 0) {
            return;
        }
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            this.layerList[layerIndex].drawPickDepths(renderFlags, frameCtx);
        }
    }

    /**
     * Called by SceneModelMesh.drawPickNormals()
     * @private
     */
    drawPickNormals(frameCtx) {
        if (this.numVisibleLayerPortions === 0) {
            return;
        }
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            this.layerList[layerIndex].drawPickNormals(renderFlags, frameCtx);
        }
    }

    /**
     * @private
     */
    drawSnapInit(frameCtx) {
        if (this.numVisibleLayerPortions === 0) {
            return;
        }
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            const layer = this.layerList[layerIndex];
            if (layer.drawSnapInit) {
                frameCtx.snapPickOrigin = [0, 0, 0];
                frameCtx.snapPickCoordinateScale = [1, 1, 1];
                frameCtx.snapPickLayerNumber++;
                layer.drawSnapInit(renderFlags, frameCtx);
                frameCtx.snapPickLayerParams[frameCtx.snapPickLayerNumber] = {
                    origin: frameCtx.snapPickOrigin.slice(),
                    coordinateScale: frameCtx.snapPickCoordinateScale.slice(),
                };
            }
        }
    }

    /**
     * @private
     */
    drawSnap(frameCtx) {
        if (this.numVisibleLayerPortions === 0) {
            return;
        }
        const renderFlags = this.renderFlags;
        for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) {
            const layerIndex = renderFlags.visibleLayers[i];
            const layer = this.layerList[layerIndex];
            if (layer.drawSnap) {
                frameCtx.snapPickOrigin = [0, 0, 0];
                frameCtx.snapPickCoordinateScale = [1, 1, 1];
                frameCtx.snapPickLayerNumber++;
                layer.drawSnap(renderFlags, frameCtx);
                frameCtx.snapPickLayerParams[frameCtx.snapPickLayerNumber] = {
                    origin: frameCtx.snapPickOrigin.slice(),
                    coordinateScale: frameCtx.snapPickCoordinateScale.slice(),
                };
            }
        }
    }

    /**
     * Destroys this SceneModel.
     */
    destroy() {
        for (let layerId in this._vboBatchingLayers) {
            if (this._vboBatchingLayers.hasOwnProperty(layerId)) {
                this._vboBatchingLayers[layerId].destroy();
            }
        }
        this._vboBatchingLayers = {};
        for (let layerId in this._vboInstancingLayers) {
            if (this._vboInstancingLayers.hasOwnProperty(layerId)) {
                this._vboInstancingLayers[layerId].destroy();
            }
        }
        this._vboInstancingLayers = {};
        this.scene.camera.off(this._onCameraViewMatrix);
        this.scene.off(this._onTick);
        for (let i = 0, len = this.layerList.length; i < len; i++) {
            this.layerList[i].destroy();
        }
        this.layerList = [];
        for (let i = 0, len = this._entityList.length; i < len; i++) {
            this._entityList[i]._destroy();
        }
        this._layersToFinalize = {};
        // Object.entries(this._geometries).forEach(([id, geometry]) => {
        //     geometry.destroy();
        // });
        this._geometries = {};
        this._dtxBuckets = {};
        this._textures = {};
        this._textureSets = {};
        this._meshes = {};
        this._entities = {};
        this.scene._aabbDirty = true;
        if (this._isModel) {
            this.scene._deregisterModel(this);
        }
        putScratchMemory();
        super.destroy();
    }
}


/**
 * This function applies two steps to the provided mesh geometry data:
 *
 * - 1st, it reduces its `.positions` to unique positions, thus removing duplicate vertices. It will adjust the `.indices` and `.edgeIndices` array accordingly to the unique `.positions`.
 *
 * - 2nd, it tries to do an optimization called `index rebucketting`
 *
 *   _Rebucketting minimizes the amount of RAM usage for a given mesh geometry by trying do demote its needed index bitness._
 *
 *   - _for 32 bit indices, will try to demote them to 16 bit indices_
 *   - _for 16 bit indices, will try to demote them to 8 bits indices_
 *   - _8 bits indices are kept as-is_
 *
 *   The fact that 32/16/8 bits are needed for indices, depends on the number of maximumm indexable vertices within the mesh geometry: this is, the number of vertices in the mesh geometry.
 *
 * The function returns the same provided input `geometry`, enrichened with the additional key `.preparedBukets`.
 *
 * @param {object} geometry The mesh information containing `.positions`, `.indices`, `.edgeIndices` arrays.
 *
 * @param enableVertexWelding
 * @param enableIndexBucketing
 * @returns {object} The mesh information enrichened with `.buckets` key.
 */
function createDTXBuckets(geometry, enableVertexWelding, enableIndexBucketing) {
    let uniquePositionsCompressed, uniqueIndices, uniqueEdgeIndices;
    if (enableVertexWelding || enableIndexBucketing) { // Expensive - careful!
        [
            uniquePositionsCompressed,
            uniqueIndices,
            uniqueEdgeIndices,
        ] = uniquifyPositions({
            positionsCompressed: geometry.positionsCompressed,
            indices: geometry.indices,
            edgeIndices: geometry.edgeIndices
        });
    } else {
        uniquePositionsCompressed = geometry.positionsCompressed;
        uniqueIndices = geometry.indices;
        uniqueEdgeIndices = geometry.edgeIndices;
    }
    let buckets;
    if (enableIndexBucketing) {
        let numUniquePositions = uniquePositionsCompressed.length / 3;
        buckets = rebucketPositions({
                positionsCompressed: uniquePositionsCompressed,
                indices: uniqueIndices,
                edgeIndices: uniqueEdgeIndices,
            },
            (numUniquePositions > (1 << 16)) ? 16 : 8,
            // true
        );
    } else {
        buckets = [{
            positionsCompressed: uniquePositionsCompressed,
            indices: uniqueIndices,
            edgeIndices: uniqueEdgeIndices,
        }];
    }
    return buckets;
}

/**
 * A set of 3D line segments.
 *
 * * Creates a set of 3D line segments.
 * * Registered by {@link LineSet#id} in {@link Scene#lineSets}.
 * * Configure color using the {@link LinesMaterial} located at {@link Scene#linesMaterial}.
 * * {@link BCFViewpointsPlugin} will save and load Linesets in BCF viewpoints.
 *
 * ## Usage
 *
 * In the example below, we'll load the Schependomlaan model, then use
 * a ````LineSet```` to show a grid underneath the model.
 *
 * [<img src="http://xeokit.github.io/xeokit-sdk/assets/images/LineSet_grid.png">](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#LineSet_grid)
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#LineSet_grid)]
 *
 * ````javascript
 * import {Viewer, XKTLoaderPlugin, LineSet, buildGridGeometry} from "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/xeokit-sdk.es.min.js";
 *
 * const viewer = new Viewer({
 *      canvasId: "myCanvas",
 *      transparent: true
 *  });
 *
 * const camera = viewer.camera;
 *
 * viewer.camera.eye = [-2.56, 8.38, 8.27];
 * viewer.camera.look = [13.44, 3.31, -14.83];
 * viewer.camera.up = [0.10, 0.98, -0.14];
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const model = xktLoader.load({
 *      id: "myModel",
 *      src: "../assets/models/xkt/v8/ifc/Schependomlaan.ifc.xkt",
 *      position: [0,1,0],
 *      edges: true,
 *      saoEnabled: true
 *  });
 *
 * const geometryArrays = buildGridGeometry({
 *      size: 100,
 *      divisions: 30
 *  });
 *
 * new LineSet(viewer.scene, {
 *      positions: geometryArrays.positions,
 *      indices: geometryArrays.indices
 * });
 * ````
 */
class LineSet extends Component {

    /**
     * Creates a new LineSet.
     *
     * Registers the LineSet in {@link Scene#lineSets}; causes Scene to fire a "lineSetCreated" event.
     *
     * @constructor
     * @param {Component} [owner]  Owner component. When destroyed, the owner will destroy this ````LineSet```` as well.
     * @param {*} [cfg]  ````LineSet```` configuration
     * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted.
     * @param {Number[]} cfg.positions World-space 3D vertex positions.
     * @param {Number[]} [cfg.indices] Indices to connect ````positions```` into line segments. Note that these are separate line segments, not a polyline.
     * @param {Number[]} [cfg.color=[1,1,1]] The color of this ````LineSet````. This is both emissive and diffuse.
     * @param {Boolean} [cfg.visible=true] Indicates whether or not this ````LineSet```` is visible.
     * @param {Number} [cfg.opacity=1.0] ````LineSet````'s initial opacity factor.
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._positions = cfg.positions || [];

        if (cfg.indices) {
            this._indices = cfg.indices;
        } else {
            this._indices = [];
            for (let i = 0, len = (this._positions.length / 3) - 1; i < len; i += 2) {
                this._indices.push(i);
                this._indices.push(i + 1);
            }
        }

        this._color = cfg.color || [1, 1, 1];
        this._opacity = cfg.opacity || 1.0;

        this._sceneModel = new SceneModel(this, {
            isModel: false // Don't register in Scene.models
        });

        this._sceneModel.createMesh({
            id: "linesMesh",
            primitive: "lines",
            positions: this._positions,
            indices: this._indices,
            color: this._color,
            opacity: this._opacity,
        });

        this._sceneModel.createEntity({
            meshIds: ["linesMesh"],
            visible: cfg.visible,
            clippable: cfg.clippable,
               collidable: cfg.collidable
        });

        this._sceneModel.finalize();

        this.scene._lineSetCreated(this);
    }

    /**
     * Sets if this ````LineSet```` is visible.
     *
     * Default value is ````true````.
     *
     * @param {Boolean} visible Set ````true```` to make this ````LineSet```` visible.
     */
    set visible(visible) {
        this._sceneModel.visible = visible;
    }

    /**
     * Gets if this ````LineSet```` is visible.
     *
     * Default value is ````true````.
     *
     * @returns {Boolean} Returns ````true```` if visible.
     */
    get visible() {
        return this._sceneModel.visible;
    }

    /**
     * Gets the 3D World-space vertex positions of the lines in this ````LineSet````.
     *
     * @returns {Number[]}
     */
    get positions() {
        return this._positions;
    }

    /**
     * Gets the vertex indices of the lines in this ````LineSet````.
     *
     * @returns {Number[]}
     */
    get indices() {
        return this._indices;
    }

    /**
     * Destroys this ````LineSet````.
     *
     * Removes the ```LineSet```` from {@link Scene#lineSets}; causes Scene to fire a "lineSetDestroyed" event.
     */
    destroy() {
        super.destroy(); // destroyes _sceneModel
        this.scene._lineSetDestroyed(this);
    }
}

/**
 * @desc A dynamic light source within a {@link Scene}.
 *
 * These are registered by {@link Light#id} in {@link Scene#lights}.
 */
class Light extends Component {

    /**
     @private
     */
    get type() {
        return "Light";
    }

    /**
     * @private
     */
    get isLight() {
        return true;
    }

    constructor(owner, cfg = {}) {
        super(owner, cfg);
    }
}

/**
 * @desc An ambient light source of fixed color and intensity that illuminates all {@link Mesh}es equally.
 *
 * * {@link AmbientLight#color} multiplies by {@link PhongMaterial#ambient} at each position of each {@link ReadableGeometry} surface.
 * * {@link AmbientLight#color} multiplies by {@link LambertMaterial#color} uniformly across each triangle of each {@link ReadableGeometry} (ie. flat shaded).
 * * {@link AmbientLight}s, {@link DirLight}s and {@link PointLight}s are registered by their {@link Component#id} on {@link Scene#lights}.
 *
 * ## Usage
 *
 * In the example below we'll destroy the {@link Scene}'s default light sources then create an AmbientLight and a couple of {@link @DirLight}s:
 *
 * [[Run this example](/examples/index.html#lights_AmbientLight)]
 *
 * ````javascript
 * import {Viewer, Mesh, buildTorusGeometry,
 * ReadableGeometry, PhongMaterial, Texture, AmbientLight} from "xeokit-sdk.es.js";
 *
 * // Create a Viewer and arrange the camera
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas"
 * });
 *
 * viewer.scene.camera.eye = [0, 0, 5];
 * viewer.scene.camera.look = [0, 0, 0];
 * viewer.scene.camera.up = [0, 1, 0];
 *
 * // Replace the Scene's default lights with a single custom AmbientLight
 *
 * viewer.scene.clearLights();
 *
 * new AmbientLight(viewer.scene, {
 *      color: [0.0, 0.3, 0.7],
 *      intensity: 1.0
 * });
 *
 * new DirLight(viewer.scene, {
 *      id: "keyLight",
 *      dir: [0.8, -0.6, -0.8],
 *      color: [1.0, 0.3, 0.3],
 *      intensity: 1.0,
 *      space: "view"
 * });
 *
 * new DirLight(viewer.scene, {
 *      id: "fillLight",
 *      dir: [-0.8, -0.4, -0.4],
 *      color: [0.3, 1.0, 0.3],
 *      intensity: 1.0,
 *      space: "view"
 * });
 *
 * new DirLight(viewer.scene, {
 *      id: "rimLight",
 *      dir: [0.2, -0.8, 0.8],
 *      color: [0.6, 0.6, 0.6],
 *      intensity: 1.0,
 *      space: "view"
 * });
 *
 * // Create a mesh with torus shape and PhongMaterial
 *
 * new Mesh(viewer.scene, {
 *      geometry: new ReadableGeometry(viewer.scene, buildSphereGeometry({
 *          center: [0, 0, 0],
 *          radius: 1.5,
 *          tube: 0.5,
 *          radialSegments: 32,
 *          tubeSegments: 24,
 *          arc: Math.PI * 2.0
 *      }),
 *      material: new PhongMaterial(viewer.scene, {
 *          ambient: [1.0, 1.0, 1.0],
 *          shininess: 30,
 *          diffuseMap: new Texture(viewer.scene, {
 *              src: "textures/diffuse/uvGrid2.jpg"
 *          })
 *      })
 * });
 *
 * // Adjust the color of our AmbientLight
 *
 * var ambientLight = viewer.scene.lights["myAmbientLight"];
 * ambientLight.color = [1.0, 0.8, 0.8];
 *````
 */
class AmbientLight extends Light {

    /**
     @private
     */
    get type() {
        return "AmbientLight";
    }

    /**
     * @param {Component} owner Owner component. When destroyed, the owner will destroy this AmbientLight as well.
     * @param {*} [cfg] AmbientLight configuration
     * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted.
     * @param {Number[]} [cfg.color=[0.7, 0.7, 0.8]]  The color of this AmbientLight.
     * @param {Number} [cfg.intensity=[1.0]]  The intensity of this AmbientLight, as a factor in range ````[0..1]````.
     */
    constructor(owner, cfg = {}) {
        super(owner, cfg);
        this._state = {
            type: "ambient",
            color: math.vec3([0.7, 0.7, 0.7]),
            intensity: 1.0
        };
        this.color = cfg.color;
        this.intensity = cfg.intensity;
        this.scene._lightCreated(this);
    }

    /**
     * Sets the RGB color of this AmbientLight.
     *
     * Default value is ````[0.7, 0.7, 0.8]````.
     *
     * @param {Number[]} color The AmbientLight's RGB color.
     */
    set color(color) {
        this._state.color.set(color || [0.7, 0.7, 0.8]);
        this.glRedraw();
    }

    /**
     * Gets the RGB color of this AmbientLight.
     *
     * Default value is ````[0.7, 0.7, 0.8]````.
     *
     * @returns {Number[]} The AmbientLight's RGB color.
     */
    get color() {
        return this._state.color;
    }

    /**
     * Sets the intensity of this AmbientLight.
     *
     * Default value is ````1.0```` for maximum intensity.
     *
     * @param {Number} intensity The AmbientLight's intensity.
     */
    set intensity(intensity) {
        this._state.intensity = intensity !== undefined ? intensity : 1.0;
        this.glRedraw();
    }

    /**
     * Gets the intensity of this AmbientLight.
     *
     * Default value is ````1.0```` for maximum intensity.
     *
     * @returns {Number} The AmbientLight's intensity.
     */
    get intensity() {
        return this._state.intensity;
    }

    /**
     * Destroys this AmbientLight.
     */
    destroy() {

        super.destroy();

        this.scene._lightDestroyed(this);
    }
}

/**
 * @desc Represents a WebGL render buffer.
 * @private
 */
class RenderBuffer {

    constructor(canvas, gl, options) {
        options = options || {};
        /** @type {WebGL2RenderingContext} */
        this.gl = gl;
        this.allocated = false;
        this.canvas = canvas;
        this.buffer = null;
        this.bound = false;
        this.size = options.size;
        this._hasDepthTexture = !!options.depthTexture;
    }

    setSize(size) {
        this.size = size;
    }

    webglContextRestored(gl) {
        this.gl = gl;
        this.buffer = null;
        this.allocated = false;
        this.bound = false;
    }

    bind(...internalformats) {
        this._touch(...internalformats);
        if (this.bound) {
            return;
        }
        const gl = this.gl;
        gl.bindFramebuffer(gl.FRAMEBUFFER, this.buffer.framebuf);
        this.bound = true;
    }

    /**
     * Create and specify a WebGL texture image.
     *
     * @param { number } width 
     * @param { number } height 
     * @param { GLenum } [internalformat=null] 
     *
     * @returns { WebGLTexture }
     */
    createTexture(width, height, internalformat = null) {
        const gl = this.gl;

        const colorTexture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, colorTexture);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

        if (internalformat) {
            gl.texStorage2D(gl.TEXTURE_2D, 1, internalformat, width, height);
        } else {
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
        }

        return colorTexture;
    }

    /**
     *
     * @param {number[]} [internalformats=[]]
     * @returns
     */
    _touch(...internalformats) {

        let width;
        let height;
        const gl = this.gl;

        if (this.size) {
            width = this.size[0];
            height = this.size[1];

        } else {
            width = gl.drawingBufferWidth;
            height = gl.drawingBufferHeight;
        }

        if (this.buffer) {

            if (this.buffer.width === width && this.buffer.height === height) {
                return;

            } else {
                this.buffer.textures.forEach(texture => gl.deleteTexture(texture));
                gl.deleteFramebuffer(this.buffer.framebuf);
                gl.deleteRenderbuffer(this.buffer.renderbuf);
            }
        }

        const colorTextures = [];
        if (internalformats.length > 0) {
            colorTextures.push(...internalformats.map(internalformat => this.createTexture(width, height, internalformat)));
        } else {
            colorTextures.push(this.createTexture(width, height));
        }

        let depthTexture;

        if (this._hasDepthTexture) {
            depthTexture = gl.createTexture();
            gl.bindTexture(gl.TEXTURE_2D, depthTexture);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT32F, width, height, 0, gl.DEPTH_COMPONENT, gl.FLOAT, null);
        }

        const renderbuf = gl.createRenderbuffer();
        gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuf);
        gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT32F, width, height);

        const framebuf = gl.createFramebuffer();
        gl.bindFramebuffer(gl.FRAMEBUFFER, framebuf);
        for (let i = 0; i < colorTextures.length; i++) {
            gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, colorTextures[i], 0);
        }
        if (internalformats.length > 0) {
            gl.drawBuffers(colorTextures.map((_, i) => gl.COLOR_ATTACHMENT0 + i));
        }

        if (this._hasDepthTexture) {
            gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0);
        } else {
            gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuf);
        }

        gl.bindTexture(gl.TEXTURE_2D, null);
        gl.bindRenderbuffer(gl.RENDERBUFFER, null);
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);

        // Verify framebuffer is OK

        gl.bindFramebuffer(gl.FRAMEBUFFER, framebuf);
        if (!gl.isFramebuffer(framebuf)) {
            throw "Invalid framebuffer";
        }
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);

        const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);

        switch (status) {

            case gl.FRAMEBUFFER_COMPLETE:
                break;

            case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
                throw "Incomplete framebuffer: FRAMEBUFFER_INCOMPLETE_ATTACHMENT";

            case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
                throw "Incomplete framebuffer: FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT";

            case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
                throw "Incomplete framebuffer: FRAMEBUFFER_INCOMPLETE_DIMENSIONS";

            case gl.FRAMEBUFFER_UNSUPPORTED:
                throw "Incomplete framebuffer: FRAMEBUFFER_UNSUPPORTED";

            default:
                throw "Incomplete framebuffer: " + status;
        }

        this.buffer = {
            framebuf: framebuf,
            renderbuf: renderbuf,
            texture: colorTextures[0],
            textures: colorTextures,
            depthTexture: depthTexture,
            width: width,
            height: height
        };

        this.bound = false;
    }

    clear() {
        if (!this.bound) {
            throw "Render buffer not bound";
        }
        const gl = this.gl;
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    }

    read(pickX, pickY, glFormat = null, glType = null, arrayType = Uint8Array, arrayMultiplier = 4, colorBufferIndex = 0) {
        const x = pickX;
        const y = this.buffer.height ? (this.buffer.height - pickY - 1) : (this.gl.drawingBufferHeight - pickY);
        const pix = new arrayType(arrayMultiplier);
        const gl = this.gl;
        gl.readBuffer(gl.COLOR_ATTACHMENT0 + colorBufferIndex);
        gl.readPixels(x, y, 1, 1, glFormat || gl.RGBA, glType || gl.UNSIGNED_BYTE, pix, 0);
        return pix;
    }

    readArray(glFormat = null, glType = null, arrayType = Uint8Array, arrayMultiplier = 4, colorBufferIndex = 0) {
        const pix = new arrayType(this.buffer.width*this.buffer.height * arrayMultiplier);
        const gl = this.gl;
        gl.readBuffer(gl.COLOR_ATTACHMENT0 + colorBufferIndex);
        gl.readPixels(0, 0, this.buffer.width, this.buffer.height, glFormat || gl.RGBA, glType || gl.UNSIGNED_BYTE, pix, 0);
        return pix;
    }

    /**
     * Returns an HTMLCanvas containing the contents of the RenderBuffer as an image.
     *
     * - The HTMLCanvas has a CanvasRenderingContext2D.
     * - Expects the caller to draw more things on the HTMLCanvas (annotations etc).
     *
     * @returns {HTMLCanvasElement}
     */
    readImageAsCanvas() {
        const gl = this.gl;
        const imageDataCache = this._getImageDataCache();
        const pixelData = imageDataCache.pixelData;
        const canvas = imageDataCache.canvas;
        const imageData = imageDataCache.imageData;
        const context = imageDataCache.context;
        gl.readPixels(0, 0, this.buffer.width, this.buffer.height, gl.RGBA, gl.UNSIGNED_BYTE, pixelData);
        const width = this.buffer.width;
        const height = this.buffer.height;
        const halfHeight = height / 2 | 0;  // the | 0 keeps the result an int
        const bytesPerRow = width * 4;
        const temp = new Uint8Array(width * 4);
        for (let y = 0; y < halfHeight; ++y) {
            const topOffset = y * bytesPerRow;
            const bottomOffset = (height - y - 1) * bytesPerRow;
            temp.set(pixelData.subarray(topOffset, topOffset + bytesPerRow));
            pixelData.copyWithin(topOffset, bottomOffset, bottomOffset + bytesPerRow);
            pixelData.set(temp, bottomOffset);
        }
        imageData.data.set(pixelData);
        context.putImageData(imageData, 0, 0);
        return canvas;
    }

    readImage(params) {
        const gl = this.gl;
        const imageDataCache = this._getImageDataCache();
        const pixelData = imageDataCache.pixelData;
        const canvas = imageDataCache.canvas;
        const imageData = imageDataCache.imageData;
        const context = imageDataCache.context;
        const { width, height } = this.buffer;
        gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixelData);
        imageData.data.set(pixelData);
        context.putImageData(imageData, 0, 0);

        // flip Y
        context.save();
        context.globalCompositeOperation = 'copy';
        context.scale(1, -1);
        context.drawImage(canvas, 0, -height, width, height);
        context.restore();

        let format = params.format || "png";
        if (format !== "jpeg" && format !== "png" && format !== "bmp") {
            console.error("Unsupported image format: '" + format + "' - supported types are 'jpeg', 'bmp' and 'png' - defaulting to 'png'");
            format = "png";
        }
        return canvas.toDataURL(`image/${format}`);
    }

    _getImageDataCache(type = Uint8Array, multiplier = 4) {

        const bufferWidth = this.buffer.width;
        const bufferHeight = this.buffer.height;

        let imageDataCache = this._imageDataCache;

        if (imageDataCache) {
            if (imageDataCache.width !== bufferWidth || imageDataCache.height !== bufferHeight) {
                this._imageDataCache = null;
                imageDataCache = null;
            }
        }

        if (!imageDataCache) {
            const canvas = document.createElement('canvas');
            const context = canvas.getContext('2d');
            canvas.width = bufferWidth;
            canvas.height = bufferHeight;
            imageDataCache = {
                pixelData: new type(bufferWidth * bufferHeight * multiplier),
                canvas: canvas,
                context: context,
                imageData: context.createImageData(bufferWidth, bufferHeight),
                width: bufferWidth,
                height: bufferHeight
            };

            this._imageDataCache = imageDataCache;
        }
        imageDataCache.context.resetTransform(); // Prevents strange scale-accumulation effect with html2canvas
        return imageDataCache;
    }

    unbind() {
        const gl = this.gl;
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
        this.bound = false;
    }

    getTexture(index = 0) {
        const self = this;
        return this._texture || (this._texture = {
            renderBuffer: this,
            bind: function (unit) {
                if (self.buffer && self.buffer.textures[index]) {
                    self.gl.activeTexture(self.gl["TEXTURE" + unit]);
                    self.gl.bindTexture(self.gl.TEXTURE_2D, self.buffer.textures[index]);
                    return true;
                }
                return false;
            },
            unbind: function (unit) {
                if (self.buffer && self.buffer.textures[index]) {
                    self.gl.activeTexture(self.gl["TEXTURE" + unit]);
                    self.gl.bindTexture(self.gl.TEXTURE_2D, null);
                }
            }
        });
    }

    hasDepthTexture() {
        return this._hasDepthTexture;
    }

    getDepthTexture() {
        if (!this._hasDepthTexture) {
            return null;
        }
        const self = this;
        return this._depthTexture || (this._dethTexture = {
            renderBuffer: this,
            bind: function (unit) {
                if (self.buffer && self.buffer.depthTexture) {
                    self.gl.activeTexture(self.gl["TEXTURE" + unit]);
                    self.gl.bindTexture(self.gl.TEXTURE_2D, self.buffer.depthTexture);
                    return true;
                }
                return false;
            },
            unbind: function (unit) {
                if (self.buffer && self.buffer.depthTexture) {
                    self.gl.activeTexture(self.gl["TEXTURE" + unit]);
                    self.gl.bindTexture(self.gl.TEXTURE_2D, null);
                }
            }
        });
    }

    destroy() {
        if (this.allocated) {
            const gl = this.gl;
            this.buffer.textures.forEach(texture => gl.deleteTexture(texture));
            gl.deleteTexture(this.buffer.depthTexture);
            gl.deleteFramebuffer(this.buffer.framebuf);
            gl.deleteRenderbuffer(this.buffer.renderbuf);
            this.allocated = false;
            this.buffer = null;
            this.bound = false;
        }
        this._imageDataCache = null;
        this._texture = null;
        this._depthTexture = null;
    }
}

/**
 * @desc A directional light source that illuminates all {@link Mesh}es equally from a given direction.
 *
 * * Has an emission direction vector in {@link DirLight#dir}, but no position.
 * * Defined in either *World* or *View* coordinate space. When in World-space, {@link DirLight#dir} is relative to the
 * World coordinate system, and will appear to move as the {@link Camera} moves. When in View-space, {@link DirLight#dir} is
 * relative to the View coordinate system, and will behave as if fixed to the viewer's head.
 * * {@link AmbientLight}s, {@link DirLight}s and {@link PointLight}s are registered by their {@link Component#id} on {@link Scene#lights}.
 *
 * ## Usage
 *
 * In the example below we'll replace the {@link Scene}'s default light sources with three View-space DirLights.
 *
 * [[Run this example](/examples/index.html#lights_DirLight_view)]
 *
 * ````javascript
 * import {Viewer, Mesh, buildSphereGeometry,
 *      buildPlaneGeometry, ReadableGeometry,
 *      PhongMaterial, Texture, DirLight} from "xeokit-sdk.es.js";
 *
 * // Create a Viewer and arrange the camera
 *
 * const viewer = new Viewer({
 *      canvasId: "myCanvas"
 * });
 *
 * viewer.scene.camera.eye = [0, 0, 5];
 * viewer.scene.camera.look = [0, 0, 0];
 * viewer.scene.camera.up = [0, 1, 0];
 *
 * // Replace the Scene's default lights with three custom view-space DirLights
 *
 * viewer.scene.clearLights();
 *
 * new DirLight(viewer.scene, {
 *      id: "keyLight",
 *      dir: [0.8, -0.6, -0.8],
 *      color: [1.0, 0.3, 0.3],
 *      intensity: 1.0,
 *      space: "view"
 * });
 *
 * new DirLight(viewer.scene, {
 *      id: "fillLight",
 *      dir: [-0.8, -0.4, -0.4],
 *      color: [0.3, 1.0, 0.3],
 *      intensity: 1.0,
 *      space: "view"
 * });
 *
 * new DirLight(viewer.scene, {
 *      id: "rimLight",
 *      dir: [0.2, -0.8, 0.8],
 *      color: [0.6, 0.6, 0.6],
 *      intensity: 1.0,
 *      space: "view"
 * });
 *
 *
 * // Create a sphere and ground plane
 *
 * new Mesh(viewer.scene, {
 *      geometry: new ReadableGeometry(viewer.scene, buildSphereGeometry({
 *          radius: 2.0
 *      }),
 *      material: new PhongMaterial(viewer.scene, {
 *          diffuse: [0.7, 0.7, 0.7],
 *          specular: [1.0, 1.0, 1.0],
 *          emissive: [0, 0, 0],
 *          alpha: 1.0,
 *          ambient: [1, 1, 0],
 *          diffuseMap: new Texture(viewer.scene, {
 *              src: "textures/diffuse/uvGrid2.jpg"
 *          })
 *      })
 *  });
 *
 * new Mesh(viewer.scene, {
 *      geometry: buildPlaneGeometry(ReadableGeometry, viewer.scene, {
 *          xSize: 30,
 *          zSize: 30
 *      }),
 *      material: new PhongMaterial(viewer.scene, {
 *          diffuseMap: new Texture(viewer.scene, {
 *              src: "textures/diffuse/uvGrid2.jpg"
 *          }),
 *          backfaces: true
 *      }),
 *      position: [0, -2.1, 0]
 * });
 * ````
 */
class DirLight extends Light {

    /**
     @private
     */
    get type() {
        return "DirLight";
    }

    /**
     * @param {Component} owner Owner component. When destroyed, the owner will destroy this DirLight as well.
     * @param {*} [cfg] The DirLight configuration
     * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted.
     * @param {Number[]} [cfg.dir=[1.0, 1.0, 1.0]]  A unit vector indicating the direction that the light is shining,  given in either World or View space, depending on the value of the ````space```` parameter.
     * @param {Number[]} [cfg.color=[0.7, 0.7, 0.8 ]] The color of this DirLight.
     * @param {Number} [cfg.intensity=1.0] The intensity of this DirLight, as a factor in range ````[0..1]````.
     * @param {String} [cfg.space="view"] The coordinate system the DirLight is defined in - ````"view"```` or ````"space"````.
     * @param {Boolean} [cfg.castsShadow=false] Flag which indicates if this DirLight casts a castsShadow.
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._shadowRenderBuf = null;
        this._shadowViewMatrix = null;
        this._shadowProjMatrix = null;
        this._shadowViewMatrixDirty = true;
        this._shadowProjMatrixDirty = true;

        const camera = this.scene.camera;
        const canvas = this.scene.canvas;

        this._onCameraViewMatrix = camera.on("viewMatrix", () => {
            this._shadowViewMatrixDirty = true;
        });

        this._onCameraProjMatrix = camera.on("projMatrix", () => {
            this._shadowProjMatrixDirty = true;
        });

        this._onCanvasBoundary = canvas.on("boundary", () => {
            this._shadowProjMatrixDirty = true;
        });

        this._state = new RenderState({

            type: "dir",
            dir: math.vec3([1.0, 1.0, 1.0]),
            color: math.vec3([0.7, 0.7, 0.8]),
            intensity: 1.0,
            space: cfg.space || "view",
            castsShadow: false,

            getShadowViewMatrix: () => {
                if (this._shadowViewMatrixDirty) {
                    if (!this._shadowViewMatrix) {
                        this._shadowViewMatrix = math.identityMat4();
                    }
                    const camera = this.scene.camera;
                    const dir = this._state.dir;
                    const look = camera.look;
                    const eye = [look[0] - dir[0], look[1] - dir[1], look[2] - dir[2]];
                    const up = [0, 1, 0];
                    math.lookAtMat4v(eye, look, up, this._shadowViewMatrix);
                    this._shadowViewMatrixDirty = false;
                }
                return this._shadowViewMatrix;
            },

            getShadowProjMatrix: () => {
                if (this._shadowProjMatrixDirty) { // TODO: Set when canvas resizes
                    if (!this._shadowProjMatrix) {
                        this._shadowProjMatrix = math.identityMat4();
                    }
                    math.orthoMat4c(-40, 40, -40, 40, -40, 80, this._shadowProjMatrix);  // left, right, bottom, top, near, far, dest
                    this._shadowProjMatrixDirty = false;
                }
                return this._shadowProjMatrix;
            },

            getShadowRenderBuf: () => {
                if (!this._shadowRenderBuf) {
                    this._shadowRenderBuf = new RenderBuffer(this.scene.canvas.canvas, this.scene.canvas.gl, {size: [1024, 1024]}); // Super old mobile devices have a limit of 1024x1024 textures
                }
                return this._shadowRenderBuf;
            }
        });

        this.dir = cfg.dir;
        this.color = cfg.color;
        this.intensity = cfg.intensity;
        this.castsShadow = cfg.castsShadow;

        this.scene._lightCreated(this);
    }

    /**
     * Sets the direction in which the DirLight is shining.
     *
     * Default value is ````[1.0, 1.0, 1.0]````.
     *
     * @param {Number[]} value The direction vector.
     */
    set dir(value) {
        this._state.dir.set(value || [1.0, 1.0, 1.0]);
        this._shadowViewMatrixDirty = true;
        this.glRedraw();
    }

    /**
     * Gets the direction in which the DirLight is shining.
     *
     * Default value is ````[1.0, 1.0, 1.0]````.
     *
     * @returns {Number[]} The direction vector.
     */
    get dir() {
        return this._state.dir;
    }

    /**
     * Sets the RGB color of this DirLight.
     *
     * Default value is ````[0.7, 0.7, 0.8]````.
     *
     * @param {Number[]} color The DirLight's RGB color.
     */
    set color(color) {
        this._state.color.set(color || [0.7, 0.7, 0.8]);
        this.glRedraw();
    }

    /**
     * Gets the RGB color of this DirLight.
     *
     * Default value is ````[0.7, 0.7, 0.8]````.
     *
     * @returns {Number[]} The DirLight's RGB color.
     */
    get color() {
        return this._state.color;
    }

    /**
     * Sets the intensity of this DirLight.
     *
     * Default intensity is ````1.0```` for maximum intensity.
     *
     * @param {Number} intensity The DirLight's intensity
     */
    set intensity(intensity) {
        intensity = intensity !== undefined ? intensity : 1.0;
        this._state.intensity = intensity;
        this.glRedraw();
    }

    /**
     * Gets the intensity of this DirLight.
     *
     * Default value is ````1.0```` for maximum intensity.
     *
     * @returns {Number} The DirLight's intensity.
     */
    get intensity() {
        return this._state.intensity;
    }

    /**
     * Sets if this DirLight casts a shadow.
     *
     * Default value is ````false````.
     *
     * @param {Boolean} castsShadow Set ````true```` to cast shadows.
     */
    set castsShadow(castsShadow) {
        castsShadow = !!castsShadow;
        if (this._state.castsShadow === castsShadow) {
            return;
        }
        this._state.castsShadow = castsShadow;
        this._shadowViewMatrixDirty = true;
        this.glRedraw();
    }

    /**
     * Gets if this DirLight casts a shadow.
     *
     * Default value is ````false````.
     *
     * @returns {Boolean} ````true```` if this DirLight casts shadows.
     */
    get castsShadow() {
        return this._state.castsShadow;
    }

    /**
     * Destroys this DirLight.
     */
    destroy() {

        const camera = this.scene.camera;
        const canvas = this.scene.canvas;
        camera.off(this._onCameraViewMatrix);
        camera.off(this._onCameraProjMatrix);
        canvas.off(this._onCanvasBoundary);

        super.destroy();
        this._state.destroy();
        if (this._shadowRenderBuf) {
            this._shadowRenderBuf.destroy();
        }
        this.scene._lightDestroyed(this);
        this.glRedraw();
    }
}

const tempVec4a$9 = math.vec4();
const tempVec4b$8 = math.vec4();


/**
 * @desc Tracks the World, View and Canvas coordinates, and visibility, of a position within a {@link Scene}.
 *
 * ## Position
 *
 * A Marker holds its position in the World, View and Canvas coordinate systems in three properties:
 *
 * * {@link Marker#worldPos} holds the Marker's 3D World-space coordinates. This property can be dynamically updated. The Marker will fire a "worldPos" event whenever this property changes.
 * * {@link Marker#viewPos} holds the Marker's 3D View-space coordinates. This property is read-only, and is automatically updated from {@link Marker#worldPos} and the current {@link Camera} position. The Marker will fire a "viewPos" event whenever this property changes.
 * * {@link Marker#canvasPos} holds the Marker's 2D Canvas-space coordinates. This property is read-only, and is automatically updated from {@link Marker#canvasPos} and the current {@link Camera} position and projection. The Marker will fire a "canvasPos" event whenever this property changes.
 *
 * ## Visibility
 *
 * {@link Marker#visible} indicates if the Marker is currently visible. The Marker will fire a "visible" event whenever {@link Marker#visible} changes.
 *
 * This property will be ````false```` when:
 *
 * * {@link Marker#entity} is set to an {@link Entity}, and {@link Entity#visible} is ````false````,
 * * {@link Marker#occludable} is ````true```` and the Marker is occluded by some {@link Entity} in the 3D view, or
 * * {@link Marker#canvasPos} is outside the boundary of the {@link Canvas}.
 *
 * ## Usage
 *
 * In the example below, we'll create a Marker that's associated with a {@link Mesh} (which a type of {@link Entity}).
 *
 * We'll configure our Marker to
 * become invisible whenever it's occluded by any Entities in the canvas.
 *
 * We'll also demonstrate how to query the Marker's visibility status and position (in the World, View and
 * Canvas coordinate systems), and how to subscribe to change events on those properties.
 *
 * ````javascript
 * import {Viewer, GLTFLoaderPlugin, Marker} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas"
 * });
 *
 * // Create the torus Mesh
 * // Recall that a Mesh is an Entity
 * new Mesh(viewer.scene, {
 *     geometry: new ReadableGeometry(viewer.scene, buildTorusGeometry({
 *         center: [0,0,0],
 *         radius: 1.0,
 *         tube: 0.5,
 *         radialSegments: 32,
 *         tubeSegments: 24,
 *         arc: Math.PI * 2.0
 *     }),
 *     material: new PhongMaterial(viewer.scene, {
 *         diffuseMap: new Texture(viewer.scene, {
 *             src: "textures/diffuse/uvGrid2.jpg"
 *         }),
 *         backfaces: true
 *     })
 * });
 *
 * // Create the Marker, associated with our Mesh Entity
 * const myMarker = new Marker(viewer, {
 *      entity: entity,
 *      worldPos: [10,0,0],
 *      occludable: true
 * });
 *
 * // Get the Marker's current World, View and Canvas coordinates
 * const worldPos   = myMarker.worldPos;     // 3D World-space position
 * const viewPos    = myMarker.viewPos;      // 3D View-space position
 * const canvasPos  = myMarker.canvasPos;    // 2D Canvas-space position
 *
 * const visible = myMarker.visible;
 *
 * // Listen for change of the Marker's 3D World-space position
 * myMarker.on("worldPos", function(worldPos) {
 *    //...
 * });
 *
 * // Listen for change of the Marker's 3D View-space position, which happens
 * // when either worldPos was updated or the Camera was moved
 * myMarker.on("viewPos", function(viewPos) {
 *    //...
 * });
 *
 * // Listen for change of the Marker's 2D Canvas-space position, which happens
 * // when worldPos or viewPos was updated, or Camera's projection was updated
 * myMarker.on("canvasPos", function(canvasPos) {
 *    //...
 * });
 *
 * // Listen for change of Marker visibility. The Marker becomes invisible when it falls outside the canvas,
 * // has an Entity that is also invisible, or when an Entity occludes the Marker's position in the 3D view.
 * myMarker.on("visible", function(visible) { // Marker visibility has changed
 *    if (visible) {
 *        this.log("Marker is visible");
 *    } else {
 *        this.log("Marker is invisible");
 *    }
 * });
 *
 * // Listen for destruction of Marker
 * myMarker.on("destroyed", () => {
 *      //...
 * });
 * ````
 */
class Marker extends Component {

    /**
     * @constructor
     * @param {Component} [owner]  Owner component. When destroyed, the owner will destroy this Marker as well.
     * @param {*} [cfg]  Marker configuration
     * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted.
     * @param {Entity} [cfg.entity] Entity to associate this Marker with. When the Marker has an Entity, then {@link Marker#visible} will always be ````false```` if {@link Entity#visible} is false.
     * @param {Boolean} [cfg.occludable=false] Indicates whether or not this Marker is hidden (ie. {@link Marker#visible} is ````false```` whenever occluded by {@link Entity}s in the {@link Scene}.
     * @param {Number[]} [cfg.worldPos=[0,0,0]] World-space 3D Marker position.
     */
    constructor(owner, cfg) {

        super(owner, cfg);

        this._entity = null;
        this._visible = null;
        this._worldPos = math.vec3();
        this._origin = math.vec3();
        this._rtcPos = math.vec3();
        this._viewPos = math.vec3();
        this._canvasPos = math.vec2();
        this._occludable = false;

        this._onCameraViewMatrix = this.scene.camera.on("matrix", () => {
            this._viewPosDirty = true;
            this._needUpdate();
        });

        this._onCameraProjMatrix = this.scene.camera.on("projMatrix", () => {
            this._canvasPosDirty = true;
            this._needUpdate();
        });

        this._onEntityDestroyed = null;
        this._onEntityModelDestroyed = null;

        this._renderer.addMarker(this);

        this.entity = cfg.entity;
        this.worldPos = cfg.worldPos;
        this.occludable = cfg.occludable;
    }

    _update() { // this._needUpdate() schedules this for next tick
        if (this._viewPosDirty) {
            math.transformPoint3(this.scene.camera.viewMatrix, this._worldPos, this._viewPos);
            this._viewPosDirty = false;
            this._canvasPosDirty = true;
            this.fire("viewPos", this._viewPos);
        }
        if (this._canvasPosDirty) {
            tempVec4a$9.set(this._viewPos);
            tempVec4a$9[3] = 1.0;
            math.transformPoint4(this.scene.camera.projMatrix, tempVec4a$9, tempVec4b$8);
            const aabb = this.scene.canvas.boundary;
            this._canvasPos[0] = Math.floor((1 + tempVec4b$8[0] / tempVec4b$8[3]) * aabb[2] / 2);
            this._canvasPos[1] = Math.floor((1 - tempVec4b$8[1] / tempVec4b$8[3]) * aabb[3] / 2);
            this._canvasPosDirty = false;
            this.fire("canvasPos", this._canvasPos);
        }
    }

    _setVisible(visible) { // Called by VisibilityTester and this._entity.on("destroyed"..)
        if (this._visible === visible) ;
        this._visible = visible;
        this.fire("visible", this._visible);
    }

    /**
     * Sets the {@link Entity} this Marker is associated with.
     *
     * An Entity is optional. When the Marker has an Entity, then {@link Marker#visible} will always be ````false````
     * if {@link Entity#visible} is false.
     *
     * @type {Entity}
     */
    set entity(entity) {
        if (this._entity === entity) {
            return;
        }
        this._cleanupDestroyedHandlers();
        this._entity = entity;
        if (this._entity) {
            if (this._entity.isSceneModelEntity) {
                this._onEntityModelDestroyed = this._entity.model.on("destroyed", () => { // SceneModelEntity does not fire events, and cannot exist beyond its VBOSceneModel
                    this._entity = null; // Marker now may become visible, if it was synched to invisible Entity
                    this._onEntityModelDestroyed = null;
                });
            } else {
                this._onEntityDestroyed = this._entity.on("destroyed", () => {
                    this._entity = null;
                    this._onEntityDestroyed = null;
                });
            }
        }
        this.fire("entity", this._entity, true /* forget */);
    }

    /**
     * Gets the {@link Entity} this Marker is associated with.
     *
     * @type {Entity}
     */
    get entity() {
        return this._entity;
    }

    /**
     * Sets whether occlusion testing is performed for this Marker.
     *
     * When this is ````true````, then {@link Marker#visible} will be ````false```` whenever the Marker is occluded by an {@link Entity} in the 3D view.
     *
     * The {@link Scene} periodically occlusion-tests all Markers on every 20th "tick" (which represents a rendered frame). We
     * can adjust that frequency via property {@link Scene#ticksPerOcclusionTest}.
     *
     * @type {Boolean}
     */
    set occludable(occludable) {
        occludable = !!occludable;
        if (occludable === this._occludable) {
            return;
        }
        this._occludable = occludable;
        if (this._occludable) {
            this._renderer.markerWorldPosUpdated(this);
        }
    }

    /**
     * Gets whether occlusion testing is performed for this Marker.
     *
     * When this is ````true````, then {@link Marker#visible} will be ````false```` whenever the Marker is occluded by an {@link Entity} in the 3D view.
     *
     * @type {Boolean}
     */
    get occludable() {
        return this._occludable;
    }

    /**
     * Sets the World-space 3D position of this Marker.
     *
     * Fires a "worldPos" event with new World position.
     *
     * @type {Number[]}
     */
    set worldPos(worldPos) {
        this._worldPos.set(worldPos || [0, 0, 0]);
        worldToRTCPos(this._worldPos, this._origin, this._rtcPos);
        if (this._occludable) {
            this._renderer.markerWorldPosUpdated(this);
        }
        this._viewPosDirty = true;
        this.fire("worldPos", this._worldPos);
        this._needUpdate();
    }

    /**
     * Gets the World-space 3D position of this Marker.
     *
     * @type {Number[]}
     */
    get worldPos() {
        return this._worldPos;
    }

    /**
     * Gets the RTC center of this Marker.
     *
     * This is automatically calculated from {@link Marker#worldPos}.
     *
     * @type {Number[]}
     */
    get origin() {
        return this._origin;
    }

    /**
     * Gets the RTC position of this Marker.
     *
     * This is automatically calculated from {@link Marker#worldPos}.
     *
     * @type {Number[]}
     */
    get rtcPos() {
        return this._rtcPos;
    }

    /**
     * View-space 3D coordinates of this Marker.
     *
     * This property is read-only and is automatically calculated from {@link Marker#worldPos} and the current {@link Camera} position.
     *
     * The Marker fires a "viewPos" event whenever this property changes.
     *
     * @type {Number[]}
     * @final
     */
    get viewPos() {
        this._update();
        return this._viewPos;
    }

    /**
     * Canvas-space 2D coordinates of this Marker.
     *
     * This property is read-only and is automatically calculated from {@link Marker#worldPos} and the current {@link Camera} position and projection.
     *
     * The Marker fires a "canvasPos" event whenever this property changes.
     *
     * @type {Number[]}
     * @final
     */
    get canvasPos() {
        this._update();
        return this._canvasPos;
    }

    /**
     * Indicates if this Marker is currently visible.
     *
     * This is read-only and is automatically calculated.
     *
     * The Marker is **invisible** whenever:
     *
     * * {@link Marker#canvasPos} is currently outside the canvas,
     * * {@link Marker#entity} is set to an {@link Entity} that has {@link Entity#visible} ````false````, or
     * * or {@link Marker#occludable} is ````true```` and the Marker is currently occluded by an Entity in the 3D view.
     *
     * The Marker fires a "visible" event whenever this property changes.
     *
     * @type {Boolean}
     * @final
     */
    get visible() {
        return !!this._visible;
    }

    /**
     * Destroys this Marker.
     */
    destroy() {
        this.fire("destroyed", true);
        this.scene.camera.off(this._onCameraViewMatrix);
        this.scene.camera.off(this._onCameraProjMatrix);
        this._cleanupDestroyedHandlers();
        this._renderer.removeMarker(this);
        super.destroy();
    }

    _cleanupDestroyedHandlers() {
        if (this._entity) {
            if (this._onEntityDestroyed !== null) {
                if (this._entity.model) {
                    this._entity.model.off(this._onEntityDestroyed);
                } else {
                    this._entity.off(this._onEntityDestroyed);
                }
                this._onEntityDestroyed = null;
            }
            if (this._onEntityModelDestroyed !== null) {
                if (this._entity.model) {
                    this._entity.model.off(this._onEntityModelDestroyed);
                }
                this._onEntityModelDestroyed = null;
            }
        }
    }
}

const color$3 = math.vec3();

/**
 * @desc Saves and restores a snapshot of the visual state of the {@link Entity}'s of a model within a {@link Scene}.
 *
 * ## Usage
 *
 * In the example below, we'll create a {@link Viewer} and use an {@link XKTLoaderPlugin} to load an ````.xkt```` model. When the model has loaded, we'll hide a couple of {@link Entity}s and save a snapshot of the visual states of all its Entitys in an ModelMemento. Then we'll show all the Entitys
 * again, and then we'll restore the visual states of all the Entitys again from the ModelMemento, which will hide those two Entitys again.
 *
 * ## See Also
 *
 * * {@link CameraMemento} - Saves and restores the state of a {@link Scene}'s {@link Camera}.
 * * {@link ObjectsMemento} - Saves and restores a snapshot of the visual state of the {@link Entity}'s that represent objects within a {@link Scene}.
 *
 * ````javascript
 * import {Viewer, XKTLoaderPlugin,  ModelMemento} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas"
 * });
 *
 * // Load a model
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const model = xktLoader.load({
 *     id: "myModel",
 *     src: "./models/xkt/schependomlaan/schependomlaan.xkt"
 * });
 *
 * model.on("loaded", () => {
 *
 *      // Model has loaded
 *
 *      // Hide a couple of objects
 *      viewer.scene.objects["0u4wgLe6n0ABVaiXyikbkA"].visible = false;
 *      viewer.scene.objects["3u4wgLe3n0AXVaiXyikbYO"].visible = false;
 *
 *      // Save memento of all object states, which includes those two hidden objects
 *      const ModelMemento = new ModelMemento();
 *
 * const metaModel = viewer.metaScene.metaModels
 *      ModelMemento.saveObjects(viewer.scene);
 *
 *      // Show all objects
 *      viewer.scene.setObjectsVisible(viewer.scene.objectIds, true);
 *
 *      // Restore the objects states again, which involves hiding those two objects again
 *      ModelMemento.restoreObjects(viewer.scene);
 * });
 * `````
 *
 * ## Masking Saved State
 *
 * We can optionally supply a mask to focus what state we save and restore.
 *
 * For example, to save and restore only the {@link Entity#visible} and {@link Entity#clippable} states:
 *
 * ````javascript
 * ModelMemento.saveObjects(viewer.scene, {
 *     visible: true,
 *     clippable: true
 * });
 *
 * //...
 *
 * // Restore the objects states again
 * ModelMemento.restoreObjects(viewer.scene);
 * ````
 */
class ModelMemento {

    /**
     * Creates a ModelMemento.
     *
     * @param {MetaModel} [metaModel] When given, immediately saves the model's {@link Entity} states to this ModelMemento.
     */
    constructor(metaModel) {

        /** @private */
        this.objectsVisible = [];

        /** @private */
        this.objectsEdges = [];

        /** @private */
        this.objectsXrayed = [];

        /** @private */
        this.objectsHighlighted = [];

        /** @private */
        this.objectsSelected = [];

        /** @private */
        this.objectsClippable = [];

        /** @private */
        this.objectsPickable = [];

        /** @private */
        this.objectsColorize = [];

        /** @private */
        this.objectsOpacity = [];

        /** @private */
        this.numObjects = 0;

        if (metaModel) {
            const metaScene = metaModel.metaScene;
            const scene = metaScene.scene;
            this.saveObjects(scene, metaModel);
        }
    }

    /**
     * Saves a snapshot of the visual state of the {@link Entity}'s that represent objects within a model.
     *
     * @param {Scene} scene The scene.
     * @param {MetaModel} metaModel Represents the model. Corresponds with an {@link Entity} that represents the model in the scene.
     * @param {Object} [mask] Masks what state gets saved. Saves all state when not supplied.
     * @param {boolean} [mask.visible] Saves {@link Entity#visible} values when ````true````.
     * @param {boolean} [mask.visible] Saves {@link Entity#visible} values when ````true````.
     * @param {boolean} [mask.edges] Saves {@link Entity#edges} values when ````true````.
     * @param {boolean} [mask.xrayed] Saves {@link Entity#xrayed} values when ````true````.
     * @param {boolean} [mask.highlighted] Saves {@link Entity#highlighted} values when ````true````.
     * @param {boolean} [mask.selected] Saves {@link Entity#selected} values when ````true````.
     * @param {boolean} [mask.clippable] Saves {@link Entity#clippable} values when ````true````.
     * @param {boolean} [mask.pickable] Saves {@link Entity#pickable} values when ````true````.
     * @param {boolean} [mask.colorize] Saves {@link Entity#colorize} values when ````true````.
     * @param {boolean} [mask.opacity] Saves {@link Entity#opacity} values when ````true````.
     */
    saveObjects(scene, metaModel, mask) {

        this.numObjects = 0;

        this._mask = mask ? utils.apply(mask, {}) : null;

        const visible = (!mask || mask.visible);
        const edges = (!mask || mask.edges);
        const xrayed = (!mask || mask.xrayed);
        const highlighted = (!mask || mask.highlighted);
        const selected = (!mask || mask.selected);
        const clippable = (!mask || mask.clippable);
        const pickable = (!mask || mask.pickable);
        const colorize = (!mask || mask.colorize);
        const opacity = (!mask || mask.opacity);

        const metaObjects = metaModel.metaObjects;
        const objects = scene.objects;

        for (let i = 0, len = metaObjects.length; i < len; i++) {
            const metaObject = metaObjects[i];
            const objectId = metaObject.id;
            const object = objects[objectId];
            if (!object) {
                continue;
            }
            if (visible) {
                this.objectsVisible[i] = object.visible;
            }
            if (edges) {
                this.objectsEdges[i] = object.edges;
            }
            if (xrayed) {
                this.objectsXrayed[i] = object.xrayed;
            }
            if (highlighted) {
                this.objectsHighlighted[i] = object.highlighted;
            }
            if (selected) {
                this.objectsSelected[i] = object.selected;
            }
            if (clippable) {
                this.objectsClippable[i] = object.clippable;
            }
            if (pickable) {
                this.objectsPickable[i] = object.pickable;
            }
            if (colorize) {
                const objectColor = object.colorize;
                this.objectsColorize[i * 3 + 0] = objectColor[0];
                this.objectsColorize[i * 3 + 1] = objectColor[1];
                this.objectsColorize[i * 3 + 2] = objectColor[2];
            }
            if (opacity) {
                this.objectsOpacity[i] = object.opacity;
            }
            this.numObjects++;
        }
    }

    /**
     * Restores a {@link Scene}'s {@link Entity}'s to their state previously captured with {@link ModelMemento#saveObjects}.
     *
     * Assumes that the model has not been destroyed or modified since saving.
     *
     * @param {Scene} scene The scene that was given to {@link ModelMemento#saveObjects}.
     * @param {MetaModel} metaModel The metamodel that was given to {@link ModelMemento#saveObjects}.
     */
    restoreObjects(scene, metaModel) {

        const mask = this._mask;

        const visible = (!mask || mask.visible);
        const edges = (!mask || mask.edges);
        const xrayed = (!mask || mask.xrayed);
        const highlighted = (!mask || mask.highlighted);
        const selected = (!mask || mask.selected);
        const clippable = (!mask || mask.clippable);
        const pickable = (!mask || mask.pickable);
        const colorize = (!mask || mask.colorize);
        const opacity = (!mask || mask.opacity);

        const metaObjects = metaModel.metaObjects;
        const objects = scene.objects;

        for (let i = 0, len = metaObjects.length; i < len; i++) {
            const metaObject = metaObjects[i];
            const objectId = metaObject.id;
            const object = objects[objectId];
            if (!object) {
                continue;
            }
            if (visible) {
                object.visible = this.objectsVisible[i];
            }
            if (edges) {
                object.edges = this.objectsEdges[i];
            }
            if (xrayed) {
                object.xrayed = this.objectsXrayed[i];
            }
            if (highlighted) {
                object.highlighted = this.objectsHighlighted[i];
            }
            if (selected) {
                object.selected = this.objectsSelected[i];
            }
            if (clippable) {
                object.clippable = this.objectsClippable[i];
            }
            if (pickable) {
                object.pickable = this.objectsPickable[i];
            }
            if (colorize) {
                color$3[0] = this.objectsColorize[i * 3 + 0];
                color$3[1] = this.objectsColorize[i * 3 + 1];
                color$3[2] = this.objectsColorize[i * 3 + 2];
                object.colorize = color$3;
            }
            if (opacity) {
                object.opacity = this.objectsOpacity[i];
            }
        }
    }
}

math.vec3();

const tempVec3a$c = math.vec3();
const tempVec3b$9 = math.vec3();
const tempVec4a$8 = math.vec4();
const front = math.vec3([0,  0, -1]);
const back  = math.vec3([0,  0,  1]);
const up    = math.vec3([0,  1,  0]);

/**
 *  @desc An arbitrarily-aligned World-space clipping plane.
 *
 * * Slices portions off objects to create cross-section views or reveal interiors.
 * * Registered by {@link SectionPlane#id} in {@link Scene#sectionPlanes}.
 * * Indicates World-space position in {@link SectionPlane#pos} and orientation in {@link SectionPlane#dir}.
 * * Discards elements from the half-space in the direction of {@link SectionPlane#dir}.
 * * Can be be enabled or disabled via {@link SectionPlane#active}.
 *
 * ## Usage
 *
 * In the example below, we'll create two SectionPlanes to slice a model loaded from glTF. Note that we could also create them
 * using a {@link SectionPlanesPlugin}.
 *
 * ````javascript
 * import {Viewer, GLTFLoaderPlugin, SectionPlane} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *      canvasId: "myCanvas"
 * });
 *
 * const gltfLoaderPlugin = new GLTFModelsPlugin(viewer, {
 *      id: "GLTFModels"
 * });
 *
 * const model = gltfLoaderPlugin.load({
 *      id: "myModel",
 *      src: "./models/gltf/mygltfmodel.gltf"
 * });
 *
 * // Create a SectionPlane on negative diagonal
 * const sectionPlane1 = new SectionPlane(viewer.scene, {
 *     pos: [1.0, 1.0, 1.0],
 *     dir: [-1.0, -1.0, -1.0],
 *     active: true
 * }),
 *
 * // Create a SectionPlane on positive diagonal
 * const sectionPlane2 = new SectionPlane(viewer.scene, {
 *     pos: [-1.0, -1.0, -1.0],
 *     dir: [1.0, 1.0, 1.0],
 *     active: true
 * });
 * ````
 */
class SectionPlane extends Component {

    /**
     @private
     */
    get type() {
        return "SectionPlane";
    }

    /**
     * @constructor
     * @param {Component} [owner]  Owner component. When destroyed, the owner will destroy this SectionPlane as well.
     * @param {*} [cfg]  SectionPlane configuration
     * @param  {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted.
     * @param {Boolean} [cfg.active=true] Indicates whether or not this SectionPlane is active.
     * @param {Number[]} [cfg.pos=[0,0,0]] World-space position of the SectionPlane.
     * @param {Number[]} [cfg.dir=[0,0,-1]] Vector perpendicular to the plane surface, indicating the SectionPlane plane orientation.
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._state = new RenderState({
            active: true,
            pos: math.vec3(),
            quaternion: math.vec4(),
            roll: 0,
            dir: math.vec3(),
            dist: 0
        });

        this.active = cfg.active;
        this.pos = cfg.pos;
        this.dir = cfg.dir;

        this.scene._sectionPlaneCreated(this);
    }

    /**
     * Sets if this SectionPlane is active or not.
     *
     * Default value is ````true````.
     *
     * @param {Boolean} value Set ````true```` to activate else ````false```` to deactivate.
     */
    set active(value) {
        this._state.active = value !== false;
        this.glRedraw();
        this.fire("active", this._state.active);
        this.scene.fire("sectionPlaneUpdated", this);
    }

    /**
     * Gets if this SectionPlane is active or not.
     *
     * Default value is ````true````.
     *
     * @returns {Boolean} Returns ````true```` if active.
     */
    get active() {
        return this._state.active;
    }

    /**
     * Sets the World-space position of this SectionPlane's plane.
     *
     * Default value is ````[0, 0, 0]````.
     *
     * @param {Number[]} value New position.
     */
    set pos(value) {
        this._state.pos.set(value || [0, 0, 0]);
        this._state.dist = (-math.dotVec3(this._state.pos, this._state.dir));
        this.fire("pos", this._state.pos);
        this.scene.fire("sectionPlaneUpdated", this);
    }

    /**
     * Gets the World-space position of this SectionPlane's plane.
     *
     * Default value is ````[0, 0, 0]````.
     *
     * @returns {Number[]} Current position.
     */
    get pos() {
        return this._state.pos;
    }

    /**
     * Sets the quaternion of this SectionPlane's plane.
     *
     * Default value is ````[0, -1, 0, 0]````.
     *
     * @param {Number[]} value New quaternion.
     */
    set quaternion(value) {
        this._state.quaternion.set(value || [0, 0, 0, -1]);
        math.vec3ApplyQuaternion(this._state.quaternion, back, this._state.dir);
        const quatUp = math.vec3ApplyQuaternion(this._state.quaternion, up, tempVec3a$c);
        const dirOnlyQ = math.vec3PairToQuaternion(back, this._state.dir, tempVec4a$8);
        const dirOnlyUp = math.vec3ApplyQuaternion(dirOnlyQ, up, tempVec3b$9);
        const angle = Math.acos(Math.min(1, math.dotVec3(quatUp, dirOnlyUp)));
        const sign = Math.sign(math.dotVec3(this._state.dir, math.cross3Vec3(quatUp, dirOnlyUp, tempVec3b$9)));
        this._state.roll = sign * angle;
        this._onDirUpdated();
    }

    /**
     * Gets the quaternion of this SectionPlane's plane.
     *
     * Default value is ````[0, -1, 0, 0]````.
     *
     * @returns {Number[]} value Current quaternion.
     */
    get quaternion() {
        return this._state.quaternion;
    }

    /**
     * Sets the roll of this SectionPlane's plane.
     *
     * Default value is ````0````.
     *
     * @param {Number[]} value New roll.
     */
    set roll(value) {
        this._state.roll = value || 0;
        this._onDirRollUpdated();
    }

    /**
     * Gets the roll of this SectionPlane's plane.
     *
     * Default value is ````0````.
     *
     * @returns {Number[]} value Current roll.
     */
    get roll() {
        return this._state.roll;
    }

    /**
     * Sets the direction of this SectionPlane's plane.
     *
     * Default value is ````[0, 0, -1]````.
     *
     * @param {Number[]} value New direction.
     */
    set dir(value) {
        this._state.dir.set(value || front);
        this._onDirRollUpdated();
    }

    _onDirRollUpdated() {
        math.vec3PairToQuaternion(back, this._state.dir, this._state.quaternion);

        tempVec4a$8[0] = 0;
        tempVec4a$8[1] = 0;
        tempVec4a$8[2] = -1;
        tempVec4a$8[3] = this._state.roll;
        math.angleAxisToQuaternion(tempVec4a$8, tempVec4a$8);

        math.mulQuaternions(this._state.quaternion, tempVec4a$8, this._state.quaternion);

        this._onDirUpdated();
    }

    _onDirUpdated() {
        this._state.dist = (-math.dotVec3(this._state.pos, this._state.dir));
        this.glRedraw();
        this.fire("dir", this._state.dir);
        this.scene.fire("sectionPlaneUpdated", this);
    }

    /**
     * Gets the direction of this SectionPlane's plane.
     *
     * Default value is ````[0, 0, -1]````.
     *
     * @returns {Number[]} value Current direction.
     */
    get dir() {
        return this._state.dir;
    }

    /**
     * Gets this SectionPlane's distance to the origin of the World-space coordinate system.
     *
     * This is the dot product of {@link SectionPlane#pos} and {@link SectionPlane#dir} and is automatically re-calculated
     * each time either of two properties are updated.
     *
     * @returns {Number}
     */
    get dist() {
        return this._state.dist;
    }

    /**
     * Inverts the direction of {@link SectionPlane#dir}.
     */
    flipDir() {
        math.mulVec3Scalar(this._state.dir, -1, this._state.dir);
        this.dir = this._state.dir;
    }

    /**
     * @destroy
     */
    destroy() {
        this._state.destroy();
        this.scene._sectionPlaneDestroyed(this);
        super.destroy();
    }
}

/**
 * @desc Pick result returned by {@link Scene#pick}.
 *
 */
class PickResult {

    /**
     * @private
     */
    constructor() {

        /**
         * Picked entity.
         * Null when no entity was picked.
         * @property entity
         * @type {Entity|*}
         */
        this.entity = null;

        /**
         * Type of primitive that was picked - usually "triangle".
         * Null when no primitive was picked.
         * @property primitive
         * @type {String}
         */
        this.primitive = null;

        /**
         * Index of primitive that was picked.
         * -1 when no entity was picked.
         * @property primIndex
         * @type {number}
         */
        this.primIndex = -1;

        /**
         * True when the picked surface position is full precision.
         * When false, the picked surface position should be regarded as approximate.
         * Full-precision surface picking is performed with the {@link Scene#pick} method's ````pickSurfacePrecision```` option.
         * @property pickSurfacePrecision
         * @type {Boolean}
         */
        this.pickSurfacePrecision = false;

        /**
         * True when picked from touch input, else false when from mouse input.
         * @type {boolean}
         */
        this.touchInput = false;

        /**
         * True when snapped to nearest edge.
         * @type {boolean}
         */
        this.snappedToEdge = false;

        /**
         * True when snapped to nearest vertex.
         * @type {boolean}
         */
        this.snappedToVertex = false;

        this._origin = new Float64Array([0, 0, 0]);
        this._direction = new Float64Array([0, 0, 0]);
        this._indices = new Int32Array(3);
        this._localPos = new Float64Array([0, 0, 0]);
        this._worldPos = new Float64Array([0, 0, 0]);
        this._viewPos = new Float64Array([0, 0, 0]);
        this._canvasPos = new Int16Array([0, 0]);
        this._snappedCanvasPos = new Int16Array([0, 0]);
        this._bary = new Float64Array([0, 0, 0]);
        this._worldNormal = new Float64Array([0, 0, 0]);
        this._uv = new Float64Array([0, 0]);

        this.reset();
    }

    /**
     * Canvas pick coordinates.
     * @property canvasPos
     * @type {Number[]}
     */
    get canvasPos() {
        return this._gotCanvasPos ? this._canvasPos : null;
    }

    /**
     * @private
     * @param value
     */
    set canvasPos(value) {
        if (value) {
            this._canvasPos[0] = value[0];
            this._canvasPos[1] = value[1];
            this._gotCanvasPos = true;
        } else {
            this._gotCanvasPos = false;
        }
    }

    /**
     * World-space 3D ray origin when raypicked.
     * @property origin
     * @type {Number[]}
     */
    get origin() {
        return this._gotOrigin ? this._origin : null;
    }

    /**
     * @private
     * @param value
     */
    set origin(value) {
        if (value) {
            this._origin[0] = value[0];
            this._origin[1] = value[1];
            this._origin[2] = value[2];
            this._gotOrigin = true;
        } else {
            this._gotOrigin = false;
        }
    }

    /**
     * World-space 3D ray direction when raypicked.
     * @property direction
     * @type {Number[]}
     */
    get direction() {
        return this._gotDirection ? this._direction : null;
    }

    /**
     * @private
     * @param value
     */
    set direction(value) {
        if (value) {
            this._direction[0] = value[0];
            this._direction[1] = value[1];
            this._direction[2] = value[2];
            this._gotDirection = true;
        } else {
            this._gotDirection = false;
        }
    }
    
    /**
     * Picked triangle's vertex indices.
     * @property indices
     * @type {Int32Array}
     */
    get indices() {
        return this.entity && this._gotIndices ? this._indices : null;
    }

    /**
     * @private
     * @param value
     */
    set indices(value) {
        if (value) {
            this._indices[0] = value[0];
            this._indices[1] = value[1];
            this._indices[2] = value[2];
            this._gotIndices = true;
        } else {
            this._gotIndices = false;
        }
    }

    /**
     * Picked Local-space point.
     * @property localPos
     * @type {Number[]}
     */
    get localPos() {
        return this.entity && this._gotLocalPos ? this._localPos : null;
    }

    /**
     * @private
     * @param value
     */
    set localPos(value) {
        if (value) {
            this._localPos[0] = value[0];
            this._localPos[1] = value[1];
            this._localPos[2] = value[2];
            this._gotLocalPos = true;
        } else {
            this._gotLocalPos = false;
        }
    }

    /**
     * Canvas cursor coordinates, snapped when snap picking, otherwise same as {@link PickResult#pointerPos}.
     * @property snappedCanvasPos
     * @type {Number[]}
     */
    get snappedCanvasPos() {
        return this._gotSnappedCanvasPos ? this._snappedCanvasPos : null;
    }

    /**
     * @private
     * @param value
     */
    set snappedCanvasPos(value) {
        if (value) {
            this._snappedCanvasPos[0] = value[0];
            this._snappedCanvasPos[1] = value[1];
            this._gotSnappedCanvasPos = true;
        } else {
            this._gotSnappedCanvasPos = false;
        }
    }

    /**
     * Picked World-space point.
     * @property worldPos
     * @type {Number[]}
     */
    get worldPos() {
        return this._gotWorldPos ? this._worldPos : null;
    }

    /**
     * @private
     * @param value
     */
    set worldPos(value) {
        if (value) {
            this._worldPos[0] = value[0];
            this._worldPos[1] = value[1];
            this._worldPos[2] = value[2];
            this._gotWorldPos = true;
        } else {
            this._gotWorldPos = false;
        }
    }

    /**
     * Picked View-space point.
     * @property viewPos
     * @type {Number[]}
     */
    get viewPos() {
        return this.entity && this._gotViewPos ? this._viewPos : null;
    }

    /**
     * @private
     * @param value
     */
    set viewPos(value) {
        if (value) {
            this._viewPos[0] = value[0];
            this._viewPos[1] = value[1];
            this._viewPos[2] = value[2];
            this._gotViewPos = true;
        } else {
            this._gotViewPos = false;
        }
    }

    /**
     * Barycentric coordinate within picked triangle.
     * @property bary
     * @type {Number[]}
     */
    get bary() {
        return this.entity && this._gotBary ? this._bary : null;
    }

    /**
     * @private
     * @param value
     */
    set bary(value) {
        if (value) {
            this._bary[0] = value[0];
            this._bary[1] = value[1];
            this._bary[2] = value[2];
            this._gotBary = true;
        } else {
            this._gotBary = false;
        }
    }

    /**
     * Normal vector at picked position on surface.
     * @property worldNormal
     * @type {Number[]}
     */
    get worldNormal() {
        return this.entity && this._gotWorldNormal ? this._worldNormal : null;
    }

    /**
     * @private
     * @param value
     */
    set worldNormal(value) {
        if (value) {
            this._worldNormal[0] = value[0];
            this._worldNormal[1] = value[1];
            this._worldNormal[2] = value[2];
            this._gotWorldNormal = true;
        } else {
            this._gotWorldNormal = false;
        }
    }

    /**
     * UV coordinates at picked position on surface.
     * @property uv
     * @type {Number[]}
     */
    get uv() {
        return this.entity && this._gotUV ? this._uv : null;
    }

    /**
     * @private
     * @param value
     */
    set uv(value) {
        if (value) {
            this._uv[0] = value[0];
            this._uv[1] = value[1];
            this._gotUV = true;
        } else {
            this._gotUV = false;
        }
    }

    /**
     * True if snapped to edge or vertex.
     * @returns {boolean}
     */
    get snapped() {
        return this.snappedToEdge || this.snappedToVertex;
    }

    /**
     * @private
     */
    reset() {
        this.entity = null;
        this.primIndex = -1;
        this.primitive = null;
        this.pickSurfacePrecision = false;
        this._gotCanvasPos = false;
        this._gotSnappedCanvasPos = false;
        this._gotOrigin = false;
        this._gotDirection = false;
        this._gotIndices = false;
        this._gotLocalPos = false;
        this._gotWorldPos = false;
        this._gotViewPos = false;
        this._gotBary = false;
        this._gotWorldNormal = false;
        this._gotUV = false;
        this.touchInput = false;
        this.snappedToEdge = false;
        this.snappedToVertex = false;
    }
}

const os = {
    isIphoneSafari() {
        const userAgent = window.navigator.userAgent;
        const isIphone = /iPhone/i.test(userAgent);
        const isSafari = /Safari/i.test(userAgent) && !/Chrome/i.test(userAgent);

        return isIphone && isSafari;
    },
    isTouchDevice() {
        return (
            'ontouchstart' in window || //works for most devices
            navigator.maxTouchPoints > 0 || //works for modern touch devices
            navigator.mxMaxTouchPoints > 0 //works for older microsoft touch devices
        )
    }
};

/**
 @desc Base class for {@link Viewer} plugin classes.
 */
class Plugin {

    /**
     * Creates this Plugin and installs it into the given {@link Viewer}.
     *
     * @param {string} id ID for this plugin, unique among all plugins in the viewer.
     * @param {Viewer} viewer The viewer.
     * @param {Object} [cfg] Options
     */
    constructor(id, viewer, cfg) {

        /**
         * ID for this Plugin, unique within its {@link Viewer}.
         *
         * @type {string}
         */
        this.id = (cfg && cfg.id) ? cfg.id : id;

        /**
         * The Viewer that contains this Plugin.
         *
         * @type {Viewer}
         */
        this.viewer = viewer;

        this._subIdMap = null; // Subscription subId pool
        this._subIdEvents = null; // Subscription subIds mapped to event names
        this._eventSubs = null; // Event names mapped to subscribers
        this._eventSubsNum = null;
        this._events = null; // Maps names to events
        this._eventCallDepth = 0; // Helps us catch stack overflows from recursive events

        viewer.addPlugin(this);
    }

    /**
     * Schedule a task to perform on the next browser interval
     * @param task
     */
    scheduleTask(task) {
        core.scheduleTask(task, null);
    }

    /**
     * Fires an event on this Plugin.
     *
     * Notifies existing subscribers to the event, optionally retains the event to give to
     * any subsequent notifications on the event as they are made.
     *
     * @param {String} event The event type name
     * @param {Object} value The event parameters
     * @param {Boolean} [forget=false] When true, does not retain for subsequent subscribers
     */
    fire(event, value, forget) {
        if (!this._events) {
            this._events = {};
        }
        if (!this._eventSubs) {
            this._eventSubs = {};
            this._eventSubsNum = {};
        }
        if (forget !== true) {
            this._events[event] = value || true; // Save notification
        }
        const subs = this._eventSubs[event];
        let sub;
        if (subs) { // Notify subscriptions
            for (const subId in subs) {
                if (subs.hasOwnProperty(subId)) {
                    sub = subs[subId];
                    this._eventCallDepth++;
                    if (this._eventCallDepth < 300) {
                        sub.callback.call(sub.scope, value);
                    } else {
                        this.error("fire: potential stack overflow from recursive event '" + event + "' - dropping this event");
                    }
                    this._eventCallDepth--;
                }
            }
        }
    }

    /**
     * Subscribes to an event on this Plugin.
     *
     * The callback is be called with this Plugin as scope.
     *
     * @param {String} event The event
     * @param {Function} callback Called fired on the event
     * @param {Object} [scope=this] Scope for the callback
     * @return {String} Handle to the subscription, which may be used to unsubscribe with {@link #off}.
     */
    on(event, callback, scope) {
        if (!this._events) {
            this._events = {};
        }
        if (!this._subIdMap) {
            this._subIdMap = new Map$1$1(); // Subscription subId pool
        }
        if (!this._subIdEvents) {
            this._subIdEvents = {};
        }
        if (!this._eventSubs) {
            this._eventSubs = {};
        }
        if (!this._eventSubsNum) {
            this._eventSubsNum = {};
        }
        let subs = this._eventSubs[event];
        if (!subs) {
            subs = {};
            this._eventSubs[event] = subs;
            this._eventSubsNum[event] = 1;
        } else {
            this._eventSubsNum[event]++;
        }
        const subId = this._subIdMap.addItem(); // Create unique subId
        subs[subId] = {
            callback: callback,
            scope: scope || this
        };
        this._subIdEvents[subId] = event;
        const value = this._events[event];
        if (value !== undefined) { // A publication exists, notify callback immediately
            callback.call(scope || this, value);
        }
        return subId;
    }

    /**
     * Cancels an event subscription that was previously made with {@link Plugin#on} or {@link Plugin#once}.
     *
     * @param {String} subId Subscription ID
     */
    off(subId) {
        if (subId === undefined || subId === null) {
            return;
        }
        if (!this._subIdEvents) {
            return;
        }
        const event = this._subIdEvents[subId];
        if (event) {
            delete this._subIdEvents[subId];
            const subs = this._eventSubs[event];
            if (subs) {
                delete subs[subId];
                this._eventSubsNum[event]--;
            }
            this._subIdMap.removeItem(subId); // Release subId
        }
    }

    /**
     * Subscribes to the next occurrence of the given event, then un-subscribes as soon as the event is subIdd.
     *
     * This is equivalent to calling {@link Plugin#on}, and then calling {@link Plugin#off} inside the callback function.
     *
     * @param {String} event Data event to listen to
     * @param {Function} callback Called when fresh data is available at the event
     * @param {Object} [scope=this] Scope for the callback
     */
    once(event, callback, scope) {
        const self = this;
        const subId = this.on(event,
            function (value) {
                self.off(subId);
                callback.call(scope || this, value);
            },
            scope);
    }

    /**
     * Returns true if there are any subscribers to the given event on this Plugin.
     *
     * @param {String} event The event
     * @return {Boolean} True if there are any subscribers to the given event on this Plugin.
     */
    hasSubs(event) {
        return (this._eventSubsNum && (this._eventSubsNum[event] > 0));
    }

    /**
     * Logs a message to the JavaScript developer console, prefixed with the ID of this Plugin.
     *
     * @param {String} msg The error message
     */
    log(msg) {
        console.log(`[xeokit plugin ${this.id}]: ${msg}`);
    }

    /**
     * Logs a warning message to the JavaScript developer console, prefixed with the ID of this Plugin.
     *
     * @param {String} msg The error message
     */
    warn(msg) {
        console.warn(`[xeokit plugin ${this.id}]: ${msg}`);
    }

    /**
     * Logs an error message to the JavaScript developer console, prefixed with the ID of this Plugin.
     *
     * @param {String} msg The error message
     */
    error(msg) {
        console.error(`[xeokit plugin ${this.id}]: ${msg}`);
    }

    /**
     * Sends a message to this Plugin.
     *
     * @private
     */
    send(name, value) {
        //...
    }

    /**
     * Destroys this Plugin and removes it from its {@link Viewer}.
     */
    destroy() {
        this.viewer.removePlugin(this);
    }
}

const defaultCSS = ".sk-fading-circle {\
        background: transparent;\
        margin: 20px auto;\
        width: 50px;\
        height:50px;\
        position: relative;\
        }\
        .sk-fading-circle .sk-circle {\
        width: 120%;\
        height: 120%;\
        position: absolute;\
        left: 0;\
        top: 0;\
        }\
        .sk-fading-circle .sk-circle:before {\
        content: '';\
        display: block;\
        margin: 0 auto;\
        width: 15%;\
        height: 15%;\
        background-color: #ff8800;\
        border-radius: 100%;\
        -webkit-animation: sk-circleFadeDelay 1.2s infinite ease-in-out both;\
        animation: sk-circleFadeDelay 1.2s infinite ease-in-out both;\
        }\
        .sk-fading-circle .sk-circle2 {\
        -webkit-transform: rotate(30deg);\
        -ms-transform: rotate(30deg);\
        transform: rotate(30deg);\
    }\
    .sk-fading-circle .sk-circle3 {\
        -webkit-transform: rotate(60deg);\
        -ms-transform: rotate(60deg);\
        transform: rotate(60deg);\
    }\
    .sk-fading-circle .sk-circle4 {\
        -webkit-transform: rotate(90deg);\
        -ms-transform: rotate(90deg);\
        transform: rotate(90deg);\
    }\
    .sk-fading-circle .sk-circle5 {\
        -webkit-transform: rotate(120deg);\
        -ms-transform: rotate(120deg);\
        transform: rotate(120deg);\
    }\
    .sk-fading-circle .sk-circle6 {\
        -webkit-transform: rotate(150deg);\
        -ms-transform: rotate(150deg);\
        transform: rotate(150deg);\
    }\
    .sk-fading-circle .sk-circle7 {\
        -webkit-transform: rotate(180deg);\
        -ms-transform: rotate(180deg);\
        transform: rotate(180deg);\
    }\
    .sk-fading-circle .sk-circle8 {\
        -webkit-transform: rotate(210deg);\
        -ms-transform: rotate(210deg);\
        transform: rotate(210deg);\
    }\
    .sk-fading-circle .sk-circle9 {\
        -webkit-transform: rotate(240deg);\
        -ms-transform: rotate(240deg);\
        transform: rotate(240deg);\
    }\
    .sk-fading-circle .sk-circle10 {\
        -webkit-transform: rotate(270deg);\
        -ms-transform: rotate(270deg);\
        transform: rotate(270deg);\
    }\
    .sk-fading-circle .sk-circle11 {\
        -webkit-transform: rotate(300deg);\
        -ms-transform: rotate(300deg);\
        transform: rotate(300deg);\
    }\
    .sk-fading-circle .sk-circle12 {\
        -webkit-transform: rotate(330deg);\
        -ms-transform: rotate(330deg);\
        transform: rotate(330deg);\
    }\
    .sk-fading-circle .sk-circle2:before {\
        -webkit-animation-delay: -1.1s;\
        animation-delay: -1.1s;\
    }\
    .sk-fading-circle .sk-circle3:before {\
        -webkit-animation-delay: -1s;\
        animation-delay: -1s;\
    }\
    .sk-fading-circle .sk-circle4:before {\
        -webkit-animation-delay: -0.9s;\
        animation-delay: -0.9s;\
    }\
    .sk-fading-circle .sk-circle5:before {\
        -webkit-animation-delay: -0.8s;\
        animation-delay: -0.8s;\
    }\
    .sk-fading-circle .sk-circle6:before {\
        -webkit-animation-delay: -0.7s;\
        animation-delay: -0.7s;\
    }\
    .sk-fading-circle .sk-circle7:before {\
        -webkit-animation-delay: -0.6s;\
        animation-delay: -0.6s;\
    }\
    .sk-fading-circle .sk-circle8:before {\
        -webkit-animation-delay: -0.5s;\
        animation-delay: -0.5s;\
    }\
    .sk-fading-circle .sk-circle9:before {\
        -webkit-animation-delay: -0.4s;\
        animation-delay: -0.4s;\
    }\
    .sk-fading-circle .sk-circle10:before {\
        -webkit-animation-delay: -0.3s;\
        animation-delay: -0.3s;\
    }\
    .sk-fading-circle .sk-circle11:before {\
        -webkit-animation-delay: -0.2s;\
        animation-delay: -0.2s;\
    }\
    .sk-fading-circle .sk-circle12:before {\
        -webkit-animation-delay: -0.1s;\
        animation-delay: -0.1s;\
    }\
    @-webkit-keyframes sk-circleFadeDelay {\
        0%, 39%, 100% { opacity: 0; }\
        40% { opacity: 1; }\
    }\
    @keyframes sk-circleFadeDelay {\
        0%, 39%, 100% { opacity: 0; }\
        40% { opacity: 1; }\
    }";

/**
 * @desc Displays a progress animation at the center of its {@link Canvas} while things are loading or otherwise busy.
 *
 *
 * * Located at {@link Canvas#spinner}.
 * * Automatically shown while things are loading, however may also be shown by application code wanting to indicate busyness.
 * * {@link Spinner#processes} holds the count of active processes. As a process starts, it increments {@link Spinner#processes}, then decrements it on completion or failure.
 * * A Spinner is only visible while {@link Spinner#processes} is greater than zero.
 *
 * ````javascript
 * var spinner = viewer.scene.canvas.spinner;
 *
 * // Increment count of busy processes represented by the spinner;
 * // assuming the count was zero, this now shows the spinner
 * spinner.processes++;
 *
 * // Increment the count again, by some other process; spinner already visible, now requires two decrements
 * // before it becomes invisible again
 * spinner.processes++;
 *
 * // Decrement the count; count still greater than zero, so spinner remains visible
 * spinner.process--;
 *
 * // Decrement the count; count now zero, so spinner becomes invisible
 * spinner.process--;
 * ````
 */
class Spinner extends Component {

    /**
     @private
     */
    get type() {
        return "Spinner";
    }

    /**
     @private
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._canvas = cfg.canvas;
        this._element = null;
        this._isCustom = false; // True when the element is custom HTML

        if (cfg.elementId) { // Custom spinner element supplied
            this._element = document.getElementById(cfg.elementId);
            if (!this._element) {
                this.error("Can't find given Spinner HTML element: '" + cfg.elementId + "' - will automatically create default element");
            } else {
                this._adjustPosition();
            }
        }

        if (!this._element) {
            this._createDefaultSpinner();
        }

        this.processes = 0;
    }

    /** @private */
    _createDefaultSpinner() {
        this._injectDefaultCSS();
        const element = document.createElement('div');
        const style = element.style;
        style["z-index"] = "9000";
        style.position = "absolute";
        element.innerHTML = '<div class="sk-fading-circle">\
                <div class="sk-circle1 sk-circle"></div>\
                <div class="sk-circle2 sk-circle"></div>\
                <div class="sk-circle3 sk-circle"></div>\
                <div class="sk-circle4 sk-circle"></div>\
                <div class="sk-circle5 sk-circle"></div>\
                <div class="sk-circle6 sk-circle"></div>\
                <div class="sk-circle7 sk-circle"></div>\
                <div class="sk-circle8 sk-circle"></div>\
                <div class="sk-circle9 sk-circle"></div>\
                <div class="sk-circle10 sk-circle"></div>\
                <div class="sk-circle11 sk-circle"></div>\
                <div class="sk-circle12 sk-circle"></div>\
                </div>';
        this._canvas.parentElement.appendChild(element);
        this._element = element;
        this._isCustom = false;
        this._adjustPosition();
    }

    /**
     * @private
     */
    _injectDefaultCSS() {
        const elementId = "xeokit-spinner-css";
        if (document.getElementById(elementId)) {
            return;
        }
        const defaultCSSNode = document.createElement('style');
        defaultCSSNode.innerHTML = defaultCSS;
        defaultCSSNode.id = elementId;
        document.body.appendChild(defaultCSSNode);
    }

    /**
     * @private
     */
    _adjustPosition() { // (Re)positions spinner DIV over the center of the canvas - called by Canvas
        if (this._isCustom) {
            return;
        }
        const canvas = this._canvas;
        const element = this._element;
        const style = element.style;
        style["left"] = (canvas.offsetLeft + (canvas.clientWidth * 0.5) - (element.clientWidth * 0.5)) + "px";
        style["top"] = (canvas.offsetTop + (canvas.clientHeight * 0.5) - (element.clientHeight * 0.5)) + "px";
    }

    /**
     * Sets the number of processes this Spinner represents.
     *
     * The Spinner is visible while this property is greater than zero.
     *
     * Increment this property whenever you commence some process during which you want the Spinner to be visible, then decrement it again when the process is complete.
     *
     * Clamps to zero if you attempt to set to to a negative value.
     *
     * Fires a {@link Spinner#processes:event} event on change.

     * Default value is ````0````.
     *
     * @param {Number} value New processes count.
     */
    set processes(value) {
        value = value || 0;
        if (this._processes === value) {
            return;
        }
        if (value < 0) {
            return;
        }
        const prevValue = this._processes;
        this._processes = value;
        const element = this._element;
        if (element) {
            element.style["visibility"] = (this._processes > 0) ? "visible" : "hidden";
        }
        /**
         Fired whenever this Spinner's {@link Spinner#visible} property changes.

         @event processes
         @param value The property's new value
         */
        this.fire("processes", this._processes);
        if (this._processes === 0 && this._processes !== prevValue) {
            /**
             Fired whenever this Spinner's {@link Spinner#visible} property becomes zero.

             @event zeroProcesses
             */
            this.fire("zeroProcesses", this._processes);
        }
    }

    /**
     * Gets the number of processes this Spinner represents.
     *
     * The Spinner is visible while this property is greater than zero.
     *
     * @returns {Number} Current processes count.
     */
    get processes() {
        return this._processes;
    }

    _destroy() {
        if (this._element && (!this._isCustom)) {
            this._element.parentNode.removeChild(this._element);
            this._element = null;
        }
        const styleElement = document.getElementById("xeokit-spinner-css");
        if (styleElement) {
            styleElement.parentNode.removeChild(styleElement);
        }
    }
}

const WEBGL_CONTEXT_NAMES = [
    "webgl2",
    "experimental-webgl",
    "webkit-3d",
    "moz-webgl",
    "moz-glweb20"
];

/**
 * @desc Manages its {@link Scene}'s HTML canvas.
 *
 * * Provides the HTML canvas element in {@link Canvas#canvas}.
 * * Has a {@link Spinner}, provided at {@link Canvas#spinner}, which manages the loading progress indicator.
 */
class Canvas extends Component {

    /**
     * @constructor
     * @private
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._backgroundColor = math.vec3([
            cfg.backgroundColor ? cfg.backgroundColor[0] : 1,
            cfg.backgroundColor ? cfg.backgroundColor[1] : 1,
            cfg.backgroundColor ? cfg.backgroundColor[2] : 1]);
        this._backgroundColorFromAmbientLight = !!cfg.backgroundColorFromAmbientLight;

        /**
         * The HTML canvas.
         *
         * @property canvas
         * @type {HTMLCanvasElement}
         * @final
         */
        this.canvas = cfg.canvas;

        /**
         * The WebGL rendering context.
         *
         * @property gl
         * @type {WebGLRenderingContext}
         * @final
         */
        this.gl = null;

        /**
         * True when WebGL 2 support is enabled.
         *
         * @property webgl2
         * @type {Boolean}
         * @final
         */
        this.webgl2 = false; // Will set true in _initWebGL if WebGL is requested and we succeed in getting it.

        /**
         * Indicates if this Canvas is transparent.
         *
         * @property transparent
         * @type {Boolean}
         * @default {false}
         * @final
         */
        this.transparent = !!cfg.transparent;

        /**
         * Attributes for the WebGL context
         *
         * @type {{}|*}
         */
        this.contextAttr = cfg.contextAttr || {};
        this.contextAttr.alpha = this.transparent;

        this.contextAttr.preserveDrawingBuffer = !!this.contextAttr.preserveDrawingBuffer;
        this.contextAttr.stencil = false;
        this.contextAttr.premultipliedAlpha = (!!this.contextAttr.premultipliedAlpha);  // False by default: https://github.com/xeokit/xeokit-sdk/issues/251
        this.contextAttr.antialias = (this.contextAttr.antialias !== false);

        // If the canvas uses css styles to specify the sizes make sure the basic
        // width and height attributes match or the WebGL context will use 300 x 150

        this.resolutionScale = cfg.resolutionScale;

        this.canvas.width = Math.round(this.canvas.clientWidth * this._resolutionScale);
        this.canvas.height = Math.round(this.canvas.clientHeight * this._resolutionScale);

        /**
         * Boundary of the Canvas in absolute browser window coordinates.
         *
         * ### Usage:
         *
         * ````javascript
         * var boundary = myScene.canvas.boundary;
         *
         * var xmin = boundary[0];
         * var ymin = boundary[1];
         * var width = boundary[2];
         * var height = boundary[3];
         * ````
         *
         * @property boundary
         * @type {Number[]}
         * @final
         */
        this.boundary = [
            this.canvas.offsetLeft, this.canvas.offsetTop,
            this.canvas.clientWidth, this.canvas.clientHeight
        ];

        // Get WebGL context

        this._initWebGL(cfg);

        // Bind context loss and recovery handlers

        const self = this;

        this.canvas.addEventListener("webglcontextlost", this._webglcontextlostListener = function (event) {
                console.time("webglcontextrestored");
                self.scene._webglContextLost();
                /**
                 * Fired whenever the WebGL context has been lost
                 * @event webglcontextlost
                 */
                self.fire("webglcontextlost");
                event.preventDefault();
            },
            false);

        this.canvas.addEventListener("webglcontextrestored", this._webglcontextrestoredListener = function (event) {
                self._initWebGL();
                if (self.gl) {
                    self.scene._webglContextRestored(self.gl);
                    /**
                     * Fired whenever the WebGL context has been restored again after having previously being lost
                     * @event webglContextRestored
                     * @param value The WebGL context object
                     */
                    self.fire("webglcontextrestored", self.gl);
                    event.preventDefault();
                }
                console.timeEnd("webglcontextrestored");
            },
            false);

        // Attach to resize events on the canvas
        let dirtyBoundary = true; // make sure we publish the 1st boundary event

        const resizeObserver = new ResizeObserver((entries) => {
            for (const entry of entries) {
                if (entry.contentBoxSize) {
                    dirtyBoundary = true;
                }
            }
        });

        resizeObserver.observe(this.canvas);

        // Publish canvas size and position changes on each scene tick
        this._tick = this.scene.on("tick", () => {
            // Only publish if the canvas bounds changed
            if (!dirtyBoundary) {
                return;
            }

            dirtyBoundary = false;

            // Set the real size of the canvas (the drawable w*h)
            self.canvas.width = Math.round(self.canvas.clientWidth * self._resolutionScale);
            self.canvas.height = Math.round(self.canvas.clientHeight * self._resolutionScale);

            // Publish the boundary change
            self.boundary[0] = self.canvas.offsetLeft;
            self.boundary[1] = self.canvas.offsetTop;
            self.boundary[2] = self.canvas.clientWidth;
            self.boundary[3] = self.canvas.clientHeight;

            self.fire("boundary", self.boundary);
        });

        this._spinner = new Spinner(this.scene, {
            canvas: this.canvas,
            elementId: cfg.spinnerElementId
        });
    }

    /**
     @private
     */
    get type() {
        return "Canvas";
    }

    /**
     * Gets whether the canvas clear color will be derived from {@link AmbientLight} or {@link Canvas#backgroundColor}
     * when {@link Canvas#transparent} is ```true```.
     *
     * When {@link Canvas#transparent} is ```true``` and this is ````true````, then the canvas clear color will
     * be taken from the {@link Scene}'s ambient light color.
     *
     * When {@link Canvas#transparent} is ```true``` and this is ````false````, then the canvas clear color will
     * be taken from {@link Canvas#backgroundColor}.
     *
     * Default value is ````true````.
     *
     * @type {Boolean}
     */
    get backgroundColorFromAmbientLight() {
        return this._backgroundColorFromAmbientLight;
    }

    /**
     * Sets if the canvas background color is derived from an {@link AmbientLight}.
     *
     * This only has effect when the canvas is not transparent. When not enabled, the background color
     * will be the canvas element's HTML/CSS background color.
     *
     * Default value is ````true````.
     *
     * @type {Boolean}
     */
    set backgroundColorFromAmbientLight(backgroundColorFromAmbientLight) {
        this._backgroundColorFromAmbientLight = (backgroundColorFromAmbientLight !== false);
        this.glRedraw();
    }

    /**
     * Gets the canvas clear color.
     *
     * Default value is ````[1, 1, 1]````.
     *
     * @type {Number[]}
     */
    get backgroundColor() {
        return this._backgroundColor;
    }

    /**
     * Sets the canvas clear color.
     *
     * Default value is ````[1, 1, 1]````.
     *
     * @type {Number[]}
     */
    set backgroundColor(value) {
        if (value) {
            this._backgroundColor[0] = value[0];
            this._backgroundColor[1] = value[1];
            this._backgroundColor[2] = value[2];
        } else {
            this._backgroundColor[0] = 1.0;
            this._backgroundColor[1] = 1.0;
            this._backgroundColor[2] = 1.0;
        }
        this.glRedraw();
    }

    /**
     * Gets the scale of the canvas back buffer relative to the CSS-defined size of the canvas.
     *
     * This is a common way to trade off rendering quality for speed. If the canvas size is defined in CSS, then
     * setting this to a value between ````[0..1]```` (eg ````0.5````) will render into a smaller back buffer, giving
     * a performance boost.
     *
     * @returns {*|number} The resolution scale.
     */
    get resolutionScale() {
        return this._resolutionScale;
    }

    /**
     * Sets the scale of the canvas back buffer relative to the CSS-defined size of the canvas.
     *
     * This is a common way to trade off rendering quality for speed. If the canvas size is defined in CSS, then
     * setting this to a value between ````[0..1]```` (eg ````0.5````) will render into a smaller back buffer, giving
     * a performance boost.
     *
     * @param {*|number} resolutionScale The resolution scale.
     */
    set resolutionScale(resolutionScale) {
        resolutionScale = resolutionScale || 1.0;
        if (resolutionScale === this._resolutionScale) {
            return;
        }
        this._resolutionScale = resolutionScale;
        const canvas = this.canvas;
        canvas.width = Math.round(canvas.clientWidth * this._resolutionScale);
        canvas.height = Math.round(canvas.clientHeight * this._resolutionScale);
        this.glRedraw();
    }

    /**
     * The busy {@link Spinner} for this Canvas.
     *
     * @property spinner
     * @type Spinner
     * @final
     */
    get spinner() {
        return this._spinner;
    }

    /**
     * Creates a default canvas in the DOM.
     * @private
     */
    _createCanvas() {

        const canvasId = "xeokit-canvas-" + math.createUUID();
        const body = document.getElementsByTagName("body")[0];
        const div = document.createElement('div');

        const style = div.style;
        style.height = "100%";
        style.width = "100%";
        style.padding = "0";
        style.margin = "0";
        style.background = "rgba(0,0,0,0);";
        style.float = "left";
        style.left = "0";
        style.top = "0";
        style.position = "absolute";
        style.opacity = "1.0";
        style["z-index"] = "-10000";

        div.innerHTML += '<canvas id="' + canvasId + '" style="width: 100%; height: 100%; float: left; margin: 0; padding: 0;"></canvas>';

        body.appendChild(div);

        this.canvas = document.getElementById(canvasId);
    }

    _getElementXY(e) {
        let x = 0, y = 0;
        while (e) {
            x += (e.offsetLeft - e.scrollLeft);
            y += (e.offsetTop - e.scrollTop);
            e = e.offsetParent;
        }
        return {x: x, y: y};
    }

    /**
     * Initialises the WebGL context
     * @private
     */
    _initWebGL() {

        // Default context attribute values

        if (!this.gl) {
            for (let i = 0; !this.gl && i < WEBGL_CONTEXT_NAMES.length; i++) {
                try {
                    this.gl = this.canvas.getContext(WEBGL_CONTEXT_NAMES[i], this.contextAttr);
                } catch (e) { // Try with next context name
                }
            }
        }

        if (!this.gl) {

            this.error('Failed to get a WebGL context');

            /**
             * Fired whenever the canvas failed to get a WebGL context, which probably means that WebGL
             * is either unsupported or has been disabled.
             * @event webglContextFailed
             */
            this.fire("webglContextFailed", true, true);
        }

        // data-textures: avoid to re-bind same texture
        {
            const gl = this.gl;

            let lastTextureUnit = "__";

            let originalActiveTexture = gl.activeTexture;

            gl.activeTexture = function (arg1) {
                if (lastTextureUnit === arg1) {
                    return;
                }

                lastTextureUnit = arg1;

                originalActiveTexture.call (this, arg1);
            };

            let lastBindTexture = {};

            let originalBindTexture = gl.bindTexture;

            gl.bindTexture = function (arg1, arg2) {
                if (lastBindTexture[lastTextureUnit] === arg2)
                {
                    return;
                }

                lastBindTexture[lastTextureUnit] = arg2;

                originalBindTexture.call (this, arg1, arg2);
            };

            // setInterval (
            //     () => {
            //         console.log (`${avoidedRebinds} avoided texture binds/sec`);
            //         avoidedRebinds = 0;
            //     },
            //     1000
            // );
        }

        if (this.gl) {
            // Setup extension (if necessary) and hints for fragment shader derivative functions
            if (this.webgl2) {
                this.gl.hint(this.gl.FRAGMENT_SHADER_DERIVATIVE_HINT, this.gl.FASTEST);

                // data-textures: not using standard-derivatives
                if (!(this.gl instanceof WebGL2RenderingContext)) ;
            }
        }
    }

    /**
     * @private
     * @deprecated
     */
    getSnapshot(params) {
        throw "Canvas#getSnapshot() has been replaced by Viewer#getSnapshot() - use that method instead.";
    }

    /**
     * Reads colors of pixels from the last rendered frame.
     *
     * Call this method like this:
     *
     * ````JavaScript
     *
     * // Ignore transparent pixels (default is false)
     * var opaqueOnly = true;
     *
     * var colors = new Float32Array(8);
     *
     * viewer.scene.canvas.readPixels([ 100, 22, 12, 33 ], colors, 2, opaqueOnly);
     * ````
     *
     * Then the r,g,b components of the colors will be set to the colors at those pixels.
     *
     * @param {Number[]} pixels
     * @param {Number[]} colors
     * @param {Number} size
     * @param {Boolean} opaqueOnly
     */
    readPixels(pixels, colors, size, opaqueOnly) {
        return this.scene._renderer.readPixels(pixels, colors, size, opaqueOnly);
    }

    /**
     * Simulates lost WebGL context.
     */
    loseWebGLContext() {
        if (this.canvas.loseContext) {
            this.canvas.loseContext();
        }
    }

    destroy() {
        this.scene.off(this._tick);
        this._spinner._destroy();
        // Memory leak avoidance
        this.canvas.removeEventListener("webglcontextlost", this._webglcontextlostListener);
        this.canvas.removeEventListener("webglcontextrestored", this._webglcontextrestoredListener);
        
        this.gl.getExtension("WEBGL_lose_context").loseContext();
        this.gl = null;
        
        super.destroy();
    }
}

/**
 * @desc Provides rendering context to {@link Drawable"}s as xeokit renders them for a frame.
 *
 * Also creates RTC viewing and picking matrices, caching and reusing matrices within each frame.
 *
 * @private
 */
class FrameContext {

    constructor(scene) {

        this._scene = scene;

        this._matPool = [];
        this._matPoolNextFreeIndex = 0;

        this._rtcViewMats = {};
        this._rtcPickViewMats = {};

        this.reset();
    }

    /**
     * Called by the renderer before each frame.
     * @private
     */
    reset() {

        this._matPoolNextFreeIndex = 0;
        this._rtcViewMats = {};
        this._rtcPickViewMats = {};

        /**
         * The WebGL rendering context.
         * @type {WebGLRenderingContext}
         */
        this.gl = this._scene.canvas.gl;

        /**
         * ID of the last {@link WebGLProgram} that was bound during the current frame.
         * @property lastProgramId
         * @type {Number}
         */
        this.lastProgramId = null;

        /**
         * Whether to render a physically-based representation for triangle surfaces.
         *
         * When ````false````, we'll render them with a fast vertex-shaded Gouraud-shaded representation, which
         * is great for zillions of objects.
         *
         * When ````true````, we'll render them at a better visual quality, using smooth, per-fragment shading
         * and a more realistic lighting model.
         *
         * @property quality
         * @default false
         * @type {Boolean}
         */
        this.pbrEnabled = false;

        /**
         * Whether to render color textures for triangle surfaces.
         *
         * @property quality
         * @default false
         * @type {Boolean}
         */
        this.colorTextureEnabled = false;

        /**
         * Whether SAO is currently enabled during the current frame.
         * @property withSAO
         * @default false
         * @type {Boolean}
         */
        this.withSAO = false;

        /**
         * Whether backfaces are currently enabled during the current frame.
         * @property backfaces
         * @default false
         * @type {Boolean}
         */
        this.backfaces = false;

        /**
         * The vertex winding order for what we currently consider to be a backface during current
         * frame: true == "cw", false == "ccw".
         * @property frontFace
         * @default true
         * @type {Boolean}
         */
        this.frontface = true;

        /**
         * The next available texture unit to bind a {@link Texture} to.
         * @defauilt 0
         * @property textureUnit
         * @type {number}
         */
        this.textureUnit = 0;

        /**
         * Performance statistic that counts how many times the renderer has called ````gl.drawElements()```` has been
         * called so far within the current frame.
         * @default 0
         * @property drawElements
         * @type {number}
         */
        this.drawElements = 0;

        /**
         * Performance statistic that counts how many times ````gl.drawArrays()```` has been called so far within
         * the current frame.
         * @default 0
         * @property drawArrays
         * @type {number}
         */
        this.drawArrays = 0;

        /**
         * Performance statistic that counts how many times ````gl.useProgram()```` has been called so far within
         * the current frame.
         * @default 0
         * @property useProgram
         * @type {number}
         */
        this.useProgram = 0;

        /**
         * Statistic that counts how many times ````gl.bindTexture()```` has been called so far within the current frame.
         * @default 0
         * @property bindTexture
         * @type {number}
         */
        this.bindTexture = 0;

        /**
         * Counts how many times the renderer has called ````gl.bindArray()```` so far within the current frame.
         * @defaulr 0
         * @property bindArray
         * @type {number}
         */
        this.bindArray = 0;

        /**
         * Indicates which pass the renderer is currently rendering.
         *
         * See {@link Scene/passes:property"}}Scene#passes{{/crossLink}}, which configures how many passes we render
         * per frame, which typically set to ````2```` when rendering a stereo view.
         *
         * @property pass
         * @type {number}
         */
        this.pass = 0;

        /**
         * The 4x4 viewing transform matrix the renderer is currently using when rendering castsShadows.
         *
         * This sets the viewpoint to look from the point of view of each {@link DirLight}
         * or {@link PointLight} that casts a shadow.
         *
         * @property shadowViewMatrix
         * @type {Number[]}
         */
        this.shadowViewMatrix = null;

        /**
         * The 4x4 viewing projection matrix the renderer is currently using when rendering shadows.
         *
         * @property shadowProjMatrix
         * @type {Number[]}
         */
        this.shadowProjMatrix = null;

        /**
         * The 4x4 viewing transform matrix the renderer is currently using when rendering a ray-pick.
         *
         * This sets the viewpoint to look along the ray given to {@link Scene/pick:method"}}Scene#pick(){{/crossLink}}
         * when picking with a ray.
         *
         * @property pickViewMatrix
         * @type {Number[]}
         */
        this.pickViewMatrix = null;

        /**
         * The 4x4 orthographic projection transform matrix the renderer is currently using when rendering a ray-pick.
         *
         * @property pickProjMatrix
         * @type {Number[]}
         */
        this.pickProjMatrix = null;

        /**
         * Distance to the near clipping plane when rendering depth fragments for GPU-accelerated 3D picking.
         *
         * @property pickZNear
         * @type {Number|*}
         */
        this.pickZNear = 0.01;

        /**
         * Distance to the far clipping plane when rendering depth fragments for GPU-accelerated 3D picking.
         *
         * @property pickZFar
         * @type {Number|*}
         */
        this.pickZFar = 5000;

        /**
         * Whether or not the renderer is currently picking invisible objects.
         *
         * @property pickInvisible
         * @type {Number}
         */
        this.pickInvisible = false;

        /**
         * Used to draw only requested elements / indices.
         *
         * @property pickElementsCount
         * @type {Number}
         */
        this.pickElementsCount = null;

        /**
         * Used to draw only requested elements / indices.
         *
         * @property pickElementsOffset
         * @type {Number}
         */
        this.pickElementsOffset = null;

        /** The current line width.
         *
         * @property lineWidth
         * @type Number
         */
        this.lineWidth = 1;

        /**
         * Collects info from SceneModel.drawSnapInit and SceneModel.drawSnap,
         * which is then used in Renderer to determine snap-picking results.
         *
         * @type {{}}
         */
        this.snapPickLayerParams = {};

        /**
         * Collects info from SceneModel.drawSnapInit and SceneModel.drawSnap,
         * which is then used in Renderer to determine snap-picking results.
         * @type {number}
         */
        this.snapPickLayerNumber = 0;

        /**
         * Collects info from SceneModel.drawSnapInit and SceneModel.drawSnap,
         * which is then used in Renderer to determine snap-picking results.
         * @type {Number[]}
         */
        this.snapPickCoordinateScale = math.vec3();

        /**
         * Collects info from SceneModel.drawSnapInit and SceneModel.drawSnap,
         * which is then used in Renderer to determine snap-picking results.
         * @type {Number[]}
         */
        this.snapPickOrigin = math.vec3();
    }

    /**
     * Get View matrix for the given RTC center.
     */
    getRTCViewMatrix(originHash, origin) {
        let rtcViewMat = this._rtcViewMats[originHash];
        if (!rtcViewMat) {
            rtcViewMat = this._getNewMat();
            createRTCViewMat(this._scene.camera.viewMatrix, origin, rtcViewMat);
            this._rtcViewMats[originHash] = rtcViewMat;
        }
        return rtcViewMat;
    }

    /**
     * Get picking View RTC matrix for the given RTC center.
     */
    getRTCPickViewMatrix(originHash, origin) {
        let rtcPickViewMat = this._rtcPickViewMats[originHash];
        if (!rtcPickViewMat) {
            rtcPickViewMat = this._getNewMat();
            const pickViewMat = this.pickViewMatrix || this._scene.camera.viewMatrix;
            createRTCViewMat(pickViewMat, origin, rtcPickViewMat);
            this._rtcPickViewMats[originHash] = rtcPickViewMat;
        }
        return rtcPickViewMat;
    }

    _getNewMat() {
        let mat = this._matPool[this._matPoolNextFreeIndex];
        if (!mat) {
            mat = math.mat4();
            this._matPool[this._matPoolNextFreeIndex] = mat;
        }
        this._matPoolNextFreeIndex++;
        return mat;
    }
}

/**
 * @private
 */
class OcclusionLayer {

    constructor(scene, origin) {

        this.scene = scene;
        this.aabb = math.AABB3();
        this.origin = math.vec3(origin);
        this.originHash = this.origin.join();
        this.numMarkers = 0;
        this.markers = {};
        this.markerList = [];                  // Ordered array of Markers
        this.markerIndices = {};               // ID map of Marker indices in _markerList
        this.positions = [];                   // Packed array of World-space marker positions
        this.indices = [];                     // Indices corresponding to array above
        this.positionsBuf = null;
        this.lenPositionsBuf = 0;
        this.indicesBuf = null;
        this.sectionPlanesActive = [];
        this.culledBySectionPlanes = false;
        this.occlusionTestList = [];           // List of
        this.lenOcclusionTestList = 0;
        this.pixels = [];
        this.aabbDirty = false;
        this.markerListDirty = false;
        this.positionsDirty = true;
        this.occlusionTestListDirty = false;
    }

    addMarker(marker) {
        this.markers[marker.id] = marker;
        this.markerListDirty = true;
        this.numMarkers++;
    }

    markerWorldPosUpdated(marker) {
        if (!this.markers[marker.id]) { // Not added
            return;
        }
        const i = this.markerIndices[marker.id];
        this.positions[i * 3 + 0] = marker.worldPos[0];
        this.positions[i * 3 + 1] = marker.worldPos[1];
        this.positions[i * 3 + 2] = marker.worldPos[2];
        this.positionsDirty = true; // TODO: avoid reallocating VBO each time
    }

    removeMarker(marker) {
        delete this.markers[marker.id];
        this.markerListDirty = true;
        this.numMarkers--;
    }

    update() {
        if (this.markerListDirty) {
            this._buildMarkerList();
            this.markerListDirty = false;
            this.positionsDirty = true;
            this.occlusionTestListDirty = true;
        }
        if (this.positionsDirty) { //////////////  TODO: Don't rebuild this when positions change, very wasteful
            this._buildPositions();
            this.positionsDirty = false;
            this.aabbDirty = true;
            this.vbosDirty = true;
        }
        if (this.aabbDirty) {
            this._buildAABB();
            this.aabbDirty = false;
        }
        if (this.vbosDirty) {
            this._buildVBOs();
            this.vbosDirty = false;
        }
        if (this.occlusionTestListDirty) {
            this._buildOcclusionTestList();
        }
        this._updateActiveSectionPlanes();
    }

    _buildMarkerList() {
        this.numMarkers = 0;
        for (var id in this.markers) {
            if (this.markers.hasOwnProperty(id)) {
                this.markerList[this.numMarkers] = this.markers[id];
                this.markerIndices[id] = this.numMarkers;
                this.numMarkers++;
            }
        }
        this.markerList.length = this.numMarkers;
    }

    _buildPositions() {
        let j = 0;
        for (let i = 0; i < this.numMarkers; i++) {
            if (this.markerList[i]) {
                const marker = this.markerList[i];
                const worldPos = marker.rtcPos;
                this.positions[j++] = worldPos[0];
                this.positions[j++] = worldPos[1];
                this.positions[j++] = worldPos[2];
                this.indices[i] = i;
            }
        }
        this.positions.length = this.numMarkers * 3;
        this.indices.length = this.numMarkers;
    }

    _buildAABB() {
        const aabb = this.aabb;
        math.collapseAABB3(aabb);
        math.expandAABB3Points3(aabb, this.positions);
        const origin = this.origin;
        aabb[0] += origin[0];
        aabb[1] += origin[1];
        aabb[2] += origin[2];
        aabb[3] += origin[0];
        aabb[4] += origin[1];
        aabb[5] += origin[2];
    }

    _buildVBOs() {
        if (this.positionsBuf) {
            if (this.lenPositionsBuf === this.positions.length) { // Just updating buffer elements, don't need to reallocate
                this.positionsBuf.setData(new Float32Array(this.positions)); // Indices don't need updating
                return;
            }
            this.positionsBuf.destroy();
            this.positionsBuf = null;
            this.indicesBuf.destroy();
            this.indicesBuf = null;
        }
        const gl = this.scene.canvas.gl;
        const lenPositions = this.numMarkers * 3;
        const lenIndices = this.numMarkers;
        this.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this.positions), lenPositions, 3, gl.STATIC_DRAW);
        this.indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(this.indices), lenIndices, 1, gl.STATIC_DRAW);
        this.lenPositionsBuf = this.positions.length;
    }

    _buildOcclusionTestList() {
        const canvas = this.scene.canvas;
        const near = this.scene.camera.perspective.near; // Assume near enough to ortho near
        const boundary = canvas.boundary;
        const canvasWidth = boundary[2];
        const canvasHeight = boundary[3];
        let lenPixels = 0;
        this.lenOcclusionTestList = 0;
        for (let i = 0; i < this.numMarkers; i++) {
            const marker = this.markerList[i];
            const viewPos = marker.viewPos;
            if (viewPos[2] > -near) { // Clipped by near plane
                marker._setVisible(false);
                continue;
            }
            const canvasPos = marker.canvasPos;
            const canvasX = canvasPos[0];
            const canvasY = canvasPos[1];
            if ((canvasX + 10) < 0 || (canvasY + 10) < 0 || (canvasX - 10) > canvasWidth || (canvasY - 10) > canvasHeight) {
                marker._setVisible(false);
                continue;
            }
            if (marker.entity && !marker.entity.visible) {
                marker._setVisible(false);
                continue;
            }
            if (marker.occludable) {
                this.occlusionTestList[this.lenOcclusionTestList++] = marker;
                this.pixels[lenPixels++] = canvasX;
                this.pixels[lenPixels++] = canvasY;
                continue;
            }
            marker._setVisible(true);
        }
    }

    _updateActiveSectionPlanes() {
        const sectionPlanes = this.scene._sectionPlanesState.sectionPlanes;
        const numSectionPlanes = sectionPlanes.length;
        if (numSectionPlanes > 0) {
            for (let i = 0; i < numSectionPlanes; i++) {
                const sectionPlane = sectionPlanes[i];
                if (!sectionPlane.active) {
                    this.sectionPlanesActive[i] = false;
                } else {
                    const intersect = math.planeAABB3Intersect(sectionPlane.dir, sectionPlane.dist, this.aabb);
                    const outside = (intersect === -1);
                    if (outside) {
                        this.culledBySectionPlanes = true;
                        return;
                    }
                    const intersecting = (intersect === 0);
                    this.sectionPlanesActive[i] = intersecting;
                }
            }
        }
        this.culledBySectionPlanes = false;
    }

    destroy() {
        this.markers = {};
        this.markerList.length = 0;
        if (this.positionsBuf) {
            this.positionsBuf.destroy();
        }
        if (this.indicesBuf) {
            this.indicesBuf.destroy();
        }
    }
}

const MARKER_COLOR = math.vec3([1.0, 0.0, 0.0]);
const POINT_SIZE = 20;

const tempVec3a$b = math.vec3();

/**
 * Manages occlusion testing. Private member of a Renderer.
 * @private
 */
class OcclusionTester {

    constructor(scene, renderBufferManager) {

        this._scene = scene;

        this._renderBufferManager = renderBufferManager;

        this._occlusionLayers = {};
        this._occlusionLayersList = [];
        this._occlusionLayersListDirty = false;

        this._shaderSource = null;
        this._program = null;

        this._shaderSourceHash = null;

        this._shaderSourceDirty = true;         // Need to build shader source code ?
        this._programDirty = false;             // Need to build shader program ?

        this._markersToOcclusionLayersMap = {};

        this._onCameraViewMatrix = scene.camera.on("viewMatrix", () => {
            this._occlusionTestListDirty = true;
        });

        this._onCameraProjMatrix = scene.camera.on("projMatrix", () => {
            this._occlusionTestListDirty = true;
        });

        this._onCanvasBoundary = scene.canvas.on("boundary", () => {
            this._occlusionTestListDirty = true;
        });
    }

    /**
     * Adds a Marker for occlusion testing.
     * @param marker
     */
    addMarker(marker) {
        const originHash = marker.origin.join();
        let occlusionLayer = this._occlusionLayers[originHash];
        if (!occlusionLayer) {
            occlusionLayer = new OcclusionLayer(this._scene, marker.origin);
            this._occlusionLayers[occlusionLayer.originHash] = occlusionLayer;
            this._occlusionLayersListDirty = true;
        }
        occlusionLayer.addMarker(marker);
        this._markersToOcclusionLayersMap[marker.id] = occlusionLayer;
        this._occlusionTestListDirty = true;
    }

    /**
     * Notifies OcclusionTester that a Marker has updated its World-space position.
     * @param marker
     */
    markerWorldPosUpdated(marker) {
        const occlusionLayer = this._markersToOcclusionLayersMap[marker.id];
        if (!occlusionLayer) {
            return;
        }
        const originHash = marker.origin.join();
        if (originHash !== occlusionLayer.originHash) {
            if (occlusionLayer.numMarkers === 1) {
                occlusionLayer.destroy();
                delete this._occlusionLayers[occlusionLayer.originHash];
                this._occlusionLayersListDirty = true;
            } else {
                occlusionLayer.removeMarker(marker);
            }
            let newOcclusionLayer = this._occlusionLayers[originHash];
            if (!newOcclusionLayer) {
                newOcclusionLayer = new OcclusionLayer(this._scene, marker.origin);
                this._occlusionLayers[originHash] = newOcclusionLayer;
                this._occlusionLayersListDirty = true;
            }
            newOcclusionLayer.addMarker(marker);
            this._markersToOcclusionLayersMap[marker.id] = newOcclusionLayer;
        } else {
            occlusionLayer.markerWorldPosUpdated(marker);
        }
    }

    /**
     * Removes a Marker from occlusion testing.
     * @param marker
     */
    removeMarker(marker) {
        const originHash = marker.origin.join();
        let occlusionLayer = this._occlusionLayers[originHash];
        if (!occlusionLayer) {
            return;
        }
        if (occlusionLayer.numMarkers === 1) {
            occlusionLayer.destroy();
            delete this._occlusionLayers[occlusionLayer.originHash];
            this._occlusionLayersListDirty = true;
        } else {
            occlusionLayer.removeMarker(marker);
        }
        delete this._markersToOcclusionLayersMap[marker.id];
    }

    /**
     * Returns true if an occlusion test is needed.
     *
     * @returns {Boolean}
     */
    get needOcclusionTest() {
        return this._occlusionTestListDirty;
    }

    /**
     * Binds the render buffer. After calling this, the caller then renders object silhouettes to the render buffer,
     * then calls drawMarkers() and doOcclusionTest().
     */
    bindRenderBuf() {

        const shaderSourceHash = [this._scene.canvas.canvas.id, this._scene._sectionPlanesState.getHash()].join(";");

        if (shaderSourceHash !== this._shaderSourceHash) {
            this._shaderSourceHash = shaderSourceHash;
            this._shaderSourceDirty = true;
        }

        if (this._shaderSourceDirty) {
            this._buildShaderSource();
            this._shaderSourceDirty = false;
            this._programDirty = true;
        }

        if (this._programDirty) {
            this._buildProgram();
            this._programDirty = false;
            this._occlusionTestListDirty = true;
        }

        if (this._occlusionLayersListDirty) {
            this._buildOcclusionLayersList();
            this._occlusionLayersListDirty = false;
        }

        if (this._occlusionTestListDirty) {
            for (let i = 0, len = this._occlusionLayersList.length; i < len; i++) {
                const occlusionLayer = this._occlusionLayersList[i];
                occlusionLayer.occlusionTestListDirty = true;
            }
            this._occlusionTestListDirty = false;
        }

        {
            this._readPixelBuf = this._renderBufferManager.getRenderBuffer("occlusionReadPix");
            this._readPixelBuf.bind();
            this._readPixelBuf.clear();
        }
    }

    _buildOcclusionLayersList() {
        let numOcclusionLayers = 0;
        for (let originHash in this._occlusionLayers) {
            if (this._occlusionLayers.hasOwnProperty(originHash)) {
                this._occlusionLayersList[numOcclusionLayers++] = this._occlusionLayers[originHash];
            }
        }
        this._occlusionLayersList.length = numOcclusionLayers;
    }

    _buildShaderSource() {
        this._shaderSource = {
            vertex: this._buildVertexShaderSource(),
            fragment: this._buildFragmentShaderSource()
        };
    }

    _buildVertexShaderSource() {
        const scene = this._scene;
        const clipping = scene._sectionPlanesState.sectionPlanes.length > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// OcclusionTester vertex shader");
        
        src.push("in vec3 position;");
        src.push("uniform mat4 modelMatrix;");
        src.push("uniform mat4 viewMatrix;");
        src.push("uniform mat4 projMatrix;");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("out float vFragDepth;");
        }
        if (clipping) {
            src.push("out vec4 vWorldPosition;");
        }
        src.push("void main(void) {");
        src.push("vec4 worldPosition = vec4(position, 1.0); ");
        src.push("   vec4 viewPosition = viewMatrix * worldPosition;");
        if (clipping) {
            src.push("   vWorldPosition = worldPosition;");
        }
        src.push("   vec4 clipPos = projMatrix * viewPosition;");
        src.push("   gl_PointSize = " + POINT_SIZE + ".0;");
        if (scene.logarithmicDepthBufferEnabled) {
           src.push("vFragDepth = 1.0 + clipPos.w;");
        } else {
            if (scene.markerZOffset < 0.000) {
                src.push("clipPos.z += " + scene.markerZOffset + ";");
            }
        }
        src.push("   gl_Position = clipPos;");
        src.push("}");
        return src;
    }

    _buildFragmentShaderSource() {
        const scene = this._scene;
        const sectionPlanesState = scene._sectionPlanesState;
        const clipping = sectionPlanesState.sectionPlanes.length > 0;
        const src = [];
        src.push("#version 300 es");
        src.push("// OcclusionTester fragment shader");
        
        src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH");
        src.push("precision highp float;");
        src.push("precision highp int;");
        src.push("#else");
        src.push("precision mediump float;");
        src.push("precision mediump int;");
        src.push("#endif");
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("uniform float logDepthBufFC;");
            src.push("in float vFragDepth;");
        }
        if (clipping) {
            src.push("in vec4 vWorldPosition;");
            for (let i = 0; i < sectionPlanesState.sectionPlanes.length; i++) {
                src.push("uniform bool sectionPlaneActive" + i + ";");
                src.push("uniform vec3 sectionPlanePos" + i + ";");
                src.push("uniform vec3 sectionPlaneDir" + i + ";");
            }
        }
        src.push("out vec4 outColor;");
        src.push("void main(void) {");
        if (clipping) {
            src.push("  float dist = 0.0;");
            for (var i = 0; i < sectionPlanesState.sectionPlanes.length; i++) {
                src.push("if (sectionPlaneActive" + i + ") {");
                src.push("   dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
                src.push("}");
            }
            src.push("  if (dist > 0.0) { discard; }");
        }
        if (scene.logarithmicDepthBufferEnabled) {
            src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;");
        }
        src.push("   outColor = vec4(1.0, 0.0, 0.0, 1.0); ");
        src.push("}");
        return src;
    }

    _buildProgram() {
        if (this._program) {
            this._program.destroy();
        }
        const scene = this._scene;
        const gl = scene.canvas.gl;
        const sectionPlanesState = scene._sectionPlanesState;
        this._program = new Program(gl, this._shaderSource);
        if (this._program.errors) {
            this.errors = this._program.errors;
            return;
        }
        const program = this._program;
        this._uViewMatrix = program.getLocation("viewMatrix");
        this._uProjMatrix = program.getLocation("projMatrix");
        this._uSectionPlanes = [];
        const sectionPlanes = sectionPlanesState.sectionPlanes;
        for (let i = 0, len = sectionPlanes.length; i < len; i++) {
            this._uSectionPlanes.push({
                active: program.getLocation("sectionPlaneActive" + i),
                pos: program.getLocation("sectionPlanePos" + i),
                dir: program.getLocation("sectionPlaneDir" + i)
            });
        }
        this._aPosition = program.getAttribute("position");
        if (scene.logarithmicDepthBufferEnabled) {
            this._uLogDepthBufFC = program.getLocation("logDepthBufFC");
        }
    }

    /**
     * Draws {@link Marker}s to the render buffer.
     */
    drawMarkers() {

        const scene = this._scene;
        const gl = scene.canvas.gl;
        const program = this._program;
        const sectionPlanesState = scene._sectionPlanesState;
        const camera = scene.camera;
        const project = scene.camera.project;

        program.bind();

        gl.uniformMatrix4fv(this._uProjMatrix, false, camera._project._state.matrix);

        if (scene.logarithmicDepthBufferEnabled) {
            const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2);
            gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC);
        }

        for (let i = 0, len = this._occlusionLayersList.length; i < len; i++) {

            const occlusionLayer = this._occlusionLayersList[i];

            occlusionLayer.update();

            if (occlusionLayer.culledBySectionPlanes) {
                continue;
            }

            const origin = occlusionLayer.origin;

            gl.uniformMatrix4fv(this._uViewMatrix, false, createRTCViewMat(camera.viewMatrix, origin));

            const numSectionPlanes = sectionPlanesState.sectionPlanes.length;
            if (numSectionPlanes > 0) {
                const sectionPlanes = sectionPlanesState.sectionPlanes;
                for (let sectionPlaneIndex = 0; sectionPlaneIndex < numSectionPlanes; sectionPlaneIndex++) {
                    const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex];
                    if (sectionPlaneUniforms) {
                        const active = occlusionLayer.sectionPlanesActive[sectionPlaneIndex];
                        gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0);
                        if (active) {
                            const sectionPlane = sectionPlanes[sectionPlaneIndex];
                            gl.uniform3fv(sectionPlaneUniforms.pos, getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$b));
                            gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
                        }
                    }
                }
            }

            this._aPosition.bindArrayBuffer(occlusionLayer.positionsBuf);

            const indicesBuf = occlusionLayer.indicesBuf;
            indicesBuf.bind();
            gl.drawElements(gl.POINTS, indicesBuf.numItems, indicesBuf.itemType, 0);
        }
    }

    /**
     * Sets visibilities of {@link Marker}s according to whether or not they are obscured by anything in the render buffer.
     */
    doOcclusionTest() {

        {

            const resolutionScale = this._scene.canvas.resolutionScale;

            const markerR = MARKER_COLOR[0] * 255;
            const markerG = MARKER_COLOR[1] * 255;
            const markerB = MARKER_COLOR[2] * 255;

            for (let i = 0, len = this._occlusionLayersList.length; i < len; i++) {

                const occlusionLayer = this._occlusionLayersList[i];

                for (let i = 0; i < occlusionLayer.lenOcclusionTestList; i++) {

                    const marker = occlusionLayer.occlusionTestList[i];
                    const j = i * 2;
                    const color = this._readPixelBuf.read(Math.round(occlusionLayer.pixels[j] * resolutionScale), Math.round(occlusionLayer.pixels[j + 1] * resolutionScale));
                    const visible = (color[0] === markerR) && (color[1] === markerG) && (color[2] === markerB);

                    marker._setVisible(visible);
                }
            }
        }
    }

    /**
     * Unbinds render buffer.
     */
    unbindRenderBuf() {
        {
            this._readPixelBuf.unbind();
        }
    }

    /**
     * Destroys this OcclusionTester.
     */
    destroy() {
        if (this.destroyed) {
            return;
        }
        for (let i = 0, len = this._occlusionLayersList.length; i < len; i++) {
            const occlusionLayer = this._occlusionLayersList[i];
            occlusionLayer.destroy();
        }

        if (this._program) {
            this._program.destroy();
        }

        this._scene.camera.off(this._onCameraViewMatrix);
        this._scene.camera.off(this._onCameraProjMatrix);
        this._scene.canvas.off(this._onCanvasBoundary);
        this.destroyed = true;
    }
}

const tempVec2 = math.vec2();

/**
 * SAO implementation inspired from previous SAO work in THREE.js by ludobaka / ludobaka.github.io and bhouston
 * @private
 */
class SAOOcclusionRenderer {

    constructor(scene) {

        this._scene = scene;

        this._numSamples = null;

        // The program

        this._program = null;
        this._programError = false;

        // Variable locations

        this._aPosition = null;
        this._aUV = null;

        this._uDepthTexture = "uDepthTexture";

        this._uCameraNear = null;
        this._uCameraFar = null;
        this._uCameraProjectionMatrix = null;
        this._uCameraInverseProjectionMatrix = null;

        this._uScale = null;
        this._uIntensity = null;
        this._uBias = null;
        this._uKernelRadius = null;
        this._uMinResolution = null;
        this._uRandomSeed = null;

        // VBOs

        this._uvBuf = null;
        this._positionsBuf = null;
        this._indicesBuf = null;

        this.init();
    }

    init() {
        this._rebuild = true;
    }

    render(depthRenderBuffer) {

        this._build();

        if (this._programError) {
            return;
        }

        if (!this._getInverseProjectMat) { // HACK: scene.camera not defined until render time
            this._getInverseProjectMat = (() => {
                let projMatDirty = true;
                this._scene.camera.on("projMatrix", function () {
                    projMatDirty = true;
                });
                const inverseProjectMat = math.mat4();
                return () => {
                    if (projMatDirty) {
                        math.inverseMat4(scene.camera.projMatrix, inverseProjectMat);
                    }
                    return inverseProjectMat;
                }
            })();
        }

        const gl = this._scene.canvas.gl;
        const program = this._program;
        const scene = this._scene;
        const sao = scene.sao;
        const viewportWidth = gl.drawingBufferWidth;
        const viewportHeight = gl.drawingBufferHeight;
        const projectState = scene.camera.project._state;
        const near = projectState.near;
        const far = projectState.far;
        const projectionMatrix = projectState.matrix;
        const inverseProjectionMatrix = this._getInverseProjectMat();
        const randomSeed = Math.random();
        const perspective = (scene.camera.projection === "perspective");

        tempVec2[0] = viewportWidth;
        tempVec2[1] = viewportHeight;

        gl.viewport(0, 0, viewportWidth, viewportHeight);
        gl.clearColor(0, 0, 0, 1);
        gl.disable(gl.DEPTH_TEST);
        gl.disable(gl.BLEND);
        gl.frontFace(gl.CCW);
        gl.clear(gl.COLOR_BUFFER_BIT);

        program.bind();

        gl.uniform1f(this._uCameraNear, near);
        gl.uniform1f(this._uCameraFar, far);

        gl.uniformMatrix4fv(this._uCameraProjectionMatrix, false, projectionMatrix);
        gl.uniformMatrix4fv(this._uCameraInverseProjectionMatrix, false, inverseProjectionMatrix);

        gl.uniform1i(this._uPerspective, perspective);

        gl.uniform1f(this._uScale, sao.scale * (far / 5));
        gl.uniform1f(this._uIntensity, sao.intensity);
        gl.uniform1f(this._uBias, sao.bias);
        gl.uniform1f(this._uKernelRadius, sao.kernelRadius);
        gl.uniform1f(this._uMinResolution, sao.minResolution);
        gl.uniform2fv(this._uViewport, tempVec2);
        gl.uniform1f(this._uRandomSeed, randomSeed);

        const depthTexture = depthRenderBuffer.getDepthTexture();

        program.bindTexture(this._uDepthTexture, depthTexture, 0);

        this._aUV.bindArrayBuffer(this._uvBuf);
        this._aPosition.bindArrayBuffer(this._positionsBuf);
        this._indicesBuf.bind();

        gl.drawElements(gl.TRIANGLES, this._indicesBuf.numItems, this._indicesBuf.itemType, 0);
    }

    _build() {

        let dirty = false;

        const sao = this._scene.sao;

        if (sao.numSamples !== this._numSamples) {
            this._numSamples = Math.floor(sao.numSamples);
            dirty = true;
        }

        if (! (dirty || this._rebuild)) {
            return;
        }

        this._rebuild = false;

        const gl = this._scene.canvas.gl;

        if (this._program) {
            this._program.destroy();
            this._program = null;
        }

        this._program = new Program(gl, {

            vertex: [`#version 300 es
                    precision highp float;
                    precision highp int;
                    
                    in vec3 aPosition;
                    in vec2 aUV;            
                    
                    out vec2 vUV;
                    
                    void main () {
                        gl_Position = vec4(aPosition, 1.0);
                        vUV = aUV;
                    }`],

            fragment: [
                `#version 300 es      
                precision highp float;
                precision highp int;           
                
                #define NORMAL_TEXTURE 0
                #define PI 3.14159265359
                #define PI2 6.28318530718
                #define EPSILON 1e-6
                #define NUM_SAMPLES ${this._numSamples}
                #define NUM_RINGS 4              
            
                in vec2        vUV;
            
                uniform sampler2D   uDepthTexture;
               
                uniform float       uCameraNear;
                uniform float       uCameraFar;
                uniform mat4        uProjectMatrix;
                uniform mat4        uInverseProjectMatrix;
                
                uniform bool        uPerspective;

                uniform float       uScale;
                uniform float       uIntensity;
                uniform float       uBias;
                uniform float       uKernelRadius;
                uniform float       uMinResolution;
                uniform vec2        uViewport;
                uniform float       uRandomSeed;

                float pow2( const in float x ) { return x*x; }
                
                highp float rand( const in vec2 uv ) {
                    const highp float a = 12.9898, b = 78.233, c = 43758.5453;
                    highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );
                    return fract(sin(sn) * c);
                }

                vec3 packNormalToRGB( const in vec3 normal ) {
                    return normalize( normal ) * 0.5 + 0.5;
                }

                vec3 unpackRGBToNormal( const in vec3 rgb ) {
                    return 2.0 * rgb.xyz - 1.0;
                }

                const float packUpscale = 256. / 255.;
                const float unpackDownScale = 255. / 256.; 

                const vec3 packFactors = vec3( 256. * 256. * 256., 256. * 256.,  256. );
                const vec4 unPackFactors = unpackDownScale / vec4( packFactors, 1. );   

                const float shiftRights = 1. / 256.;

                vec4 packFloatToRGBA( const in float v ) {
                    vec4 r = vec4( fract( v * packFactors ), v );
                    r.yzw -= r.xyz * shiftRights; 
                    return r * packUpscale;
                }

                float unpackRGBAToFloat( const in vec4 v ) {                   
                    return dot( floor( v * 255.0 + 0.5 ) / 255.0, unPackFactors );
                }
                
                float perspectiveDepthToViewZ( const in float invClipZ, const in float near, const in float far ) {
                    return ( near * far ) / ( ( far - near ) * invClipZ - far );
                }

                float orthographicDepthToViewZ( const in float linearClipZ, const in float near, const in float far ) {
                    return linearClipZ * ( near - far ) - near;
                }
                
                float getDepth( const in vec2 screenPosition ) {
                    return vec4(texture(uDepthTexture, screenPosition)).r;
                }

                float getViewZ( const in float depth ) {
                     if (uPerspective) {
                         return perspectiveDepthToViewZ( depth, uCameraNear, uCameraFar );
                     } else {
                        return orthographicDepthToViewZ( depth, uCameraNear, uCameraFar );
                     }
                }

                vec3 getViewPos( const in vec2 screenPos, const in float depth, const in float viewZ ) {
                	float clipW = uProjectMatrix[2][3] * viewZ + uProjectMatrix[3][3];
                	vec4 clipPosition = vec4( ( vec3( screenPos, depth ) - 0.5 ) * 2.0, 1.0 );
                	clipPosition *= clipW; 
                	return ( uInverseProjectMatrix * clipPosition ).xyz;
                }

                vec3 getViewNormal( const in vec3 viewPosition, const in vec2 screenPos ) {               
                    return normalize( cross( dFdx( viewPosition ), dFdy( viewPosition ) ) );
                }

                float scaleDividedByCameraFar;
                float minResolutionMultipliedByCameraFar;

                float getOcclusion( const in vec3 centerViewPosition, const in vec3 centerViewNormal, const in vec3 sampleViewPosition ) {
                	vec3 viewDelta = sampleViewPosition - centerViewPosition;
                	float viewDistance = length( viewDelta );
                	float scaledScreenDistance = scaleDividedByCameraFar * viewDistance;
                	return max(0.0, (dot(centerViewNormal, viewDelta) - minResolutionMultipliedByCameraFar) / scaledScreenDistance - uBias) / (1.0 + pow2( scaledScreenDistance ) );
                }

                const float ANGLE_STEP = PI2 * float( NUM_RINGS ) / float( NUM_SAMPLES );
                const float INV_NUM_SAMPLES = 1.0 / float( NUM_SAMPLES );

                float getAmbientOcclusion( const in vec3 centerViewPosition ) {
            
                	scaleDividedByCameraFar = uScale / uCameraFar;
                	minResolutionMultipliedByCameraFar = uMinResolution * uCameraFar;
                	vec3 centerViewNormal = getViewNormal( centerViewPosition, vUV );

                	float angle = rand( vUV + uRandomSeed ) * PI2;
                	vec2 radius = vec2( uKernelRadius * INV_NUM_SAMPLES ) / uViewport;
                	vec2 radiusStep = radius;

                	float occlusionSum = 0.0;
                	float weightSum = 0.0;

                	for( int i = 0; i < NUM_SAMPLES; i ++ ) {
                		vec2 sampleUv = vUV + vec2( cos( angle ), sin( angle ) ) * radius;
                		radius += radiusStep;
                		angle += ANGLE_STEP;

                		float sampleDepth = getDepth( sampleUv );
                		if( sampleDepth >= ( 1.0 - EPSILON ) ) {
                			continue;
                		}

                		float sampleViewZ = getViewZ( sampleDepth );
                		vec3 sampleViewPosition = getViewPos( sampleUv, sampleDepth, sampleViewZ );
                		occlusionSum += getOcclusion( centerViewPosition, centerViewNormal, sampleViewPosition );
                		weightSum += 1.0;
                	}

                	if( weightSum == 0.0 ) discard;

                	return occlusionSum * ( uIntensity / weightSum );
                }

                out vec4 outColor;
   
                void main() {
                
                	float centerDepth = getDepth( vUV );
                	
                	if( centerDepth >= ( 1.0 - EPSILON ) ) {
                		discard;
                	}

                	float centerViewZ = getViewZ( centerDepth );
                	vec3 viewPosition = getViewPos( vUV, centerDepth, centerViewZ );

                	float ambientOcclusion = getAmbientOcclusion( viewPosition );
                
                	outColor = packFloatToRGBA(  1.0- ambientOcclusion );
                }`]
        });

        if (this._program.errors) {
            console.error(this._program.errors.join("\n"));
            this._programError = true;
            return;
        }

        const uv = new Float32Array([1, 1, 0, 1, 0, 0, 1, 0]);
        const positions = new Float32Array([1, 1, 0, -1, 1, 0, -1, -1, 0, 1, -1, 0]);
        
        // Mitigation: if Uint8Array is used, the geometry is corrupted on OSX when using Chrome with data-textures
        const indices = new Uint32Array([0, 1, 2, 0, 2, 3]);

        this._positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, positions, positions.length, 3, gl.STATIC_DRAW);
        this._uvBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, uv, uv.length, 2, gl.STATIC_DRAW);
        this._indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, indices, indices.length, 1, gl.STATIC_DRAW);

        this._program.bind();

        this._uCameraNear = this._program.getLocation("uCameraNear");
        this._uCameraFar = this._program.getLocation("uCameraFar");

        this._uCameraProjectionMatrix = this._program.getLocation("uProjectMatrix");
        this._uCameraInverseProjectionMatrix = this._program.getLocation("uInverseProjectMatrix");

        this._uPerspective = this._program.getLocation("uPerspective");

        this._uScale = this._program.getLocation("uScale");
        this._uIntensity = this._program.getLocation("uIntensity");
        this._uBias = this._program.getLocation("uBias");
        this._uKernelRadius = this._program.getLocation("uKernelRadius");
        this._uMinResolution = this._program.getLocation("uMinResolution");
        this._uViewport = this._program.getLocation("uViewport");
        this._uRandomSeed = this._program.getLocation("uRandomSeed");

        this._aPosition = this._program.getAttribute("aPosition");
        this._aUV = this._program.getAttribute("aUV");

        this._dirty = false;
    }

    destroy() {
        if (this._program) {
            this._program.destroy();
            this._program = null;
        }
    }
}

const blurStdDev = 4;
const blurDepthCutoff = 0.01;
const KERNEL_RADIUS = 16;

const sampleOffsetsVert = new Float32Array(createSampleOffsets(KERNEL_RADIUS + 1, [0, 1]));
const sampleOffsetsHor = new Float32Array(createSampleOffsets(KERNEL_RADIUS + 1, [1, 0]));
const sampleWeights = new Float32Array(createSampleWeights(KERNEL_RADIUS + 1, blurStdDev));

const tempVec2a$1 = new Float32Array(2);

/**
 * SAO implementation inspired from previous SAO work in THREE.js by ludobaka / ludobaka.github.io and bhouston
 * @private
 */
class SAODepthLimitedBlurRenderer {

    constructor(scene) {

        this._scene = scene;

        // The program

        this._program = null;
        this._programError = false;

        // Variable locations

        this._aPosition = null;
        this._aUV = null;

        this._uDepthTexture = "uDepthTexture";
        this._uOcclusionTexture = "uOcclusionTexture";

        this._uViewport = null;
        this._uCameraNear = null;
        this._uCameraFar = null;
        this._uCameraProjectionMatrix = null;
        this._uCameraInverseProjectionMatrix = null;

        // VBOs

        this._uvBuf = null;
        this._positionsBuf = null;
        this._indicesBuf = null;

        this.init();
    }

    init() {

        // Create program & VBOs, locate attributes and uniforms

        const gl = this._scene.canvas.gl;

        this._program = new Program(gl, {

            vertex: [
                `#version 300 es
                precision highp float;
                precision highp int;
                    
                in vec3 aPosition;
                in vec2 aUV;
                uniform vec2 uViewport;
                out vec2 vUV;
                out vec2 vInvSize;
                void main () {
                    vUV = aUV;
                    vInvSize = 1.0 / uViewport;
                    gl_Position = vec4(aPosition, 1.0);
                }`],

            fragment: [
                `#version 300 es
                precision highp float;
                precision highp int;
                    
                #define PI 3.14159265359
                #define PI2 6.28318530718
                #define EPSILON 1e-6

                #define KERNEL_RADIUS ${KERNEL_RADIUS}

                in vec2        vUV;
                in vec2        vInvSize;
            
                uniform sampler2D   uDepthTexture;
                uniform sampler2D   uOcclusionTexture;              
               
                uniform float       uCameraNear;
                uniform float       uCameraFar;               
                uniform float       uDepthCutoff;

                uniform vec2        uSampleOffsets[ KERNEL_RADIUS + 1 ];
                uniform float       uSampleWeights[ KERNEL_RADIUS + 1 ];

                const float         unpackDownscale = 255. / 256.; 

                const vec3          packFactors = vec3( 256. * 256. * 256., 256. * 256.,  256. );
                const vec4          unpackFactors = unpackDownscale / vec4( packFactors, 1. );   

                const float packUpscale = 256. / 255.;
       
                const float shiftRights = 1. / 256.;
                
                float unpackRGBAToFloat( const in vec4 v ) {
                    return dot( floor( v * 255.0 + 0.5 ) / 255.0, unpackFactors );
                }               

                vec4 packFloatToRGBA( const in float v ) {
                    vec4 r = vec4( fract( v * packFactors ), v );
                    r.yzw -= r.xyz * shiftRights; 
                    return r * packUpscale;
                }

                float viewZToOrthographicDepth( const in float viewZ) {
                    return ( viewZ + uCameraNear ) / ( uCameraNear - uCameraFar );
                }
              
                float orthographicDepthToViewZ( const in float linearClipZ) {
                    return linearClipZ * ( uCameraNear - uCameraFar ) - uCameraNear;
                }

                float viewZToPerspectiveDepth( const in float viewZ) {
                    return (( uCameraNear + viewZ ) * uCameraFar ) / (( uCameraFar - uCameraNear ) * viewZ );
                }
                
                float perspectiveDepthToViewZ( const in float invClipZ) {
                    return ( uCameraNear * uCameraFar ) / ( ( uCameraFar - uCameraNear ) * invClipZ - uCameraFar );
                }

                float getDepth( const in vec2 screenPosition ) {
                    return vec4(texture(uDepthTexture, screenPosition)).r;
                }

                float getViewZ( const in float depth ) {
                     return perspectiveDepthToViewZ( depth );
                }

                out vec4 outColor;
        
                void main() {
                
                    float depth = getDepth( vUV );
                    if( depth >= ( 1.0 - EPSILON ) ) {
                        discard;
                    }

                    float centerViewZ = -getViewZ( depth );
                    bool rBreak = false;
                    bool lBreak = false;

                    float weightSum = uSampleWeights[0];
                    float occlusionSum = unpackRGBAToFloat(texture( uOcclusionTexture, vUV )) * weightSum;

                    for( int i = 1; i <= KERNEL_RADIUS; i ++ ) {

                        float sampleWeight = uSampleWeights[i];
                        vec2 sampleUVOffset = uSampleOffsets[i] * vInvSize;

                        vec2 sampleUV = vUV + sampleUVOffset;
                        float viewZ = -getViewZ( getDepth( sampleUV ) );

                        if( abs( viewZ - centerViewZ ) > uDepthCutoff ) {
                            rBreak = true;
                        }

                        if( ! rBreak ) {
                            occlusionSum += unpackRGBAToFloat(texture( uOcclusionTexture, sampleUV )) * sampleWeight;
                            weightSum += sampleWeight;
                        }

                        sampleUV = vUV - sampleUVOffset;
                        viewZ = -getViewZ( getDepth( sampleUV ) );

                        if( abs( viewZ - centerViewZ ) > uDepthCutoff ) {
                            lBreak = true;
                        }

                        if( ! lBreak ) {
                            occlusionSum += unpackRGBAToFloat(texture( uOcclusionTexture, sampleUV )) * sampleWeight;
                            weightSum += sampleWeight;
                        }
                    }

                    outColor = packFloatToRGBA(occlusionSum / weightSum);
                }`
            ]
        });

        if (this._program.errors) {
            console.error(this._program.errors.join("\n"));
            this._programError = true;
            return;
        }

        const uv = new Float32Array([1, 1, 0, 1, 0, 0, 1, 0]);
        const positions = new Float32Array([1, 1, 0, -1, 1, 0, -1, -1, 0, 1, -1, 0]);

        // Mitigation: if Uint8Array is used, the geometry is corrupted on OSX when using Chrome with data-textures
        const indices = new Uint32Array([0, 1, 2, 0, 2, 3]);

        this._positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, positions, positions.length, 3, gl.STATIC_DRAW);
        this._uvBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, uv, uv.length, 2, gl.STATIC_DRAW);
        this._indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, indices, indices.length, 1, gl.STATIC_DRAW);

        this._program.bind();

        this._uViewport = this._program.getLocation("uViewport");

        this._uCameraNear = this._program.getLocation("uCameraNear");
        this._uCameraFar = this._program.getLocation("uCameraFar");

        this._uDepthCutoff = this._program.getLocation("uDepthCutoff");

        this._uSampleOffsets = gl.getUniformLocation(this._program.handle, "uSampleOffsets");
        this._uSampleWeights = gl.getUniformLocation(this._program.handle, "uSampleWeights");

        this._aPosition = this._program.getAttribute("aPosition");
        this._aUV = this._program.getAttribute("aUV");
    }

    render(depthRenderBuffer, occlusionRenderBuffer, direction) {

        if (this._programError) {
            return;
        }

        if (!this._getInverseProjectMat) { // HACK: scene.camera not defined until render time
            this._getInverseProjectMat = (() => {
                let projMatDirty = true;
                this._scene.camera.on("projMatrix", function () {
                    projMatDirty = true;
                });
                const inverseProjectMat = math.mat4();
                return () => {
                    if (projMatDirty) {
                        math.inverseMat4(scene.camera.projMatrix, inverseProjectMat);
                    }
                    return inverseProjectMat;
                }
            })();
        }

        const gl = this._scene.canvas.gl;
        const program = this._program;
        const scene = this._scene;
        const viewportWidth = gl.drawingBufferWidth;
        const viewportHeight = gl.drawingBufferHeight;
        const projectState = scene.camera.project._state;
        const near = projectState.near;
        const far = projectState.far;

        gl.viewport(0, 0, viewportWidth, viewportHeight);
        gl.clearColor(0, 0, 0, 1);
        gl.enable(gl.DEPTH_TEST);
        gl.disable(gl.BLEND);
        gl.frontFace(gl.CCW);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        program.bind();

        tempVec2a$1[0] = viewportWidth;
        tempVec2a$1[1] = viewportHeight;

        gl.uniform2fv(this._uViewport, tempVec2a$1);
        gl.uniform1f(this._uCameraNear, near);
        gl.uniform1f(this._uCameraFar, far);

        gl.uniform1f(this._uDepthCutoff, blurDepthCutoff);

        if (direction === 0) {// Horizontal
            gl.uniform2fv(this._uSampleOffsets, sampleOffsetsHor);
        } else { // Vertical
            gl.uniform2fv(this._uSampleOffsets, sampleOffsetsVert);
        }

        gl.uniform1fv(this._uSampleWeights, sampleWeights);

        const depthTexture = depthRenderBuffer.getDepthTexture();
        const occlusionTexture = occlusionRenderBuffer.getTexture();

        program.bindTexture(this._uDepthTexture, depthTexture, 0); // TODO: use FrameCtx.textureUnit
        program.bindTexture(this._uOcclusionTexture, occlusionTexture, 1);

        this._aUV.bindArrayBuffer(this._uvBuf);
        this._aPosition.bindArrayBuffer(this._positionsBuf);
        this._indicesBuf.bind();

        gl.drawElements(gl.TRIANGLES, this._indicesBuf.numItems, this._indicesBuf.itemType, 0);
    }

    destroy() {
        this._program.destroy();
    }
}

function createSampleWeights(kernelRadius, stdDev) {
    const weights = [];
    for (let i = 0; i <= kernelRadius; i++) {
        weights.push(gaussian(i, stdDev));
    }
    return weights; // TODO: Optimize
}

function gaussian(x, stdDev) {
    return Math.exp(-(x * x) / (2.0 * (stdDev * stdDev))) / (Math.sqrt(2.0 * Math.PI) * stdDev);
}

function createSampleOffsets(kernelRadius, uvIncrement) {
    const offsets = [];
    for (let i = 0; i <= kernelRadius; i++) {
        offsets.push(uvIncrement[0] * i);
        offsets.push(uvIncrement[1] * i);
    }
    return offsets;
}

/**
 * @private
 */
class RenderBufferManager {

    constructor(scene) {
        this.scene = scene;
        this._renderBuffersBasic = {};
        this._renderBuffersScaled = {};
    }

    getRenderBuffer(id, options) {
        const renderBuffers = (this.scene.canvas.resolutionScale === 1.0) ? this._renderBuffersBasic : this._renderBuffersScaled;
        let renderBuffer = renderBuffers[id];
        if (!renderBuffer) {
            renderBuffer = new RenderBuffer(this.scene.canvas.canvas, this.scene.canvas.gl, options);
            renderBuffers[id] = renderBuffer;
        }
        return renderBuffer;
    }

    destroy() {
        for (let id in this._renderBuffersBasic) {
            this._renderBuffersBasic[id].destroy();
        }
        for (let id in this._renderBuffersScaled) {
            this._renderBuffersScaled[id].destroy();
        }
    }
}

const vec3_0 = math.vec3([0,0,0]);

/**
 * @private
 */
const Renderer$1 = function (scene, options) {

    options = options || {};

    const frameCtx = new FrameContext(scene);
    const canvas = scene.canvas.canvas;
    /**
     * @type {WebGL2RenderingContext}
     */
    const gl = scene.canvas.gl;
    const canvasTransparent = (!!options.transparent);
    const alphaDepthMask = options.alphaDepthMask;

    const pickIDs = new Map$1$1({});

    let drawableTypeInfo = {};
    let drawables = {};

    let postSortDrawableList = [];
    let postCullDrawableList = [];
    let uiDrawableList       = [];

    let drawableListDirty = true;
    let stateSortDirty = true;
    let imageDirty = true;

    let transparentEnabled = true;
    let edgesEnabled = true;
    let saoEnabled = true;
    let pbrEnabled = true;
    let colorTextureEnabled = true;

    const renderBufferManager = new RenderBufferManager(scene);

    let snapshotBound = false;

    const saoOcclusionRenderer = new SAOOcclusionRenderer(scene);
    const saoDepthLimitedBlurRenderer = new SAODepthLimitedBlurRenderer(scene);

    this.scene = scene;

    this._occlusionTester = null; // Lazy-created in #addMarker()

    this.capabilities = {
        astcSupported: !!getExtension(gl, 'WEBGL_compressed_texture_astc'),
        etc1Supported: true, // WebGL2
        etc2Supported: !!getExtension(gl, 'WEBGL_compressed_texture_etc'),
        dxtSupported: !!getExtension(gl, 'WEBGL_compressed_texture_s3tc'),
        bptcSupported: !!getExtension(gl, 'EXT_texture_compression_bptc'),
        pvrtcSupported: !!(getExtension(gl, 'WEBGL_compressed_texture_pvrtc') || getExtension(gl, 'WEBKIT_WEBGL_compressed_texture_pvrtc'))
    };

    this.setTransparentEnabled = function (enabled) {
        transparentEnabled = enabled;
        imageDirty = true;
    };

    this.setEdgesEnabled = function (enabled) {
        edgesEnabled = enabled;
        imageDirty = true;
    };

    this.setSAOEnabled = function (enabled) {
        saoEnabled = enabled;
        imageDirty = true;
    };

    this.setPBREnabled = function (enabled) {
        pbrEnabled = enabled;
        imageDirty = true;
    };

    this.setColorTextureEnabled = function (enabled) {
        colorTextureEnabled = enabled;
        imageDirty = true;
    };

    this.needStateSort = function () {
        stateSortDirty = true;
    };

    this.shadowsDirty = function () {
    };

    this.imageDirty = function () {
        imageDirty = true;
    };

    this.webglContextLost = function () {
    };

    this.webglContextRestored = function (gl) {

        // renderBufferManager.webglContextRestored(gl);

        saoOcclusionRenderer.init();
        saoDepthLimitedBlurRenderer.init();

        imageDirty = true;
    };

    /**
     * Inserts a drawable into this renderer.
     *  @private
     */
    this.addDrawable = function (id, drawable) {
        const type = drawable.type;
        if (!type) {
            console.error("Renderer#addDrawable() : drawable with ID " + id + " has no 'type' - ignoring");
            return;
        }
        let drawableInfo = drawableTypeInfo[type];
        if (!drawableInfo) {
            drawableInfo = {
                type: drawable.type,
                count: 0,
                isStateSortable: drawable.isStateSortable,
                stateSortCompare: drawable.stateSortCompare,
                drawableMap: {},
                drawableListPreCull: [],
                drawableList: []
            };
            drawableTypeInfo[type] = drawableInfo;
        }
        drawableInfo.count++;
        drawableInfo.drawableMap[id] = drawable;
        drawables[id] = drawable;
        drawableListDirty = true;
    };

    /**
     * Removes a drawable from this renderer.
     *  @private
     */
    this.removeDrawable = function (id) {
        const drawable = drawables[id];
        if (!drawable) {
            console.error("Renderer#removeDrawable() : drawable not found with ID " + id + " - ignoring");
            return;
        }
        const type = drawable.type;
        const drawableInfo = drawableTypeInfo[type];
        if (--drawableInfo.count <= 0) {
            delete drawableTypeInfo[type];
        } else {
            delete drawableInfo.drawableMap[id];
        }
        delete drawables[id];
        drawableListDirty = true;
    };

    /**
     * Gets a unique pick ID for the given Pickable. A Pickable can be a {@link Mesh} or a {@link PerformanceMesh}.
     * @returns {Number} New pick ID.
     */
    this.getPickID = function (entity) {
        return pickIDs.addItem(entity);
    };

    /**
     * Released a pick ID for reuse.
     * @param {Number} pickID Pick ID to release.
     */
    this.putPickID = function (pickID) {
        pickIDs.removeItem(pickID);
    };

    /**
     * Clears the canvas.
     *  @private
     */
    this.clear = function (params) {
        gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
        if (canvasTransparent) {
            gl.clearColor(1, 1, 1, 1);
        } else {
            const backgroundColor = scene.canvas.backgroundColorFromAmbientLight ? this.lights.getAmbientColorAndIntensity() : scene.canvas.backgroundColor;
            gl.clearColor(backgroundColor[0], backgroundColor[1], backgroundColor[2], 1.0);
        }
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    };

    /**
     * Returns true if the next call to render() will draw something
     * @returns {Boolean}
     */
    this.needsRender = function () {
        const needsRender = (imageDirty || drawableListDirty || stateSortDirty);
        return needsRender;
    };

    /**
     * Renders inserted drawables.
     *  @private
     */
    this.render = function (params) {
        params = params || {};
        if (params.force) {
            imageDirty = true;
        }
        updateDrawlist();
        if (imageDirty) {
            draw(params);
            stats.frame.frameCount++;
            imageDirty = false;
        }
    };

    function updateDrawlist() { // Prepares state-sorted array of drawables from maps of inserted drawables
        if (drawableListDirty) {
            buildDrawableList();
            drawableListDirty = false;
            stateSortDirty = true;
        }
        if (stateSortDirty) {
            sortDrawableList();
            stateSortDirty = false;
            imageDirty = true;
        }
        if (imageDirty) { // Image is usually dirty because the camera moved
            cullDrawableList();
        }
    }

    function buildDrawableList() {
        for (let type in drawableTypeInfo) {
            if (drawableTypeInfo.hasOwnProperty(type)) {
                const drawableInfo = drawableTypeInfo[type];
                const drawableMap = drawableInfo.drawableMap;
                const drawableListPreCull = drawableInfo.drawableListPreCull;
                let lenDrawableList = 0;
                for (let id in drawableMap) {
                    if (drawableMap.hasOwnProperty(id)) {
                        drawableListPreCull[lenDrawableList++] = drawableMap[id];
                    }
                }
                drawableListPreCull.length = lenDrawableList;
            }
        }
    }

    function sortDrawableList() {
        let lenDrawableList = 0;
        for (let type in drawableTypeInfo) {
            if (drawableTypeInfo.hasOwnProperty(type)) {
                const drawableInfo = drawableTypeInfo[type];
                const drawableListPreCull = drawableInfo.drawableListPreCull;
                for (let i = 0, len = drawableListPreCull.length; i < len; i++) {
                    const drawable = drawableListPreCull[i];
                    postSortDrawableList[lenDrawableList++] = drawable;
                }
            }
        }
        postSortDrawableList.length = lenDrawableList;
        postSortDrawableList.sort((a, b) => {
            return a.renderOrder - b.renderOrder;
        });
    }

    function cullDrawableList() {
        let lenDrawableList = 0;
        let lenUiList       = 0;
        for (let i = 0, len = postSortDrawableList.length; i < len; i++) {
            const drawable = postSortDrawableList[i];
            drawable.rebuildRenderFlags();
            if (!drawable.renderFlags.culled) {
                if (drawable.isUI) {
                    uiDrawableList[lenUiList++] = drawable;
                } else {
                    postCullDrawableList[lenDrawableList++] = drawable;
                }
            }
        }
        postCullDrawableList.length = lenDrawableList;
        uiDrawableList.length       = lenUiList;
    }

    function draw(params) {

        const sao = scene.sao;

        if (saoEnabled && sao.possible) {
            drawSAOBuffers(params);
        }

        drawShadowMaps();

        drawColor(params);
    }

    function drawSAOBuffers(params) {

        const sao = scene.sao;

        // Render depth buffer

        const saoDepthRenderBuffer = renderBufferManager.getRenderBuffer("saoDepth", {
            depthTexture: true
        });

        saoDepthRenderBuffer.bind();
        saoDepthRenderBuffer.clear();
        drawDepth(params);
        saoDepthRenderBuffer.unbind();

        // Render occlusion buffer

        const occlusionRenderBuffer1 = renderBufferManager.getRenderBuffer("saoOcclusion");

        occlusionRenderBuffer1.bind();
        occlusionRenderBuffer1.clear();
        saoOcclusionRenderer.render(saoDepthRenderBuffer);
        occlusionRenderBuffer1.unbind();

        if (sao.blur) {

            // Horizontally blur occlusion buffer 1 into occlusion buffer 2

            const occlusionRenderBuffer2 = renderBufferManager.getRenderBuffer("saoOcclusion2");

            occlusionRenderBuffer2.bind();
            occlusionRenderBuffer2.clear();
            saoDepthLimitedBlurRenderer.render(saoDepthRenderBuffer, occlusionRenderBuffer1, 0);
            occlusionRenderBuffer2.unbind();

            // Vertically blur occlusion buffer 2 back into occlusion buffer 1

            occlusionRenderBuffer1.bind();
            occlusionRenderBuffer1.clear();
            saoDepthLimitedBlurRenderer.render(saoDepthRenderBuffer, occlusionRenderBuffer2, 1);
            occlusionRenderBuffer1.unbind();
        }
    }

    function drawDepth(params) {

        frameCtx.reset();
        frameCtx.pass = params.pass;

        gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);

        gl.clearColor(0, 0, 0, 0);
        gl.enable(gl.DEPTH_TEST);
        gl.frontFace(gl.CCW);
        gl.enable(gl.CULL_FACE);
        gl.depthMask(true);

        if (params.clear !== false) {
            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        }
        for (let i = 0, len = postCullDrawableList.length; i < len; i++) {

            const drawable = postCullDrawableList[i];

            if (drawable.culled === true || drawable.visible === false || !drawable.drawDepth || !drawable.saoEnabled) {
                continue;
            }

            if (drawable.renderFlags.colorOpaque) {
                drawable.drawDepth(frameCtx);
            }
        }

        // const numVertexAttribs = WEBGL_INFO.MAX_VERTEX_ATTRIBS; // Fixes https://github.com/xeokit/xeokit-sdk/issues/174
        // for (let ii = 0; ii < numVertexAttribs; ii++) {
        //     gl.disableVertexAttribArray(ii);
        // }

    }

    function drawShadowMaps() {

        let lights = scene._lightsState.lights;

        for (let i = 0, len = lights.length; i < len; i++) {
            const light = lights[i];
            if (!light.castsShadow) {
                continue;
            }
            drawShadowMap(light);
        }
    }

    function drawShadowMap(light) {

        const castsShadow = light.castsShadow;

        if (!castsShadow) {
            return;
        }

        const shadowRenderBuf = light.getShadowRenderBuf();

        if (!shadowRenderBuf) {
            return;
        }

        shadowRenderBuf.bind();

        frameCtx.reset();

        frameCtx.backfaces = true;
        frameCtx.frontface = true;
        frameCtx.shadowViewMatrix = light.getShadowViewMatrix();
        frameCtx.shadowProjMatrix = light.getShadowProjMatrix();

        gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);

        gl.clearColor(0, 0, 0, 1);
        gl.enable(gl.DEPTH_TEST);
        gl.disable(gl.BLEND);

        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        for (let type in drawableTypeInfo) {

            if (drawableTypeInfo.hasOwnProperty(type)) {

                const drawableInfo = drawableTypeInfo[type];
                const drawableList = drawableInfo.drawableList;

                for (let i = 0, len = drawableList.length; i < len; i++) {

                    const drawable = drawableList[i];

                    if (drawable.visible === false || !drawable.castsShadow || !drawable.drawShadow) {
                        continue;
                    }

                    if (drawable.renderFlags.colorOpaque) { // Transparent objects don't cast shadows (yet)
                        drawable.drawShadow(frameCtx);
                    }
                }
            }
        }

        shadowRenderBuf.unbind();
    }

    function drawColor(params) {

        const normalDrawSAOBin = [];
        const normalEdgesOpaqueBin = [];
        const normalFillTransparentBin = [];
        const normalEdgesTransparentBin = [];

        const xrayedFillOpaqueBin = [];
        const xrayEdgesOpaqueBin = [];
        const xrayedFillTransparentBin = [];
        const xrayEdgesTransparentBin = [];

        const highlightedFillOpaqueBin = [];
        const highlightedEdgesOpaqueBin = [];
        const highlightedFillTransparentBin = [];
        const highlightedEdgesTransparentBin = [];

        const selectedFillOpaqueBin = [];
        const selectedEdgesOpaqueBin = [];
        const selectedFillTransparentBin = [];
        const selectedEdgesTransparentBin = [];


        const ambientColorAndIntensity = scene._lightsState.getAmbientColorAndIntensity();

        frameCtx.reset();
        frameCtx.pass = params.pass;
        frameCtx.withSAO = false;
        frameCtx.pbrEnabled = pbrEnabled && !!scene.pbrEnabled;
        frameCtx.colorTextureEnabled = colorTextureEnabled && !!scene.colorTextureEnabled;

        gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);

        if (canvasTransparent) {
            gl.clearColor(0, 0, 0, 0);
        } else {
            const backgroundColor = scene.canvas.backgroundColorFromAmbientLight ? ambientColorAndIntensity : scene.canvas.backgroundColor;
            gl.clearColor(backgroundColor[0], backgroundColor[1], backgroundColor[2], 1.0);
        }

        gl.enable(gl.DEPTH_TEST);
        gl.frontFace(gl.CCW);
        gl.enable(gl.CULL_FACE);
        gl.depthMask(true);
        gl.lineWidth(1);

        frameCtx.lineWidth = 1;

        const saoPossible = scene.sao.possible;

        if (saoEnabled && saoPossible) {
            const occlusionRenderBuffer1 = renderBufferManager.getRenderBuffer("saoOcclusion");
            frameCtx.occlusionTexture = occlusionRenderBuffer1 ? occlusionRenderBuffer1.getTexture() : null;
        } else {
            frameCtx.occlusionTexture = null;

        }

        let i;
        let drawable;

        const startTime = Date.now();

        if (params.clear !== false) {
            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        }

        const renderDrawables = function(drawables) {

        let normalDrawSAOBinLen = 0;
        let normalEdgesOpaqueBinLen = 0;
        let normalFillTransparentBinLen = 0;
        let normalEdgesTransparentBinLen = 0;

        let xrayedFillOpaqueBinLen = 0;
        let xrayEdgesOpaqueBinLen = 0;
        let xrayedFillTransparentBinLen = 0;
        let xrayEdgesTransparentBinLen = 0;

        let highlightedFillOpaqueBinLen = 0;
        let highlightedEdgesOpaqueBinLen = 0;
        let highlightedFillTransparentBinLen = 0;
        let highlightedEdgesTransparentBinLen = 0;

        let selectedFillOpaqueBinLen = 0;
        let selectedEdgesOpaqueBinLen = 0;
        let selectedFillTransparentBinLen = 0;
        let selectedEdgesTransparentBinLen = 0;

        //------------------------------------------------------------------------------------------------------
        // Render normal opaque solids, defer others to bins to render after
        //------------------------------------------------------------------------------------------------------
        for (let i = 0, len = drawables.length; i < len; i++) {

            drawable = drawables[i];

            if (drawable.culled === true || drawable.visible === false) {
                continue;
            }

            const renderFlags = drawable.renderFlags;

            if (renderFlags.colorOpaque) {
                if (saoEnabled && saoPossible && drawable.saoEnabled) {
                    normalDrawSAOBin[normalDrawSAOBinLen++] = drawable;
                } else {
                    drawable.drawColorOpaque(frameCtx);
                }
            }

            if (transparentEnabled) {
                if (renderFlags.colorTransparent) {
                    normalFillTransparentBin[normalFillTransparentBinLen++] = drawable;
                }
            }

            if (renderFlags.xrayedSilhouetteTransparent) {
                xrayedFillTransparentBin[xrayedFillTransparentBinLen++] = drawable;
            }

            if (renderFlags.xrayedSilhouetteOpaque) {
                xrayedFillOpaqueBin[xrayedFillOpaqueBinLen++] = drawable;
            }

            if (renderFlags.highlightedSilhouetteTransparent) {
                highlightedFillTransparentBin[highlightedFillTransparentBinLen++] = drawable;
            }

            if (renderFlags.highlightedSilhouetteOpaque) {
                highlightedFillOpaqueBin[highlightedFillOpaqueBinLen++] = drawable;
            }

            if (renderFlags.selectedSilhouetteTransparent) {
                selectedFillTransparentBin[selectedFillTransparentBinLen++] = drawable;
            }

            if (renderFlags.selectedSilhouetteOpaque) {
                selectedFillOpaqueBin[selectedFillOpaqueBinLen++] = drawable;
            }

            if (drawable.edges && edgesEnabled) {
                if (renderFlags.edgesOpaque) {
                    normalEdgesOpaqueBin[normalEdgesOpaqueBinLen++] = drawable;
                }

                if (renderFlags.edgesTransparent) {
                    normalEdgesTransparentBin[normalEdgesTransparentBinLen++] = drawable;
                }

                if (renderFlags.selectedEdgesTransparent) {
                    selectedEdgesTransparentBin[selectedEdgesTransparentBinLen++] = drawable;
                }

                if (renderFlags.selectedEdgesOpaque) {
                    selectedEdgesOpaqueBin[selectedEdgesOpaqueBinLen++] = drawable;
                }

                if (renderFlags.xrayedEdgesTransparent) {
                    xrayEdgesTransparentBin[xrayEdgesTransparentBinLen++] = drawable;
                }

                if (renderFlags.xrayedEdgesOpaque) {
                    xrayEdgesOpaqueBin[xrayEdgesOpaqueBinLen++] = drawable;
                }

                if (renderFlags.highlightedEdgesTransparent) {
                    highlightedEdgesTransparentBin[highlightedEdgesTransparentBinLen++] = drawable;
                }

                if (renderFlags.highlightedEdgesOpaque) {
                    highlightedEdgesOpaqueBin[highlightedEdgesOpaqueBinLen++] = drawable;
                }
            }
        }

        //------------------------------------------------------------------------------------------------------
        // Render deferred bins
        //------------------------------------------------------------------------------------------------------

        // Opaque color with SAO

        if (normalDrawSAOBinLen > 0) {
            frameCtx.withSAO = true;
            for (i = 0; i < normalDrawSAOBinLen; i++) {
                normalDrawSAOBin[i].drawColorOpaque(frameCtx);
            }
        }

        // Opaque edges

        if (normalEdgesOpaqueBinLen > 0) {
            for (i = 0; i < normalEdgesOpaqueBinLen; i++) {
                normalEdgesOpaqueBin[i].drawEdgesColorOpaque(frameCtx);
            }
        }

        // Opaque X-ray fill

        if (xrayedFillOpaqueBinLen > 0) {
            for (i = 0; i < xrayedFillOpaqueBinLen; i++) {
                xrayedFillOpaqueBin[i].drawSilhouetteXRayed(frameCtx);
            }
        }

        // Opaque X-ray edges

        if (xrayEdgesOpaqueBinLen > 0) {
            for (i = 0; i < xrayEdgesOpaqueBinLen; i++) {
                xrayEdgesOpaqueBin[i].drawEdgesXRayed(frameCtx);
            }
        }

        // Transparent

        if (xrayedFillTransparentBinLen > 0 || xrayEdgesTransparentBinLen > 0 || normalFillTransparentBinLen > 0 || normalEdgesTransparentBinLen > 0) {
            gl.enable(gl.CULL_FACE);
            gl.enable(gl.BLEND);
            if (canvasTransparent) {
                gl.blendEquation(gl.FUNC_ADD);
                gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
            } else {
                gl.blendEquation(gl.FUNC_ADD);
                gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
            }
            frameCtx.backfaces = false;
            if (!alphaDepthMask) {
                gl.depthMask(false);
            }

            // Transparent color edges

            if (normalFillTransparentBinLen > 0 || normalEdgesTransparentBinLen > 0) {
                gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
            }
            if (normalEdgesTransparentBinLen > 0) {
                for (i = 0; i < normalEdgesTransparentBinLen; i++) {
                    drawable = normalEdgesTransparentBin[i];
                    drawable.drawEdgesColorTransparent(frameCtx);
                }
            }

            // Transparent color fill

            if (normalFillTransparentBinLen > 0) {
                const eye = frameCtx.pickOrigin || scene.camera.eye;
                const byDist = normalFillTransparentBin.map(d => ({ drawable: d, distSq: math.distVec3(d.origin || vec3_0, eye) }));
                byDist.sort((a, b) => b.distSq - a.distSq);
                for (i = 0; i < normalFillTransparentBinLen; i++) {
                    byDist[i].drawable.drawColorTransparent(frameCtx);
                }
            }

            // Transparent X-ray edges

            if (xrayEdgesTransparentBinLen > 0) {
                for (i = 0; i < xrayEdgesTransparentBinLen; i++) {
                    xrayEdgesTransparentBin[i].drawEdgesXRayed(frameCtx);
                }
            }

            // Transparent X-ray fill

            if (xrayedFillTransparentBinLen > 0) {
                for (i = 0; i < xrayedFillTransparentBinLen; i++) {
                    xrayedFillTransparentBin[i].drawSilhouetteXRayed(frameCtx);
                }
            }

            gl.disable(gl.BLEND);
            if (!alphaDepthMask) {
                gl.depthMask(true);
            }
        }

        // Opaque highlight

        if (highlightedFillOpaqueBinLen > 0 || highlightedEdgesOpaqueBinLen > 0) {
            frameCtx.lastProgramId = null;
            if (scene.highlightMaterial.glowThrough) {
                gl.clear(gl.DEPTH_BUFFER_BIT);
            }

            // Opaque highlighted edges

            if (highlightedEdgesOpaqueBinLen > 0) {
                for (i = 0; i < highlightedEdgesOpaqueBinLen; i++) {
                    highlightedEdgesOpaqueBin[i].drawEdgesHighlighted(frameCtx);
                }
            }

            // Opaque highlighted fill

            if (highlightedFillOpaqueBinLen > 0) {
                for (i = 0; i < highlightedFillOpaqueBinLen; i++) {
                    highlightedFillOpaqueBin[i].drawSilhouetteHighlighted(frameCtx);
                }
            }
        }

        // Highlighted transparent

        if (highlightedFillTransparentBinLen > 0 || highlightedEdgesTransparentBinLen > 0 || highlightedFillOpaqueBinLen > 0) {
            frameCtx.lastProgramId = null;
            if (scene.selectedMaterial.glowThrough) {
                gl.clear(gl.DEPTH_BUFFER_BIT);
            }
            gl.enable(gl.BLEND);
            if (canvasTransparent) {
                gl.blendEquation(gl.FUNC_ADD);
                gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
            } else {
                gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
            }
            gl.enable(gl.CULL_FACE);

            // Highlighted transparent edges

            if (highlightedEdgesTransparentBinLen > 0) {
                for (i = 0; i < highlightedEdgesTransparentBinLen; i++) {
                    highlightedEdgesTransparentBin[i].drawEdgesHighlighted(frameCtx);
                }
            }

            // Highlighted transparent fill

            if (highlightedFillTransparentBinLen > 0) {
                for (i = 0; i < highlightedFillTransparentBinLen; i++) {
                    highlightedFillTransparentBin[i].drawSilhouetteHighlighted(frameCtx);
                }
            }
            gl.disable(gl.BLEND);
        }

        // Selected opaque

        if (selectedFillOpaqueBinLen > 0 || selectedEdgesOpaqueBinLen > 0) {
            frameCtx.lastProgramId = null;
            if (scene.selectedMaterial.glowThrough) {
                gl.clear(gl.DEPTH_BUFFER_BIT);
            }

            // Selected opaque fill

            if (selectedEdgesOpaqueBinLen > 0) {
                for (i = 0; i < selectedEdgesOpaqueBinLen; i++) {
                    selectedEdgesOpaqueBin[i].drawEdgesSelected(frameCtx);
                }
            }

            // Selected opaque edges

            if (selectedFillOpaqueBinLen > 0) {
                for (i = 0; i < selectedFillOpaqueBinLen; i++) {
                    selectedFillOpaqueBin[i].drawSilhouetteSelected(frameCtx);
                }
            }
        }

        // Selected transparent

        if (selectedFillTransparentBinLen > 0 || selectedEdgesTransparentBinLen > 0) {
            frameCtx.lastProgramId = null;
            if (scene.selectedMaterial.glowThrough) {
                gl.clear(gl.DEPTH_BUFFER_BIT);
            }
            gl.enable(gl.CULL_FACE);
            gl.enable(gl.BLEND);
            if (canvasTransparent) {
                gl.blendEquation(gl.FUNC_ADD);
                gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
            } else {
                gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
            }

            // Selected transparent edges

            if (selectedEdgesTransparentBinLen > 0) {
                for (i = 0; i < selectedEdgesTransparentBinLen; i++) {
                    selectedEdgesTransparentBin[i].drawEdgesSelected(frameCtx);
                }
            }

            // Selected transparent fill

            if (selectedFillTransparentBinLen > 0) {
                for (i = 0; i < selectedFillTransparentBinLen; i++) {
                    selectedFillTransparentBin[i].drawSilhouetteSelected(frameCtx);
                }
            }
            gl.disable(gl.BLEND);
        }

        };

        renderDrawables(postCullDrawableList);
        if (uiDrawableList.length > 0) {
            gl.clear(gl.DEPTH_BUFFER_BIT);
            renderDrawables(uiDrawableList);
        }

        const endTime = Date.now();
        const frameStats = stats.frame;

        frameStats.renderTime = (endTime - startTime) / 1000.0;
        frameStats.drawElements = frameCtx.drawElements;
        frameStats.drawArrays = frameCtx.drawArrays;
        frameStats.useProgram = frameCtx.useProgram;
        frameStats.bindTexture = frameCtx.bindTexture;
        frameStats.bindArray = frameCtx.bindArray;

        const numTextureUnits = WEBGL_INFO.MAX_TEXTURE_IMAGE_UNITS;
        for (let ii = 0; ii < numTextureUnits; ii++) {
            gl.activeTexture(gl.TEXTURE0 + ii);
        }
        gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);
        gl.bindTexture(gl.TEXTURE_2D, null);

        const numVertexAttribs = WEBGL_INFO.MAX_VERTEX_ATTRIBS; // Fixes https://github.com/xeokit/xeokit-sdk/issues/174
        for (let ii = 0; ii < numVertexAttribs; ii++) {
            gl.disableVertexAttribArray(ii);
        }
    }

    /**
     * Picks an Entity.
     * @private
     */
    this.pick = (function () {

        const tempVec3a = math.vec3();
        math.mat4();
        const tempMat4b = math.mat4();

        const randomVec3 = math.vec3();
        const up = math.vec3([0, 1, 0]);
        const _pickResult = new PickResult();

        const nearAndFar = math.vec2();

        const canvasPos = math.vec3();

        const worldRayOrigin = math.vec3();
        const worldRayDir = math.vec3();
        math.vec3();
        math.vec3();

        return function (params, pickResult = _pickResult) {

            pickResult.reset();

            updateDrawlist();

            let look;
            let pickViewMatrix = null;
            let pickProjMatrix = null;
            let projection = null;

            pickResult.pickSurface = params.pickSurface;

            if (params.canvasPos) {

                canvasPos[0] = params.canvasPos[0];
                canvasPos[1] = params.canvasPos[1];

                pickViewMatrix = scene.camera.viewMatrix;
                pickProjMatrix = scene.camera.projMatrix;
                projection     = scene.camera.projection;

                nearAndFar[0] = scene.camera.project.near;
                nearAndFar[1] = scene.camera.project.far;

                pickResult.canvasPos = params.canvasPos;

            } else {

                // Picking with arbitrary World-space ray
                // Align camera along ray and fire ray through center of canvas

                if (params.matrix) {

                    pickViewMatrix = params.matrix;
                    pickProjMatrix = scene.camera.projMatrix;
                    projection     = scene.camera.projection;

                    nearAndFar[0] = scene.camera.project.near;
                    nearAndFar[1] = scene.camera.project.far;

                } else {

                    worldRayOrigin.set(params.origin || [0, 0, 0]);
                    worldRayDir.set(params.direction || [0, 0, 1]);

                    look = math.addVec3(worldRayOrigin, worldRayDir, tempVec3a);

                    randomVec3[0] = Math.random();
                    randomVec3[1] = Math.random();
                    randomVec3[2] = Math.random();

                    math.normalizeVec3(randomVec3);
                    math.cross3Vec3(worldRayDir, randomVec3, up);

                    pickViewMatrix = math.lookAtMat4v(worldRayOrigin, look, up, tempMat4b);
                    //    pickProjMatrix = scene.camera.projMatrix;
                    pickProjMatrix = scene.camera.ortho.matrix;
                    projection     = "ortho";

                    nearAndFar[0] = scene.camera.ortho.near;
                    nearAndFar[1] = scene.camera.ortho.far;

                    pickResult.origin = worldRayOrigin;
                    pickResult.direction = worldRayDir;
                }

                canvasPos[0] = canvas.clientWidth * 0.5;
                canvasPos[1] = canvas.clientHeight * 0.5;
            }

            for (let type in drawableTypeInfo) {
                if (drawableTypeInfo.hasOwnProperty(type)) {
                    const drawableList = drawableTypeInfo[type].drawableList;
                    for (let i = 0, len = drawableList.length; i < len; i++) {
                        const drawable = drawableList[i];
                        if (drawable.setPickMatrices) { // Eg. SceneModel, which needs pre-loading into texture
                            drawable.setPickMatrices(pickViewMatrix, pickProjMatrix);
                        }
                    }
                }
            }

            const pickBuffer = renderBufferManager.getRenderBuffer("pick", {size: [1, 1]});

            pickBuffer.bind();

            const pickable = gpuPickPickable(pickBuffer, canvasPos, pickViewMatrix, pickProjMatrix, params, pickResult);

            if (!pickable) {
                pickBuffer.unbind();
                return null;
            }

            const pickedEntity = (pickable.delegatePickedEntity) ? pickable.delegatePickedEntity() : pickable;

            if (!pickedEntity) {
                pickBuffer.unbind();
                return null;
            }

            if (params.pickSurface) {

                // GPU-based ray-picking

                if (pickable.canPickTriangle && pickable.canPickTriangle()) {

                    gpuPickTriangle(pickBuffer, pickable, canvasPos, pickViewMatrix, pickProjMatrix, pickResult);

                    pickable.pickTriangleSurface(pickViewMatrix, pickProjMatrix, projection, pickResult);

                    pickResult.pickSurfacePrecision = false;

                } else {

                    if (pickable.canPickWorldPos && pickable.canPickWorldPos()) {

                        gpuPickWorldPos(pickBuffer, pickable, canvasPos, pickViewMatrix, pickProjMatrix, nearAndFar, pickResult);

                        if (params.pickSurfaceNormal !== false) {
                            gpuPickWorldNormal(pickBuffer, pickable, canvasPos, pickViewMatrix, pickProjMatrix, pickResult);
                        }

                        pickResult.pickSurfacePrecision = false;
                    }
                }
            }
            pickBuffer.unbind();
            pickResult.entity = pickedEntity;
            return pickResult;
        };
    })();

    function gpuPickPickable(pickBuffer, canvasPos, pickViewMatrix, pickProjMatrix, params, pickResult) {

        const resolutionScale = scene.canvas.resolutionScale;

        frameCtx.reset();
        frameCtx.backfaces = true;
        frameCtx.frontface = true; // "ccw"
        frameCtx.pickOrigin = pickResult.origin;
        frameCtx.pickViewMatrix = pickViewMatrix;
        frameCtx.pickProjMatrix = pickProjMatrix;
        frameCtx.pickInvisible = !!params.pickInvisible;
        frameCtx.pickClipPos = [
            getClipPosX(canvasPos[0] * resolutionScale, gl.drawingBufferWidth),
            getClipPosY(canvasPos[1] * resolutionScale, gl.drawingBufferHeight)
        ];

        gl.viewport(0, 0, 1, 1);
        gl.depthMask(true);
        gl.enable(gl.DEPTH_TEST);
        gl.disable(gl.CULL_FACE);
        gl.disable(gl.BLEND);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        const includeEntityIds = params.includeEntityIds;
        const excludeEntityIds = params.excludeEntityIds;

        const renderDrawables = function(drawables) {
        for (let i = 0, len = drawables.length; i < len; i++) {
            const drawable = drawables[i];
            if (drawable.culled === true || drawable.visible === false) {
                continue;
            }
            if (!drawable.drawPickMesh || (params.pickInvisible !== true && drawable.visible === false) || drawable.pickable === false) {
                continue;
            }
            if (includeEntityIds && !includeEntityIds[drawable.id]) { // TODO: push this logic into drawable
                continue;
            }
            if (excludeEntityIds && excludeEntityIds[drawable.id]) {
                continue;
            }
            drawable.drawPickMesh(frameCtx);
        }
        };

        renderDrawables(postCullDrawableList);
        if (uiDrawableList.length > 0) {
            gl.clear(gl.DEPTH_BUFFER_BIT);
            renderDrawables(uiDrawableList);
        }

        const pix = pickBuffer.read(0, 0);
        const pickID = pix[0] + (pix[1] << 8) + (pix[2] << 16) + (pix[3] << 24);

        if (pickID < 0) {
            return;
        }

        const pickable = pickIDs.items[pickID];

        return pickable;
    }

    function gpuPickTriangle(pickBuffer, pickable, canvasPos, pickViewMatrix, pickProjMatrix, pickResult) {

        if (!pickable.drawPickTriangles) {
            return;
        }

        const resolutionScale = scene.canvas.resolutionScale;

        frameCtx.reset();
        frameCtx.backfaces = true;
        frameCtx.frontface = true; // "ccw"
        frameCtx.pickOrigin = pickResult.origin;
        frameCtx.pickViewMatrix = pickViewMatrix; // Can be null
        frameCtx.pickProjMatrix = pickProjMatrix; // Can be null
        // frameCtx.pickInvisible = !!params.pickInvisible;
        frameCtx.pickClipPos = [
            getClipPosX(canvasPos[0] * resolutionScale, gl.drawingBufferWidth),
            getClipPosY(canvasPos[1] * resolutionScale, gl.drawingBufferHeight)
        ];

        gl.viewport(0, 0, 1, 1);

        gl.clearColor(0, 0, 0, 0);
        gl.enable(gl.DEPTH_TEST);
        gl.disable(gl.CULL_FACE);
        gl.disable(gl.BLEND);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        pickable.drawPickTriangles(frameCtx);

        const pix = pickBuffer.read(0, 0);

        let primIndex = pix[0] + (pix[1] * 256) + (pix[2] * 256 * 256) + (pix[3] * 256 * 256 * 256);

        primIndex *= 3; // Convert from triangle number to first vertex in indices

        pickResult.primIndex = primIndex;
    }

    const gpuPickWorldPos = (function () {

        const tempVec4a = math.vec4();
        const tempVec4b = math.vec4();
        const tempVec4c = math.vec4();
        const tempVec4d = math.vec4();
        const tempVec4e = math.vec4();
        const tempMat4a = math.mat4();
        const tempMat4b = math.mat4();
        const tempMat4c = math.mat4();

        return function (pickBuffer, pickable, canvasPos, pickViewMatrix, pickProjMatrix, nearAndFar, pickResult) {

            const resolutionScale = scene.canvas.resolutionScale;

            frameCtx.reset();
            frameCtx.backfaces = true;
            frameCtx.frontface = true; // "ccw"
            frameCtx.pickOrigin = pickResult.origin;
            frameCtx.pickViewMatrix = pickViewMatrix;
            frameCtx.pickProjMatrix = pickProjMatrix;
            frameCtx.pickZNear = nearAndFar[0];
            frameCtx.pickZFar = nearAndFar[1];
            frameCtx.pickElementsCount = pickable.pickElementsCount;
            frameCtx.pickElementsOffset = pickable.pickElementsOffset;
            frameCtx.pickClipPos = [
                getClipPosX(canvasPos[0] * resolutionScale, gl.drawingBufferWidth),
                getClipPosY(canvasPos[1] * resolutionScale, gl.drawingBufferHeight)
            ];

            gl.viewport(0, 0, 1, 1);

            gl.clearColor(0, 0, 0, 0);
            gl.depthMask(true);
            gl.enable(gl.DEPTH_TEST);
            gl.disable(gl.CULL_FACE);
            gl.disable(gl.BLEND);
            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

            pickable.drawPickDepths(frameCtx); // Draw color-encoded fragment screen-space depths

            const pix = pickBuffer.read(0, 0);

            const screenZ = unpackDepth(pix); // Get screen-space Z at the given canvas coords

            // Calculate clip space coordinates, which will be in range of x=[-1..1] and y=[-1..1], with y=(+1) at top
            const x = (canvasPos[0] - canvas.clientWidth / 2) / (canvas.clientWidth / 2);
            const y = -(canvasPos[1] - canvas.clientHeight / 2) / (canvas.clientHeight / 2);

            const origin = pickable.origin;
            let pvMat;

            if (origin) {
                const rtcPickViewMat = createRTCViewMat(pickViewMatrix, origin, tempMat4a);
                pvMat = math.mulMat4(pickProjMatrix, rtcPickViewMat, tempMat4b);

            } else {
                pvMat = math.mulMat4(pickProjMatrix, pickViewMatrix, tempMat4b);
            }

            const pvMatInverse = math.inverseMat4(pvMat, tempMat4c);

            tempVec4a[0] = x;
            tempVec4a[1] = y;
            tempVec4a[2] = -1;
            tempVec4a[3] = 1;

            let world1 = math.transformVec4(pvMatInverse, tempVec4a);
            world1 = math.mulVec4Scalar(world1, 1 / world1[3]);

            tempVec4b[0] = x;
            tempVec4b[1] = y;
            tempVec4b[2] = 1;
            tempVec4b[3] = 1;

            let world2 = math.transformVec4(pvMatInverse, tempVec4b);
            world2 = math.mulVec4Scalar(world2, 1 / world2[3]);

            const dir = math.subVec3(world2, world1, tempVec4c);
            const worldPos = math.addVec3(world1, math.mulVec4Scalar(dir, screenZ, tempVec4d), tempVec4e);

            if (origin) {
                math.addVec3(worldPos, origin);
            }

            pickResult.worldPos = worldPos;
        }
    })();

    function drawSnapInit(frameCtx) {
        frameCtx.snapPickLayerParams = [];
        frameCtx.snapPickLayerNumber = 0;

        for (let i = 0, len = postCullDrawableList.length; i < len; i++) {

            const drawable = postCullDrawableList[i];

            if (drawable.drawSnapInit) {
                if (!drawable.culled && drawable.visible && drawable.pickable) {
                    drawable.drawSnapInit(frameCtx);
                }
            }
        }
        return frameCtx.snapPickLayerParams;
    }

    function drawSnap(frameCtx) {
        frameCtx.snapPickLayerParams = frameCtx.snapPickLayerParams || [];
        frameCtx.snapPickLayerNumber = frameCtx.snapPickLayerParams.length;
        for (let i = 0, len = postCullDrawableList.length; i < len; i++) {

            const drawable = postCullDrawableList[i];

            if (drawable.drawSnapInit) {
                if (drawable.drawSnap) {
                    if (!drawable.culled && drawable.visible && drawable.pickable) {
                        drawable.drawSnap(frameCtx);
                    }
                }
            }
        }
        return frameCtx.snapPickLayerParams;
    }

    function getClipPosX(pos, size) {
        return 2 * (pos / size) - 1;
    }

    function getClipPosY(pos, size) {
        return 1 - 2 * (pos / size);
    }

    /**
     * @param {[number, number]} canvasPos
     * @param {number} [snapRadiusInPixels=30]
     * @param {boolean} [snapToVertex=true]
     * @param {boolean} [snapToEdge=true]
     * @param pickResult
     * @returns {PickResult}
     */
    this.snapPick = (function () {

        const _pickResult = new PickResult();

        return function (params, pickResult = _pickResult) {

            const {canvasPos, origin, direction, snapRadius, snapToVertex, snapToEdge} = params;

            if (!snapToVertex && !snapToEdge) {
                return this.pick({canvasPos, pickSurface: true});
            }

            const resolutionScale = scene.canvas.resolutionScale;

            frameCtx.reset();
            frameCtx.backfaces = true;
            frameCtx.frontface = true; // "ccw"
            frameCtx.pickZNear = scene.camera.project.near;
            frameCtx.pickZFar = scene.camera.project.far;

            const snapRadiusInPixels = snapRadius || 30;

            const vertexPickBuffer = renderBufferManager.getRenderBuffer(
                `uniquePickColors-aabs-${snapRadiusInPixels}`,
                {
                    depthTexture: true,
                    size: [
                        2 * snapRadiusInPixels + 1,
                        2 * snapRadiusInPixels + 1,
                    ]
                }
            );

            frameCtx.snapVectorA = [
                canvasPos ? getClipPosX(canvasPos[0] * resolutionScale, gl.drawingBufferWidth) : 0,
                canvasPos ? getClipPosY(canvasPos[1] * resolutionScale, gl.drawingBufferHeight) : 0,
            ];

            frameCtx.snapInvVectorAB = [
                gl.drawingBufferWidth / (2 * snapRadiusInPixels),
                gl.drawingBufferHeight / (2 * snapRadiusInPixels),
            ];

            // Bind and clear the snap render target

            vertexPickBuffer.bind(gl.RGBA32I, gl.RGBA32I, gl.RGBA8UI);
            gl.viewport(0, 0, vertexPickBuffer.size[0], vertexPickBuffer.size[1]);
            gl.enable(gl.DEPTH_TEST);
            gl.frontFace(gl.CCW);
            gl.disable(gl.CULL_FACE);
            gl.depthMask(true);
            gl.disable(gl.BLEND);
            gl.depthFunc(gl.LEQUAL);
            gl.clear(gl.DEPTH_BUFFER_BIT);
            gl.clearBufferiv(gl.COLOR, 0, new Int32Array([0, 0, 0, 0]));
            gl.clearBufferiv(gl.COLOR, 1, new Int32Array([0, 0, 0, 0]));
            gl.clearBufferuiv(gl.COLOR, 2, new Uint32Array([0, 0, 0, 0]));

            //////////////////////////////////
            // Set view and proj mats for VBO renderers
            ///////////////////////////////////////

            frameCtx.pickViewMatrix = (canvasPos
                                       ? scene.camera.viewMatrix
                                       : math.lookAtMat4v(
                                           origin,
                                           math.addVec3(origin, direction, math.vec3()),
                                           math.vec3([0, 1, 0]),
                                           math.mat4()));

            const pickProjMatrix = scene.camera.projMatrix;

            for (let type in drawableTypeInfo) {
                if (drawableTypeInfo.hasOwnProperty(type)) {
                    const drawableList = drawableTypeInfo[type].drawableList;
                    for (let i = 0, len = drawableList.length; i < len; i++) {
                        const drawable = drawableList[i];
                        if (drawable.setPickMatrices) { // Eg. SceneModel, which needs pre-loading into texture
                            drawable.setPickMatrices(frameCtx.pickViewMatrix, pickProjMatrix);
                        }
                    }
                }
            }

            // a) init z-buffer
            gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2]);
            const layerParamsSurface = drawSnapInit(frameCtx);

            // b) snap-pick
            const layerParamsSnap = [];
            frameCtx.snapPickLayerParams = layerParamsSnap;

            gl.depthMask(false);
            gl.drawBuffers([gl.COLOR_ATTACHMENT0]);

            if (snapToVertex && snapToEdge) {
                frameCtx.snapMode = "edge";
                drawSnap(frameCtx);

                frameCtx.snapMode = "vertex";
                frameCtx.snapPickLayerNumber++;

                drawSnap(frameCtx);
            } else {
                frameCtx.snapMode = snapToVertex ? "vertex" : "edge";

                drawSnap(frameCtx);
            }

            gl.depthMask(true);

            // Read and decode the snapped coordinates

            const snapPickResultArray = vertexPickBuffer.readArray(gl.RGBA_INTEGER, gl.INT, Int32Array, 4);
            const snapPickNormalResultArray = vertexPickBuffer.readArray(gl.RGBA_INTEGER, gl.INT, Int32Array, 4, 1);
            const snapPickIdResultArray = vertexPickBuffer.readArray(gl.RGBA_INTEGER, gl.UNSIGNED_INT, Uint32Array, 4, 2);

            vertexPickBuffer.unbind();

            // result 1) regular hi-precision world position

            let worldPos = null;
            let worldNormal = null;
            let pickable = null;

            const middleX = snapRadiusInPixels;
            const middleY = snapRadiusInPixels;
            const middleIndex = (middleX * 4) + (middleY * vertexPickBuffer.size[0] * 4);
            const pickResultMiddleXY = snapPickResultArray.slice(middleIndex, middleIndex + 4);
            const pickNormalResultMiddleXY = snapPickNormalResultArray.slice(middleIndex, middleIndex + 4);
            const pickPickableResultMiddleXY = snapPickIdResultArray.slice(middleIndex, middleIndex + 4);

            if (pickResultMiddleXY[3] !== 0) {
                const pickedLayerParmasSurface = layerParamsSurface[Math.abs(pickResultMiddleXY[3]) % layerParamsSurface.length];
                const origin = pickedLayerParmasSurface.origin;
                const scale = pickedLayerParmasSurface.coordinateScale;
                worldPos = [
                    pickResultMiddleXY[0] * scale[0] + origin[0],
                    pickResultMiddleXY[1] * scale[1] + origin[1],
                    pickResultMiddleXY[2] * scale[2] + origin[2],
                ];
                worldNormal = math.normalizeVec3([
                    pickNormalResultMiddleXY[0] / math.MAX_INT,
                    pickNormalResultMiddleXY[1] / math.MAX_INT,
                    pickNormalResultMiddleXY[2] / math.MAX_INT,
                ]);

                const pickID =
                    pickPickableResultMiddleXY[0]
                    + (pickPickableResultMiddleXY[1] << 8)
                    + (pickPickableResultMiddleXY[2] << 16)
                    + (pickPickableResultMiddleXY[3] << 24);

                pickable = pickIDs.items[pickID];
            }

            // result 2) hi-precision snapped (to vertex/edge) world position

            let snapPickResult = [];

            for (let i = 0; i < snapPickResultArray.length; i += 4) {
                if (snapPickResultArray[i + 3] > 0) {
                    const pixelNumber = Math.floor(i / 4);
                    const w = vertexPickBuffer.size[0];
                    const x = pixelNumber % w - Math.floor(w / 2);
                    const y = Math.floor(pixelNumber / w) - Math.floor(w / 2);
                    const dist = (Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)));
                    snapPickResult.push({
                        x,
                        y,
                        dist,
                        isVertex: snapToVertex && snapToEdge ? snapPickResultArray[i + 3] > layerParamsSnap.length / 2 : snapToVertex,
                        result: [
                            snapPickResultArray[i + 0],
                            snapPickResultArray[i + 1],
                            snapPickResultArray[i + 2],
                            snapPickResultArray[i + 3],
                        ],
                        normal: [
                            snapPickNormalResultArray[i + 0],
                            snapPickNormalResultArray[i + 1],
                            snapPickNormalResultArray[i + 2],
                            snapPickNormalResultArray[i + 3],
                        ],
                        id: [
                            snapPickIdResultArray[i + 0],
                            snapPickIdResultArray[i + 1],
                            snapPickIdResultArray[i + 2],
                            snapPickIdResultArray[i + 3],
                        ]
                    });
                }
            }

            let snappedWorldPos = null;
            let snappedWorldNormal = null;
            let snappedPickable = null;
            let snapType = null;

            if (snapPickResult.length > 0) {
                // vertex snap first, then edge snap
                snapPickResult.sort((a, b) => {
                    if (a.isVertex !== b.isVertex) {
                        return a.isVertex ? -1 : 1;
                    } else {
                        return a.dist - b.dist;
                    }
                });

                snapType = snapPickResult[0].isVertex ? "vertex" : "edge";
                const snapPick = snapPickResult[0].result;
                const snapPickNormal = snapPickResult[0].normal;
                const snapPickId = snapPickResult[0].id;

                const pickedLayerParmas = layerParamsSnap[snapPick[3]];

                const origin = pickedLayerParmas.origin;
                const scale = pickedLayerParmas.coordinateScale;

                snappedWorldNormal = math.normalizeVec3([
                    snapPickNormal[0] / math.MAX_INT,
                    snapPickNormal[1] / math.MAX_INT,
                    snapPickNormal[2] / math.MAX_INT,
                ]);

                snappedWorldPos = [
                    snapPick[0] * scale[0] + origin[0],
                    snapPick[1] * scale[1] + origin[1],
                    snapPick[2] * scale[2] + origin[2],
                ];

                snappedPickable = pickIDs.items[
                snapPickId[0]
                + (snapPickId[1] << 8)
                + (snapPickId[2] << 16)
                + (snapPickId[3] << 24)
                    ];
            }

            if (null === worldPos && null == snappedWorldPos) {   // If neither regular pick or snap pick, return null
                return null;
            }

            let snappedCanvasPos = null;

            if (null !== snappedWorldPos) {
                snappedCanvasPos = scene.camera.projectWorldPos(snappedWorldPos);
            }

            const snappedEntity = (snappedPickable && snappedPickable.delegatePickedEntity) ? snappedPickable.delegatePickedEntity() : snappedPickable;
            if (!snappedEntity && pickable) {
                pickable = pickable.delegatePickedEntity ? pickable.delegatePickedEntity() : pickable;
            }

            pickResult.reset();
            pickResult.snappedToEdge = (snapType === "edge");
            pickResult.snappedToVertex = (snapType === "vertex");
            pickResult.worldPos = snappedWorldPos || worldPos;
            pickResult.worldNormal = snappedWorldNormal || worldNormal;
            pickResult.entity = snappedEntity || pickable;
            pickResult.canvasPos = canvasPos || scene.camera.projectWorldPos(worldPos || snappedWorldPos);
            pickResult.snappedCanvasPos = snappedCanvasPos || canvasPos;

            return pickResult;
        };
    })();

    function unpackDepth(depthZ) {
        const vec = [depthZ[0] / 256.0, depthZ[1] / 256.0, depthZ[2] / 256.0, depthZ[3] / 256.0];
        const bitShift = [1.0 / (256.0 * 256.0 * 256.0), 1.0 / (256.0 * 256.0), 1.0 / 256.0, 1.0];
        return math.dotVec4(vec, bitShift);
    }

    function gpuPickWorldNormal(pickBuffer, pickable, canvasPos, pickViewMatrix, pickProjMatrix, pickResult) {

        const resolutionScale = scene.canvas.resolutionScale;

        frameCtx.reset();
        frameCtx.backfaces = true;
        frameCtx.frontface = true; // "ccw"
        frameCtx.pickOrigin = pickResult.origin;
        frameCtx.pickViewMatrix = pickViewMatrix;
        frameCtx.pickProjMatrix = pickProjMatrix;
        frameCtx.pickClipPos = [
            getClipPosX(canvasPos[0] * resolutionScale, gl.drawingBufferWidth),
            getClipPosY(canvasPos[1] * resolutionScale, gl.drawingBufferHeight),
        ];

        const pickNormalBuffer = renderBufferManager.getRenderBuffer("pick-normal", {size: [3, 3]});

        pickNormalBuffer.bind(gl.RGBA32I);

        gl.viewport(0, 0, pickNormalBuffer.size[0], pickNormalBuffer.size[1]);

        gl.enable(gl.DEPTH_TEST);
        gl.disable(gl.CULL_FACE);
        gl.disable(gl.BLEND);
        gl.clear(gl.DEPTH_BUFFER_BIT);
        gl.clearBufferiv(gl.COLOR, 0, new Int32Array([0, 0, 0, 0]));

        pickable.drawPickNormals(frameCtx); // Draw color-encoded fragment World-space normals

        const pix = pickNormalBuffer.read(1, 1, gl.RGBA_INTEGER, gl.INT, Int32Array, 4);

        pickNormalBuffer.unbind();

        const worldNormal = [
            pix[0] / math.MAX_INT,
            pix[1] / math.MAX_INT,
            pix[2] / math.MAX_INT,
        ];

        math.normalizeVec3(worldNormal);

        pickResult.worldNormal = worldNormal;
    }

    /**
     * Adds a {@link Marker} for occlusion testing.
     * @param marker
     */
    this.addMarker = function (marker) {
        this._occlusionTester = this._occlusionTester || new OcclusionTester(scene, renderBufferManager);
        this._occlusionTester.addMarker(marker);
        scene.occlusionTestCountdown = 0;
    };

    /**
     * Notifies that a {@link Marker#worldPos} has updated.
     * @param marker
     */
    this.markerWorldPosUpdated = function (marker) {
        this._occlusionTester.markerWorldPosUpdated(marker);
    };

    /**
     * Removes a {@link Marker} from occlusion testing.
     * @param marker
     */
    this.removeMarker = function (marker) {
        this._occlusionTester.removeMarker(marker);
    };

    /**
     * Performs an occlusion test for all added {@link Marker}s, updating
     * their {@link Marker#visible} properties accordingly.
     */
    this.doOcclusionTest = function () {

        if (this._occlusionTester && this._occlusionTester.needOcclusionTest) {

            updateDrawlist();

            this._occlusionTester.bindRenderBuf();

            frameCtx.reset();
            frameCtx.backfaces = true;
            frameCtx.frontface = true; // "ccw"

            gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
            gl.clearColor(0, 0, 0, 0);
            gl.enable(gl.DEPTH_TEST);
            gl.disable(gl.CULL_FACE);
            gl.disable(gl.BLEND);
            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);


            for (let i = 0, len = postCullDrawableList.length; i < len; i++) {

                const drawable = postCullDrawableList[i];

                if (!drawable.drawOcclusion || drawable.culled === true || drawable.visible === false || drawable.pickable === false) { // TODO: Option to exclude transparent?
                    continue;
                }

                drawable.drawOcclusion(frameCtx);
            }

            this._occlusionTester.drawMarkers(frameCtx);
            this._occlusionTester.doOcclusionTest(); // Updates Marker "visible" properties
            this._occlusionTester.unbindRenderBuf();
        }
    };

    /**
     * Read pixels from the renderer's current output. Performs a force-render first.
     * @param pixels
     * @param colors
     * @param len
     * @param opaqueOnly
     * @private
     */
    this.readPixels = function (pixels, colors, len, opaqueOnly) {
        const snapshotBuffer = renderBufferManager.getRenderBuffer("snapshot");
        snapshotBuffer.bind();
        snapshotBuffer.clear();
        this.render({force: true, opaqueOnly: opaqueOnly});
        let color;
        let i;
        let j;
        let k;
        for (i = 0; i < len; i++) {
            j = i * 2;
            k = i * 4;
            color = snapshotBuffer.read(pixels[j], pixels[j + 1]);
            colors[k] = color[0];
            colors[k + 1] = color[1];
            colors[k + 2] = color[2];
            colors[k + 3] = color[3];
        }
        snapshotBuffer.unbind();
        imageDirty = true;
    };

    /**
     * Enter snapshot mode.
     *
     * Switches rendering to a hidden snapshot canvas.
     *
     * Exit snapshot mode using endSnapshot().
     */
    this.beginSnapshot = function (params = {}) {
        const snapshotBuffer = renderBufferManager.getRenderBuffer("snapshot");
        if (params.width && params.height) {
            snapshotBuffer.setSize([params.width, params.height]);
        }
        snapshotBuffer.bind();
        snapshotBuffer.clear();
        snapshotBound = true;
    };

    /**
     * When in snapshot mode, renders a frame of the current Scene state to the snapshot canvas.
     */
    this.renderSnapshot = function () {
        if (!snapshotBound) {
            return;
        }
        const snapshotBuffer = renderBufferManager.getRenderBuffer("snapshot");
        snapshotBuffer.clear();
        this.render({force: true, opaqueOnly: false});
        imageDirty = true;
    };

    /**
     * When in snapshot mode, gets an image of the snapshot canvas.
     *
     * @private
     * @returns {String} The image data URI.
     */
    this.readSnapshot = function (params) {
        const snapshotBuffer = renderBufferManager.getRenderBuffer("snapshot");
        return snapshotBuffer.readImage(params);
    };

    /**
     * Returns an HTMLCanvas containing an image of the snapshot canvas.
     *
     * - The HTMLCanvas has a CanvasRenderingContext2D.
     * - Expects the caller to draw more things on the HTMLCanvas (annotations etc).
     *
     * @returns {HTMLCanvasElement}
     */
    this.readSnapshotAsCanvas = function () {
        const snapshotBuffer = renderBufferManager.getRenderBuffer("snapshot");
        return snapshotBuffer.readImageAsCanvas();
    };

    /**
     * Exists snapshot mode.
     *
     * Switches rendering back to the main canvas.
     */
    this.endSnapshot = function () {
        if (!snapshotBound) {
            return;
        }
        const snapshotBuffer = renderBufferManager.getRenderBuffer("snapshot");
        snapshotBuffer.unbind();
        snapshotBound = false;
    };

    /**
     * Destroys this renderer.
     * @private
     */
    this.destroy = function () {

        drawableTypeInfo = {};
        drawables = {};

        renderBufferManager.destroy();

        saoOcclusionRenderer.destroy();
        saoDepthLimitedBlurRenderer.destroy();

        if (this._occlusionTester) {
            this._occlusionTester.destroy();
        }
    };
};

/**
 * @desc Meditates mouse, touch and keyboard events for various interaction controls.
 *
 * Ordinarily, you would only use this component as a utility to help manage input events and state for your
 * own custom input handlers.
 *
 * * Located at {@link Scene#input}
 * * Used by (at least) {@link CameraControl}
 *
 * ## Usage
 *
 * Subscribing to mouse events on the canvas:
 *
 * ````javascript
 * import {Viewer} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *      canvasId: "myCanvas"
 * });
 *
 * const input = viewer.scene.input;
 *
 * const onMouseDown = input.on("mousedown", (canvasCoords) => {
 *       console.log("Mouse down at: x=" + canvasCoords[0] + ", y=" + coords[1]);
 * });
 *
 * const onMouseUp = input.on("mouseup", (canvasCoords) => {
 *       console.log("Mouse up at: x=" + canvasCoords[0] + ", y=" + canvasCoords[1]);
 * });
 *
 * const onMouseClicked = input.on("mouseclicked", (canvasCoords) => {
 *      console.log("Mouse clicked at: x=" + canvasCoords[0] + ", y=" + canvasCoords[1]);
 * });
 *
 * const onDblClick = input.on("dblclick", (canvasCoords) => {
 *       console.log("Double-click at: x=" + canvasCoords[0] + ", y=" + canvasCoords[1]);
 * });
 * ````
 *
 * Subscribing to keyboard events on the canvas:
 *
 * ````javascript
 * const onKeyDown = input.on("keydown", (keyCode) => {
 *      switch (keyCode) {
 *          case this.KEY_A:
 *              console.log("The 'A' key is down");
 *              break;
 *
 *          case this.KEY_B:
 *              console.log("The 'B' key is down");
 *              break;
 *
 *          case this.KEY_C:
 *              console.log("The 'C' key is down");
 *              break;
 *
 *          default:
 *              console.log("Some other key is down");
 *      }
 * });
 *
 * const onKeyUp = input.on("keyup", (keyCode) => {
 *      switch (keyCode) {
 *          case this.KEY_A:
 *              console.log("The 'A' key is up");
 *              break;
 *
 *          case this.KEY_B:
 *              console.log("The 'B' key is up");
 *              break;
 *
 *          case this.KEY_C:
 *              console.log("The 'C' key is up");
 *              break;
 *
 *          default:
 *              console.log("Some other key is up");
 *      }
 *  });
 * ````
 *
 * Checking if keys are down:
 *
 * ````javascript
 * const isCtrlDown = input.ctrlDown;
 * const isAltDown = input.altDown;
 * const shiftDown = input.shiftDown;
 * //...
 *
 * const isAKeyDown = input.keyDown[input.KEY_A];
 * const isBKeyDown = input.keyDown[input.KEY_B];
 * const isShiftKeyDown = input.keyDown[input.KEY_SHIFT];
 * //...
 *
 * ````
 * Unsubscribing from events:
 *
 * ````javascript
 * input.off(onMouseDown);
 * input.off(onMouseUp);
 * //...
 * ````
 *
 * ## Disabling all events
 *
 * Event handling is enabled by default.
 *
 * To disable all events:
 *
 * ````javascript
 * myViewer.scene.input.setEnabled(false);
 * ````
 * To enable all events again:
 *
 * ````javascript
 * myViewer.scene.input.setEnabled(true);
 * ````
 *
 * ## Disabling keyboard input
 *
 * When the mouse is over the canvas, the canvas will consume keyboard events. Therefore, sometimes we need
 * to disable keyboard control, so that other UI elements can get those events.
 *
 * To disable keyboard events:
 *
 * ````javascript
 * myViewer.scene.input.setKeyboardEnabled(false);
 * ````
 *
 * To enable keyboard events again:
 *
 * ````javascript
 * myViewer.scene.input.setKeyboardEnabled(true)
 * ````
 */
class Input extends Component {

    /**
     * @private
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        /**
         * Code for the BACKSPACE key.
         * @property KEY_BACKSPACE
         * @final
         * @type {Number}
         */
        this.KEY_BACKSPACE = 8;

        /**
         * Code for the TAB key.
         * @property KEY_TAB
         * @final
         * @type {Number}
         */
        this.KEY_TAB = 9;

        /**
         * Code for the ENTER key.
         * @property KEY_ENTER
         * @final
         * @type {Number}
         */
        this.KEY_ENTER = 13;

        /**
         * Code for the SHIFT key.
         * @property KEY_SHIFT
         * @final
         * @type {Number}
         */
        this.KEY_SHIFT = 16;

        /**
         * Code for the CTRL key.
         * @property KEY_CTRL
         * @final
         * @type {Number}
         */
        this.KEY_CTRL = 17;

        /**
         * Code for the ALT key.
         * @property KEY_ALT
         * @final
         * @type {Number}
         */
        this.KEY_ALT = 18;

        /**
         * Code for the PAUSE_BREAK key.
         * @property KEY_PAUSE_BREAK
         * @final
         * @type {Number}
         */
        this.KEY_PAUSE_BREAK = 19;

        /**
         * Code for the CAPS_LOCK key.
         * @property KEY_CAPS_LOCK
         * @final
         * @type {Number}
         */
        this.KEY_CAPS_LOCK = 20;

        /**
         * Code for the ESCAPE key.
         * @property KEY_ESCAPE
         * @final
         * @type {Number}
         */
        this.KEY_ESCAPE = 27;

        /**
         * Code for the PAGE_UP key.
         * @property KEY_PAGE_UP
         * @final
         * @type {Number}
         */
        this.KEY_PAGE_UP = 33;

        /**
         * Code for the PAGE_DOWN key.
         * @property KEY_PAGE_DOWN
         * @final
         * @type {Number}
         */
        this.KEY_PAGE_DOWN = 34;

        /**
         * Code for the END key.
         * @property KEY_END
         * @final
         * @type {Number}
         */
        this.KEY_END = 35;

        /**
         * Code for the HOME key.
         * @property KEY_HOME
         * @final
         * @type {Number}
         */
        this.KEY_HOME = 36;

        /**
         * Code for the LEFT_ARROW key.
         * @property KEY_LEFT_ARROW
         * @final
         * @type {Number}
         */
        this.KEY_LEFT_ARROW = 37;

        /**
         * Code for the UP_ARROW key.
         * @property KEY_UP_ARROW
         * @final
         * @type {Number}
         */
        this.KEY_UP_ARROW = 38;

        /**
         * Code for the RIGHT_ARROW key.
         * @property KEY_RIGHT_ARROW
         * @final
         * @type {Number}
         */
        this.KEY_RIGHT_ARROW = 39;

        /**
         * Code for the DOWN_ARROW key.
         * @property KEY_DOWN_ARROW
         * @final
         * @type {Number}
         */
        this.KEY_DOWN_ARROW = 40;

        /**
         * Code for the INSERT key.
         * @property KEY_INSERT
         * @final
         * @type {Number}
         */
        this.KEY_INSERT = 45;

        /**
         * Code for the DELETE key.
         * @property KEY_DELETE
         * @final
         * @type {Number}
         */
        this.KEY_DELETE = 46;

        /**
         * Code for the 0 key.
         * @property KEY_NUM_0
         * @final
         * @type {Number}
         */
        this.KEY_NUM_0 = 48;

        /**
         * Code for the 1 key.
         * @property KEY_NUM_1
         * @final
         * @type {Number}
         */
        this.KEY_NUM_1 = 49;

        /**
         * Code for the 2 key.
         * @property KEY_NUM_2
         * @final
         * @type {Number}
         */
        this.KEY_NUM_2 = 50;

        /**
         * Code for the 3 key.
         * @property KEY_NUM_3
         * @final
         * @type {Number}
         */
        this.KEY_NUM_3 = 51;

        /**
         * Code for the 4 key.
         * @property KEY_NUM_4
         * @final
         * @type {Number}
         */
        this.KEY_NUM_4 = 52;

        /**
         * Code for the 5 key.
         * @property KEY_NUM_5
         * @final
         * @type {Number}
         */
        this.KEY_NUM_5 = 53;

        /**
         * Code for the 6 key.
         * @property KEY_NUM_6
         * @final
         * @type {Number}
         */
        this.KEY_NUM_6 = 54;

        /**
         * Code for the 7 key.
         * @property KEY_NUM_7
         * @final
         * @type {Number}
         */
        this.KEY_NUM_7 = 55;

        /**
         * Code for the 8 key.
         * @property KEY_NUM_8
         * @final
         * @type {Number}
         */
        this.KEY_NUM_8 = 56;

        /**
         * Code for the 9 key.
         * @property KEY_NUM_9
         * @final
         * @type {Number}
         */
        this.KEY_NUM_9 = 57;

        /**
         * Code for the A key.
         * @property KEY_A
         * @final
         * @type {Number}
         */
        this.KEY_A = 65;

        /**
         * Code for the B key.
         * @property KEY_B
         * @final
         * @type {Number}
         */
        this.KEY_B = 66;

        /**
         * Code for the C key.
         * @property KEY_C
         * @final
         * @type {Number}
         */
        this.KEY_C = 67;

        /**
         * Code for the D key.
         * @property KEY_D
         * @final
         * @type {Number}
         */
        this.KEY_D = 68;

        /**
         * Code for the E key.
         * @property KEY_E
         * @final
         * @type {Number}
         */
        this.KEY_E = 69;

        /**
         * Code for the F key.
         * @property KEY_F
         * @final
         * @type {Number}
         */
        this.KEY_F = 70;

        /**
         * Code for the G key.
         * @property KEY_G
         * @final
         * @type {Number}
         */
        this.KEY_G = 71;

        /**
         * Code for the H key.
         * @property KEY_H
         * @final
         * @type {Number}
         */
        this.KEY_H = 72;

        /**
         * Code for the I key.
         * @property KEY_I
         * @final
         * @type {Number}
         */
        this.KEY_I = 73;

        /**
         * Code for the J key.
         * @property KEY_J
         * @final
         * @type {Number}
         */
        this.KEY_J = 74;

        /**
         * Code for the K key.
         * @property KEY_K
         * @final
         * @type {Number}
         */
        this.KEY_K = 75;

        /**
         * Code for the L key.
         * @property KEY_L
         * @final
         * @type {Number}
         */
        this.KEY_L = 76;

        /**
         * Code for the M key.
         * @property KEY_M
         * @final
         * @type {Number}
         */
        this.KEY_M = 77;

        /**
         * Code for the N key.
         * @property KEY_N
         * @final
         * @type {Number}
         */
        this.KEY_N = 78;

        /**
         * Code for the O key.
         * @property KEY_O
         * @final
         * @type {Number}
         */
        this.KEY_O = 79;

        /**
         * Code for the P key.
         * @property KEY_P
         * @final
         * @type {Number}
         */
        this.KEY_P = 80;

        /**
         * Code for the Q key.
         * @property KEY_Q
         * @final
         * @type {Number}
         */
        this.KEY_Q = 81;

        /**
         * Code for the R key.
         * @property KEY_R
         * @final
         * @type {Number}
         */
        this.KEY_R = 82;

        /**
         * Code for the S key.
         * @property KEY_S
         * @final
         * @type {Number}
         */
        this.KEY_S = 83;

        /**
         * Code for the T key.
         * @property KEY_T
         * @final
         * @type {Number}
         */
        this.KEY_T = 84;

        /**
         * Code for the U key.
         * @property KEY_U
         * @final
         * @type {Number}
         */
        this.KEY_U = 85;

        /**
         * Code for the V key.
         * @property KEY_V
         * @final
         * @type {Number}
         */
        this.KEY_V = 86;

        /**
         * Code for the W key.
         * @property KEY_W
         * @final
         * @type {Number}
         */
        this.KEY_W = 87;

        /**
         * Code for the X key.
         * @property KEY_X
         * @final
         * @type {Number}
         */
        this.KEY_X = 88;

        /**
         * Code for the Y key.
         * @property KEY_Y
         * @final
         * @type {Number}
         */
        this.KEY_Y = 89;

        /**
         * Code for the Z key.
         * @property KEY_Z
         * @final
         * @type {Number}
         */
        this.KEY_Z = 90;

        /**
         * Code for the LEFT_WINDOW key.
         * @property KEY_LEFT_WINDOW
         * @final
         * @type {Number}
         */
        this.KEY_LEFT_WINDOW = 91;

        /**
         * Code for the RIGHT_WINDOW key.
         * @property KEY_RIGHT_WINDOW
         * @final
         * @type {Number}
         */
        this.KEY_RIGHT_WINDOW = 92;

        /**
         * Code for the SELECT key.
         * @property KEY_SELECT
         * @final
         * @type {Number}
         */
        this.KEY_SELECT_KEY = 93;

        /**
         * Code for the number pad 0 key.
         * @property KEY_NUMPAD_0
         * @final
         * @type {Number}
         */
        this.KEY_NUMPAD_0 = 96;

        /**
         * Code for the number pad 1 key.
         * @property KEY_NUMPAD_1
         * @final
         * @type {Number}
         */
        this.KEY_NUMPAD_1 = 97;

        /**
         * Code for the number pad 2 key.
         * @property KEY_NUMPAD 2
         * @final
         * @type {Number}
         */
        this.KEY_NUMPAD_2 = 98;

        /**
         * Code for the number pad 3 key.
         * @property KEY_NUMPAD_3
         * @final
         * @type {Number}
         */
        this.KEY_NUMPAD_3 = 99;

        /**
         * Code for the number pad 4 key.
         * @property KEY_NUMPAD_4
         * @final
         * @type {Number}
         */
        this.KEY_NUMPAD_4 = 100;

        /**
         * Code for the number pad 5 key.
         * @property KEY_NUMPAD_5
         * @final
         * @type {Number}
         */
        this.KEY_NUMPAD_5 = 101;

        /**
         * Code for the number pad 6 key.
         * @property KEY_NUMPAD_6
         * @final
         * @type {Number}
         */
        this.KEY_NUMPAD_6 = 102;

        /**
         * Code for the number pad 7 key.
         * @property KEY_NUMPAD_7
         * @final
         * @type {Number}
         */
        this.KEY_NUMPAD_7 = 103;

        /**
         * Code for the number pad 8 key.
         * @property KEY_NUMPAD_8
         * @final
         * @type {Number}
         */
        this.KEY_NUMPAD_8 = 104;

        /**
         * Code for the number pad 9 key.
         * @property KEY_NUMPAD_9
         * @final
         * @type {Number}
         */
        this.KEY_NUMPAD_9 = 105;

        /**
         * Code for the MULTIPLY key.
         * @property KEY_MULTIPLY
         * @final
         * @type {Number}
         */
        this.KEY_MULTIPLY = 106;

        /**
         * Code for the ADD key.
         * @property KEY_ADD
         * @final
         * @type {Number}
         */
        this.KEY_ADD = 107;

        /**
         * Code for the SUBTRACT key.
         * @property KEY_SUBTRACT
         * @final
         * @type {Number}
         */
        this.KEY_SUBTRACT = 109;

        /**
         * Code for the DECIMAL POINT key.
         * @property KEY_DECIMAL_POINT
         * @final
         * @type {Number}
         */
        this.KEY_DECIMAL_POINT = 110;

        /**
         * Code for the DIVIDE key.
         * @property KEY_DIVIDE
         * @final
         * @type {Number}
         */
        this.KEY_DIVIDE = 111;

        /**
         * Code for the F1 key.
         * @property KEY_F1
         * @final
         * @type {Number}
         */
        this.KEY_F1 = 112;

        /**
         * Code for the F2 key.
         * @property KEY_F2
         * @final
         * @type {Number}
         */
        this.KEY_F2 = 113;

        /**
         * Code for the F3 key.
         * @property KEY_F3
         * @final
         * @type {Number}
         */
        this.KEY_F3 = 114;

        /**
         * Code for the F4 key.
         * @property KEY_F4
         * @final
         * @type {Number}
         */
        this.KEY_F4 = 115;

        /**
         * Code for the F5 key.
         * @property KEY_F5
         * @final
         * @type {Number}
         */
        this.KEY_F5 = 116;

        /**
         * Code for the F6 key.
         * @property KEY_F6
         * @final
         * @type {Number}
         */
        this.KEY_F6 = 117;

        /**
         * Code for the F7 key.
         * @property KEY_F7
         * @final
         * @type {Number}
         */
        this.KEY_F7 = 118;

        /**
         * Code for the F8 key.
         * @property KEY_F8
         * @final
         * @type {Number}
         */
        this.KEY_F8 = 119;

        /**
         * Code for the F9 key.
         * @property KEY_F9
         * @final
         * @type {Number}
         */
        this.KEY_F9 = 120;

        /**
         * Code for the F10 key.
         * @property KEY_F10
         * @final
         * @type {Number}
         */
        this.KEY_F10 = 121;

        /**
         * Code for the F11 key.
         * @property KEY_F11
         * @final
         * @type {Number}
         */
        this.KEY_F11 = 122;

        /**
         * Code for the F12 key.
         * @property KEY_F12
         * @final
         * @type {Number}
         */
        this.KEY_F12 = 123;

        /**
         * Code for the NUM_LOCK key.
         * @property KEY_NUM_LOCK
         * @final
         * @type {Number}
         */
        this.KEY_NUM_LOCK = 144;

        /**
         * Code for the SCROLL_LOCK key.
         * @property KEY_SCROLL_LOCK
         * @final
         * @type {Number}
         */
        this.KEY_SCROLL_LOCK = 145;

        /**
         * Code for the SEMI_COLON key.
         * @property KEY_SEMI_COLON
         * @final
         * @type {Number}
         */
        this.KEY_SEMI_COLON = 186;

        /**
         * Code for the EQUAL_SIGN key.
         * @property KEY_EQUAL_SIGN
         * @final
         * @type {Number}
         */
        this.KEY_EQUAL_SIGN = 187;

        /**
         * Code for the COMMA key.
         * @property KEY_COMMA
         * @final
         * @type {Number}
         */
        this.KEY_COMMA = 188;

        /**
         * Code for the DASH key.
         * @property KEY_DASH
         * @final
         * @type {Number}
         */
        this.KEY_DASH = 189;

        /**
         * Code for the PERIOD key.
         * @property KEY_PERIOD
         * @final
         * @type {Number}
         */
        this.KEY_PERIOD = 190;

        /**
         * Code for the FORWARD_SLASH key.
         * @property KEY_FORWARD_SLASH
         * @final
         * @type {Number}
         */
        this.KEY_FORWARD_SLASH = 191;

        /**
         * Code for the GRAVE_ACCENT key.
         * @property KEY_GRAVE_ACCENT
         * @final
         * @type {Number}
         */
        this.KEY_GRAVE_ACCENT = 192;

        /**
         * Code for the OPEN_BRACKET key.
         * @property KEY_OPEN_BRACKET
         * @final
         * @type {Number}
         */
        this.KEY_OPEN_BRACKET = 219;

        /**
         * Code for the BACK_SLASH key.
         * @property KEY_BACK_SLASH
         * @final
         * @type {Number}
         */
        this.KEY_BACK_SLASH = 220;

        /**
         * Code for the CLOSE_BRACKET key.
         * @property KEY_CLOSE_BRACKET
         * @final
         * @type {Number}
         */
        this.KEY_CLOSE_BRACKET = 221;

        /**
         * Code for the SINGLE_QUOTE key.
         * @property KEY_SINGLE_QUOTE
         * @final
         * @type {Number}
         */
        this.KEY_SINGLE_QUOTE = 222;

        /**
         * Code for the SPACE key.
         * @property KEY_SPACE
         * @final
         * @type {Number}
         */
        this.KEY_SPACE = 32;

        /**
         * Code for the left mouse button.
         * @property MOUSE_LEFT_BUTTON
         * @final
         * @type {Number}
         */
        this.MOUSE_LEFT_BUTTON = 260;

        /**
         * Code for the middle mouse button.
         * @property MOUSE_MIDDLE_BUTTON
         * @final
         * @type {Number}
         */
        this.MOUSE_MIDDLE_BUTTON = 261;

        /**
         * Code for the right mouse button.
         * @property MOUSE_RIGHT_BUTTON
         * @final
         * @type {Number}
         */
        this.MOUSE_RIGHT_BUTTON = 262;

        /**
         * The canvas element that mouse and keyboards are bound to.
         *
         * @final
         * @type {HTMLCanvasElement}
         */
        this.element = cfg.element;

        /** True whenever ALT key is down.
         *
         * @type {boolean}
         */
        this.altDown = false;

        /** True whenever CTRL key is down.
         *
         * @type {boolean}
         */
        this.ctrlDown = false;

        /** True whenever left mouse button is down.
         *
         * @type {boolean}
         */
        this.mouseDownLeft = false;

        /**
         * True whenever middle mouse button is down.
         *
         * @type {boolean}
         */
        this.mouseDownMiddle = false;

        /**
         * True whenever the right mouse button is down.
         *
         * @type {boolean}
         */
        this.mouseDownRight = false;

        /**
         * Flag for each key that's down.
         *
         * @type {boolean[]}
         */
        this.keyDown = [];

        /** True while input enabled
         *
         * @type {boolean}
         */
        this.enabled = true;

        /** True while keyboard input is enabled.
         *
         * Default value is ````true````.
         *
         * {@link CameraControl} will not respond to keyboard events while this is ````false````.
         *
         * @type {boolean}
         */
        this.keyboardEnabled = true;

        /** True while the mouse is over the canvas.
         *
         * @type {boolean}
         */
        this.mouseover = false;

        /**
         * Current mouse position within the canvas.
         * @type {Number[]}
         */
        this.mouseCanvasPos = math.vec2();

        this._keyboardEventsElement = cfg.keyboardEventsElement || document;

        this._bindEvents();
    }

    _bindEvents() {

        if (this._eventsBound) {
            return;
        }

        this._keyboardEventsElement.addEventListener("keydown", this._keyDownListener = (e) => {
            if (!this.enabled || (!this.keyboardEnabled)) {
                return;
            }
            if (e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") {
                if (e.keyCode === this.KEY_CTRL) {
                    this.ctrlDown = true;
                } else if (e.keyCode === this.KEY_ALT) {
                    this.altDown = true;
                } else if (e.keyCode === this.KEY_SHIFT) {
                    this.shiftDown = true;
                }
                this.keyDown[e.keyCode] = true;
                this.fire("keydown", e.keyCode, true);
            }
        }, false);

        this._keyboardEventsElement.addEventListener("keyup", this._keyUpListener = (e) => {
            if (!this.enabled || (!this.keyboardEnabled)) {
                return;
            }
            if (e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") {
                if (e.keyCode === this.KEY_CTRL) {
                    this.ctrlDown = false;
                } else if (e.keyCode === this.KEY_ALT) {
                    this.altDown = false;
                } else if (e.keyCode === this.KEY_SHIFT) {
                    this.shiftDown = false;
                }
                this.keyDown[e.keyCode] = false;
                this.fire("keyup", e.keyCode, true);
            }
        });

        this.element.addEventListener("mouseenter", this._mouseEnterListener = (e) => {
            if (!this.enabled) {
                return;
            }
            this.mouseover = true;
            this._getMouseCanvasPos(e);
            this.fire("mouseenter", this.mouseCanvasPos, true);
        });

        this.element.addEventListener("mouseleave", this._mouseLeaveListener = (e) => {
            if (!this.enabled) {
                return;
            }
            this.mouseover = false;
            this._getMouseCanvasPos(e);
            this.fire("mouseleave", this.mouseCanvasPos, true);
        });

        this.element.addEventListener("mousedown", this._mouseDownListener = (e) => {
            if (!this.enabled) {
                return;
            }
            switch (e.which) {
                case 1:// Left button
                    this.mouseDownLeft = true;
                    break;
                case 2:// Middle/both buttons
                    this.mouseDownMiddle = true;
                    break;
                case 3:// Right button
                    this.mouseDownRight = true;
                    break;
            }
            this._getMouseCanvasPos(e);
            this.element.focus();
            this.fire("mousedown", this.mouseCanvasPos, true);
            if (this.mouseover) {
                e.preventDefault();
            }
        });

        document.addEventListener("mouseup", this._mouseUpListener = (e) => {
            if (!this.enabled) {
                return;
            }
            switch (e.which) {
                case 1:// Left button
                    this.mouseDownLeft = false;
                    break;
                case 2:// Middle/both buttons
                    this.mouseDownMiddle = false;
                    break;
                case 3:// Right button
                    this.mouseDownRight = false;
                    break;
            }
            this.fire("mouseup", this.mouseCanvasPos, true);
            // if (this.mouseover) {
            //     e.preventDefault();
            // }
        }, true);

        document.addEventListener("click", this._clickListener = (e) => {
            if (!this.enabled) {
                return;
            }
            switch (e.which) {
                case 1:// Left button
                    this.mouseDownLeft = false;
                    this.mouseDownRight = false;
                    break;
                case 2:// Middle/both buttons
                    this.mouseDownMiddle = false;
                    break;
                case 3:// Right button
                    this.mouseDownLeft = false;
                    this.mouseDownRight = false;
                    break;
            }
            this._getMouseCanvasPos(e);
            this.fire("click", this.mouseCanvasPos, true);
            if (this.mouseover) {
                e.preventDefault();
            }
        });

        document.addEventListener("dblclick", this._dblClickListener = (e) => {
            if (!this.enabled) {
                return;
            }
            switch (e.which) {
                case 1:// Left button
                    this.mouseDownLeft = false;
                    this.mouseDownRight = false;
                    break;
                case 2:// Middle/both buttons
                    this.mouseDownMiddle = false;
                    break;
                case 3:// Right button
                    this.mouseDownLeft = false;
                    this.mouseDownRight = false;
                    break;
            }
            this._getMouseCanvasPos(e);
            this.fire("dblclick", this.mouseCanvasPos, true);
            if (this.mouseover) {
                e.preventDefault();
            }
        });

        const tickifedMouseMoveFn = this.scene.tickify(
            () => this.fire("mousemove", this.mouseCanvasPos, true)
        );

        this.element.addEventListener("mousemove", this._mouseMoveListener = (e) => {
            if (!this.enabled) {
                return;
            }
            this._getMouseCanvasPos(e);
            tickifedMouseMoveFn();  
            if (this.mouseover) {
                e.preventDefault();
            }
        });

        this.element.addEventListener("contextmenu", this._contextmenuListener = (e) => {
            if (!this.enabled) {
                return;
            }
            this._getMouseCanvasPos(e);
            this.fire("contextmenu", this.mouseCanvasPos, true);
        });

        const tickifiedMouseWheelFn = this.scene.tickify(
            (delta) => { this.fire("mousewheel", delta, true); }
        );

        this.element.addEventListener("wheel", this._mouseWheelListener = (e, d) => {
            if (!this.enabled) {
                return;
            }
            const delta = Math.max(-1, Math.min(1, -e.deltaY * 40));
            tickifiedMouseWheelFn(delta);
        }, {passive: true});

        // mouseclicked

        {
            let downX;
            let downY;
            // Tolerance between down and up positions for a mouse click
            const tolerance = 2;
            this.on("mousedown", (params) => {
                downX = params[0];
                downY = params[1];
            });
            this.on("mouseup", (params) => {
                if (downX >= (params[0] - tolerance) &&
                    downX <= (params[0] + tolerance) &&
                    downY >= (params[1] - tolerance) &&
                    downY <= (params[1] + tolerance)) {
                    this.fire("mouseclicked", params, true);
                }
            });
        }

        this.element.addEventListener("touchstart", this._touchstartListener = (e) => {
            if (!this.enabled) {
                return;
            }
            [...e.changedTouches].forEach(e => {
                this.fire("touchstart", [ e.identifier, this._getTouchCanvasPos(e) ], true);
            });
        });

        this.element.addEventListener("touchend", this._touchendListener = (e) => {
            if (!this.enabled) {
                return;
            }
            [...e.changedTouches].forEach(e => {
                this.fire("touchend", [ e.identifier, this._getTouchCanvasPos(e) ], true);
            });
        });

        this._eventsBound = true;
    }

    _unbindEvents() {
        if (!this._eventsBound) {
            return;
        }
        this._keyboardEventsElement.removeEventListener("keydown", this._keyDownListener);
        this._keyboardEventsElement.removeEventListener("keyup", this._keyUpListener);
        this.element.removeEventListener("mouseenter", this._mouseEnterListener);
        this.element.removeEventListener("mouseleave", this._mouseLeaveListener);
        this.element.removeEventListener("mousedown", this._mouseDownListener);
        document.removeEventListener("mouseup", this._mouseDownListener);
        document.removeEventListener("click", this._clickListener);
        document.removeEventListener("dblclick", this._dblClickListener);
        this.element.removeEventListener("mousemove", this._mouseMoveListener);
        this.element.removeEventListener("contextmenu", this._contextmenuListener);
        this.element.removeEventListener("wheel", this._mouseWheelListener);
        this.element.removeEventListener("touchstart", this._touchstartListener);
        this.element.removeEventListener("touchend", this._touchendListener);
        if (window.OrientationChangeEvent) {
            window.removeEventListener('orientationchange', this._orientationchangedListener);
        }
        if (window.DeviceMotionEvent) {
            window.removeEventListener('devicemotion', this._deviceMotionListener);
        }
        if (window.DeviceOrientationEvent) {
            window.removeEventListener("deviceorientation", this._deviceOrientListener);
        }
        this._eventsBound = false;
    }

    _getTouchCanvasPos(event) {
        let element = event.target;
        let totalOffsetLeft = 0;
        let totalOffsetTop = 0;
        while (element.offsetParent) {
            totalOffsetLeft += element.offsetLeft;
            totalOffsetTop += element.offsetTop;
            element = element.offsetParent;
        }
        return [ event.pageX - totalOffsetLeft, event.pageY - totalOffsetTop ];
    }

    _getMouseCanvasPos(event) {
        if (!event) {
            event = window.event;
            this.mouseCanvasPos[0] = event.x;
            this.mouseCanvasPos[1] = event.y;
        } else {
            let element = event.target;

            const rect = element.getBoundingClientRect();

            this.mouseCanvasPos[0] = event.clientX - rect.left;
            this.mouseCanvasPos[1] = event.clientY - rect.top;
        }
    }

    /**
     * Sets whether input handlers are enabled.
     *
     * Default value is ````true````.
     *
     * @param {Boolean} enable Indicates if input handlers are enabled.
     */
    setEnabled(enable) {
        if (this.enabled !== enable) {
            this.fire("enabled", this.enabled = enable);
        }
    }

    /**
     * Gets whether input handlers are enabled.
     *
     * Default value is ````true````.
     *
     * @returns {Boolean} Indicates if input handlers are enabled.
     */
    getEnabled() {
        return this.enabled;
    }

    /**
     * Sets whether or not keyboard input is enabled.
     *
     * Default value is ````true````.
     *
     * {@link CameraControl} will not respond to keyboard events while this is set ````false````.
     *
     * @param {Boolean} value Indicates whether keyboard input is enabled.
     */
    setKeyboardEnabled(value) {
        this.keyboardEnabled = value;
    }

    /**
     * Gets whether keyboard input is enabled.
     *
     * Default value is ````true````.
     *
     * {@link CameraControl} will not respond to keyboard events while this is set ````false````.
     *
     * @returns {Boolean} Returns whether keyboard input is enabled.
     */
    getKeyboardEnabled() {
        return this.keyboardEnabled;
    }

    /**
     * @private
     */
    destroy() {
        super.destroy();
        this._unbindEvents();
    }
}

/**
 * @desc controls the canvas viewport for a {@link Scene}.
 *
 * * One Viewport per scene.
 * * You can configure a Scene to render multiple times per frame, while setting the Viewport to different extents on each render.
 * * Make a Viewport automatically size to its {@link Scene} {@link Canvas} by setting its {@link Viewport#autoBoundary} ````true````.
 *
 *
 * Configuring the Scene to render twice on each frame, each time to a separate viewport:
 *
 * ````Javascript
 * // Load glTF model
 * var model = new xeokit.GLTFModel({
    src: "models/gltf/GearboxAssy/glTF-MaterialsCommon/GearboxAssy.gltf"
 });

 var scene = model.scene;
 var viewport = scene.viewport;

 // Configure Scene to render twice for each frame
 scene.passes = 2; // Default is 1
 scene.clearEachPass = false; // Default is false

 // Render to a separate viewport on each render

 var viewport = scene.viewport;
 viewport.autoBoundary = false;

 scene.on("rendering", function (e) {
     switch (e.pass) {
         case 0:
             viewport.boundary = [0, 0, 200, 200]; // xmin, ymin, width, height
             break;

         case 1:
             viewport.boundary = [200, 0, 200, 200];
             break;
     }
 });
 ````

 @class Viewport
 @module xeokit
 @submodule rendering
 @constructor
 @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well.
 @param {*} [cfg] Viewport configuration
 @param {String} [cfg.id] Optional ID, unique among all components in the parent
 {@link Scene}, generated automatically when omitted.
 @param {String:Object} [cfg.meta] Optional map of user-defined metadata to attach to this Viewport.
 @param [cfg.boundary] {Number[]} Canvas-space Viewport boundary, given as
 (min, max, width, height). Defaults to the size of the parent
 {@link Scene} {@link Canvas}.
 @param [cfg.autoBoundary=false] {Boolean} Indicates if this Viewport's {@link Viewport#boundary}
 automatically synchronizes with the size of the parent {@link Scene} {@link Canvas}.

 @extends Component
 */

class Viewport extends Component {

    /**
     @private
     */
    get type() {
        return "Viewport";
    }

    /**
     @private
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._state = new RenderState({
            boundary: [0, 0, 100, 100]
        });

        this.boundary = cfg.boundary;
        this.autoBoundary = cfg.autoBoundary;
    }


    /**
     * Sets the canvas-space boundary of this Viewport, indicated as ````[min, max, width, height]````.
     *
     * When {@link Viewport#autoBoundary} is ````true````, ignores calls to this method and automatically synchronizes with {@link Canvas#boundary}.
     *
     * Fires a "boundary"" event on change.
     *
     * Defaults to the {@link Canvas} extents.
     *
     * @param {Number[]} value New Viewport extents.
     */
    set boundary(value) {

        if (this._autoBoundary) {
            return;
        }

        if (!value) {

            const canvasBoundary = this.scene.canvas.boundary;

            const width = canvasBoundary[2];
            const height = canvasBoundary[3];

            value = [0, 0, width, height];
        }

        this._state.boundary = value;

        this.glRedraw();

        /**
         Fired whenever this Viewport's {@link Viewport#boundary} property changes.

         @event boundary
         @param value {Boolean} The property's new value
         */
        this.fire("boundary", this._state.boundary);
    }

    /**
     * Gets the canvas-space boundary of this Viewport, indicated as ````[min, max, width, height]````.
     *
     * @returns {Number[]} The Viewport extents.
     */
    get boundary() {
        return this._state.boundary;
    }

    /**
     * Sets if {@link Viewport#boundary} automatically synchronizes with {@link Canvas#boundary}.
     *
     * Default is ````false````.
     *
     * @param {Boolean} value Set true to automatically sycnhronize.
     */
    set autoBoundary(value) {

        value = !!value;

        if (value === this._autoBoundary) {
            return;
        }

        this._autoBoundary = value;

        if (this._autoBoundary) {
            this._onCanvasSize = this.scene.canvas.on("boundary",
                function (boundary) {

                    const width = boundary[2];
                    const height = boundary[3];

                    this._state.boundary = [0, 0, width, height];

                    this.glRedraw();

                    /**
                     Fired whenever this Viewport's {@link Viewport#boundary} property changes.

                     @event boundary
                     @param value {Boolean} The property's new value
                     */
                    this.fire("boundary", this._state.boundary);

                }, this);

        } else if (this._onCanvasSize) {
            this.scene.canvas.off(this._onCanvasSize);
            this._onCanvasSize = null;
        }

        /**
         Fired whenever this Viewport's {@link autoBoundary/autoBoundary} property changes.

         @event autoBoundary
         @param value The property's new value
         */
        this.fire("autoBoundary", this._autoBoundary);
    }

    /**
     * Gets if {@link Viewport#boundary} automatically synchronizes with {@link Canvas#boundary}.
     *
     * Default is ````false````.
     *
     * @returns {Boolean} Returns ````true```` when automatically sycnhronizing.
     */
    get autoBoundary() {
        return this._autoBoundary;
    }

    _getState() {
        return this._state;
    }

    /**
     * @private
     */
    destroy() {
        super.destroy();
        this._state.destroy();
    }
}

/**
 * @desc Defines its {@link Camera}'s perspective projection using a field-of-view angle.
 *
 * * Located at {@link Camera#perspective}.
 * * Implicitly sets the left, right, top, bottom frustum planes using {@link Perspective#fov}.
 * * {@link Perspective#near} and {@link Perspective#far} specify the distances to the WebGL clipping planes.
 */
class Perspective extends Component {

    /**
     @private
     */
    get type() {
        return "Perspective";
    }

    /**
     * @constructor
     * @private
     */
    constructor(camera, cfg = {}) {

        super(camera, cfg);

        /**
         * The Camera this Perspective belongs to.
         *
         * @property camera
         * @type {Camera}
         * @final
         */
        this.camera = camera;

        this._state = new RenderState({
            matrix: math.mat4(),
            inverseMatrix: math.mat4(),
            transposedMatrix: math.mat4(),
            near: 0.1,
            far: 10000.0
        });

        this._inverseMatrixDirty = true;
        this._transposedMatrixDirty = true;

        this._fov = 60.0;

        // Recompute aspect from change in canvas size
        this._canvasResized = this.scene.canvas.on("boundary", this._needUpdate, this);

        this.fov = cfg.fov;
        this.fovAxis = cfg.fovAxis;
        this.near = cfg.near;
        this.far = cfg.far;
    }

    _update() {

        const WIDTH_INDEX = 2;
        const HEIGHT_INDEX = 3;
        const boundary = this.scene.canvas.boundary;
        const aspect = boundary[WIDTH_INDEX] / boundary[HEIGHT_INDEX];
        const fovAxis = this._fovAxis;

        let fov = this._fov;
        if (fovAxis === "x" || (fovAxis === "min" && aspect < 1) || (fovAxis === "max" && aspect > 1)) {
            fov = fov / aspect;
        }
        fov = Math.min(fov, 120);

        math.perspectiveMat4(fov * (Math.PI / 180.0), aspect, this._state.near, this._state.far, this._state.matrix);

        this._inverseMatrixDirty = true;
        this._transposedMatrixDirty = true;

        this.glRedraw();

        this.camera._updateScheduled = true;

        this.fire("matrix", this._state.matrix);
    }

    /**
     * Sets the Perspective's field-of-view angle (FOV).
     *
     * Fires an "fov" event on change.

     * Default value is ````60.0````.
     *
     * @param {Number} value New field-of-view.
     */
    set fov(value) {
        value = (value !== undefined && value !== null) ? value : 60.0;
        if (value === this._fov) {
            return;
        }
        this._fov = value;
        this._needUpdate(0); // Ensure matrix built on next "tick"
        this.fire("fov", this._fov);
    }

    /**
     * Gets the Perspective's field-of-view angle (FOV).
     *
     * Default value is ````60.0````.
     *
     * @returns {Number} Current field-of-view.
     */
    get fov() {
        return this._fov;
    }

    /**
     * Sets the Perspective's FOV axis.
     *
     * Options are ````"x"````, ````"y"```` or ````"min"````, to use the minimum axis.
     *
     * Fires an "fovAxis" event on change.

     * Default value ````"min"````.
     *
     * @param {String} value New FOV axis value.
     */
    set fovAxis(value) {
        value = value || "min";
        if (this._fovAxis === value) {
            return;
        }
        if (value !== "x" && value !== "y" && value !== "min") {
            this.error("Unsupported value for 'fovAxis': " + value + " - defaulting to 'min'");
            value = "min";
        }
        this._fovAxis = value;
        this._needUpdate(0); // Ensure matrix built on next "tick"
        this.fire("fovAxis", this._fovAxis);
    }

    /**
     * Gets the Perspective's FOV axis.
     *
     * Options are ````"x"````, ````"y"```` or ````"min"````, to use the minimum axis.
     *
     * Fires an "fovAxis" event on change.

     * Default value is ````"min"````.
     *
     * @returns {String} The current FOV axis value.
     */
    get fovAxis() {
        return this._fovAxis;
    }

    /**
     * Sets the position of the Perspective's near plane on the positive View-space Z-axis.
     *
     * Fires a "near" event on change.
     *
     * Default value is ````0.1````.
     *
     * @param {Number} value New Perspective near plane position.
     */
    set near(value) {
        const near = (value !== undefined && value !== null) ? value : 0.1;
        if (this._state.near === near) {
            return;
        }
        this._state.near = near;
        this._needUpdate(0); // Ensure matrix built on next "tick"
        this.fire("near", this._state.near);
    }

    /**
     * Gets the position of the Perspective's near plane on the positive View-space Z-axis.
     *
     * Fires an "emits" emits on change.
     *
     * Default value is ````0.1````.
     *
     * @returns The Perspective's near plane position.
     */
    get near() {
        return this._state.near;
    }

    /**
     * Sets the position of this Perspective's far plane on the positive View-space Z-axis.
     *
     * Fires a "far" event on change.
     *
     * Default value is ````10000.0````.
     *
     * @param {Number} value New Perspective far plane position.
     */
    set far(value) {
        const far = (value !== undefined && value !== null) ? value : 10000.0;
        if (this._state.far === far) {
            return;
        }
        this._state.far = far;
        this._needUpdate(0); // Ensure matrix built on next "tick"
        this.fire("far", this._state.far);
    }

    /**
     * Gets the position of this Perspective's far plane on the positive View-space Z-axis.
     *
     * Default value is ````10000.0````.
     *
     * @return {Number} The Perspective's far plane position.
     */
    get far() {
        return this._state.far;
    }

    /**
     * Gets the Perspective's projection transform matrix.
     *
     * Fires a "matrix" event on change.
     *
     * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````.
     *
     * @returns {Number[]} The Perspective's projection matrix.
     */
    get matrix() {
        if (this._updateScheduled) {
            this._doUpdate();
        }
        return this._state.matrix;
    }

    /**
     * Gets the inverse of {@link Perspective#matrix}.
     *
     * @returns {Number[]} The inverse of {@link Perspective#matrix}.
     */
    get inverseMatrix() {
        if (this._updateScheduled) {
            this._doUpdate();
        }
        if (this._inverseMatrixDirty) {
            math.inverseMat4(this._state.matrix, this._state.inverseMatrix);
            this._inverseMatrixDirty = false;
        }
        return this._state.inverseMatrix;
    }

    /**
     * Gets the transpose of {@link Perspective#matrix}.
     *
     * @returns {Number[]} The transpose of {@link Perspective#matrix}.
     */
    get transposedMatrix() {
        if (this._updateScheduled) {
            this._doUpdate();
        }
        if (this._transposedMatrixDirty) {
            math.transposeMat4(this._state.matrix, this._state.transposedMatrix);
            this._transposedMatrixDirty = false;
        }
        return this._state.transposedMatrix;
    }

    /**
     * Un-projects the given Canvas-space coordinates and Screen-space depth, using this Perspective projection.
     *
     * @param {Number[]} canvasPos Inputs 2D Canvas-space coordinates.
     * @param {Number} screenZ Inputs Screen-space Z coordinate.
     * @param {Number[]} screenPos Outputs 3D Screen/Clip-space coordinates.
     * @param {Number[]} viewPos Outputs un-projected 3D View-space coordinates.
     * @param {Number[]} worldPos Outputs un-projected 3D World-space coordinates.
     */
    unproject(canvasPos, screenZ, screenPos, viewPos, worldPos) {

        const canvas = this.scene.canvas.canvas;

        const halfCanvasWidth = canvas.offsetWidth / 2.0;
        const halfCanvasHeight = canvas.offsetHeight / 2.0;

        screenPos[0] = (canvasPos[0] - halfCanvasWidth) / halfCanvasWidth;
        screenPos[1] = (canvasPos[1] - halfCanvasHeight) / halfCanvasHeight;
        screenPos[2] = screenZ;
        screenPos[3] = 1.0;

        math.mulMat4v4(this.inverseMatrix, screenPos, viewPos);
        math.mulVec3Scalar(viewPos, 1.0 / viewPos[3]);

        viewPos[3] = 1.0;
        viewPos[1] *= -1;

        math.mulMat4v4(this.camera.inverseViewMatrix, viewPos, worldPos);

        return worldPos;
    }

    /** @private
     *
     */
    destroy() {
        super.destroy();
        this._state.destroy();
        this.scene.canvas.off(this._canvasResized);
    }
}

/**
 * @desc Defines its {@link Camera}'s orthographic projection as a box-shaped view volume.
 *
 * * Located at {@link Camera#ortho}.
 * * Works like Blender's orthographic projection, where the positions of the left, right, top and bottom planes are implicitly
 * indicated with a single {@link Ortho#scale} property, which causes the frustum to be symmetrical on X and Y axis, large enough to
 * contain the number of units given by {@link Ortho#scale}.
 * * {@link Ortho#near} and {@link Ortho#far} indicated the distances to the WebGL clipping planes.
 */
class Ortho extends Component {

    /**
     @private
     */
    get type() {
        return "Ortho";
    }

    /**
     * @constructor
     * @private
     */
    constructor(camera, cfg = {}) {

        super(camera, cfg);

        /**
         * The Camera this Ortho belongs to.
         *
         * @property camera
         * @type {Camera}
         * @final
         */
        this.camera = camera;

        this._state = new RenderState({
            matrix: math.mat4(),
            inverseMatrix: math.mat4(),
            transposedMatrix: math.mat4(),
            near: 0.1,
            far: 10000.0
        });

        this._inverseMatrixDirty = true;
        this._transposedMatrixDirty = true;

        this.scale = cfg.scale;
        this.near = cfg.near;
        this.far = cfg.far;

        this._onCanvasBoundary = this.scene.canvas.on("boundary", this._needUpdate, this);
    }

    _update() {

        const WIDTH_INDEX = 2;
        const HEIGHT_INDEX = 3;

        const scene = this.scene;
        const scale = this._scale;
        const halfSize = 0.5 * scale;

        const boundary = scene.canvas.boundary;
        const boundaryWidth = boundary[WIDTH_INDEX];
        const boundaryHeight = boundary[HEIGHT_INDEX];
        const aspect = boundaryWidth / boundaryHeight;

        let left;
        let right;
        let top;
        let bottom;

        if (boundaryWidth > boundaryHeight) {
            left = -halfSize;
            right = halfSize;
            top = halfSize / aspect;
            bottom = -halfSize / aspect;

        } else {
            left = -halfSize * aspect;
            right = halfSize * aspect;
            top = halfSize;
            bottom = -halfSize;
        }

        math.orthoMat4c(left, right, bottom, top, this._state.near, this._state.far, this._state.matrix);

        this._inverseMatrixDirty = true;
        this._transposedMatrixDirty = true;

        this.glRedraw();

        this.fire("matrix", this._state.matrix);
    }


    /**
     * Sets scale factor for this Ortho's extents on X and Y axis.
     *
     * Clamps to minimum value of ````0.01```.
     *
     * Fires a "scale" event on change.
     *
     * Default value is ````1.0````
     * @param {Number} value New scale value.
     */
    set scale(value) {
        if (value === undefined || value === null) {
            value = 1.0;
        }
        if (value <= 0) {
            value = 0.01;
        }
        this._scale = value;
        this._needUpdate(0);
        this.fire("scale", this._scale);
    }

    /**
     * Gets scale factor for this Ortho's extents on X and Y axis.
     *
     * Clamps to minimum value of ````0.01```.
     *
     * Default value is ````1.0````
     *
     * @returns {Number} New Ortho scale value.
     */
    get scale() {
        return this._scale;
    }

    /**
     * Sets the position of the Ortho's near plane on the positive View-space Z-axis.
     *
     * Fires a "near" emits on change.
     *
     * Default value is ````0.1````.
     *
     * @param {Number} value New Ortho near plane position.
     */
    set near(value) {
        const near = (value !== undefined && value !== null) ? value : 0.1;
        if (this._state.near === near) {
            return;
        }
        this._state.near = near;
        this._needUpdate(0);
        this.fire("near", this._state.near);
    }

    /**
     * Gets the position of the Ortho's near plane on the positive View-space Z-axis.
     *
     * Default value is ````0.1````.
     *
     * @returns {Number} New Ortho near plane position.
     */
    get near() {
        return this._state.near;
    }

    /**
     * Sets the position of the Ortho's far plane on the positive View-space Z-axis.
     *
     * Fires a "far" event on change.
     *
     * Default value is ````10000.0````.
     *
     * @param {Number} value New far ortho plane position.
     */
    set far(value) {
        const far = (value !== undefined && value !== null) ? value : 10000.0;
        if (this._state.far === far) {
            return;
        }
        this._state.far = far;
        this._needUpdate(0);
        this.fire("far", this._state.far);
    }

    /**
     * Gets the position of the Ortho's far plane on the positive View-space Z-axis.
     *
     * Default value is ````10000.0````.
     *
     * @returns {Number} New far ortho plane position.
     */
    get far() {
        return this._state.far;
    }

    /**
     * Gets the Ortho's projection transform matrix.
     *
     * Fires a "matrix" event on change.
     *
     * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````.
     *
     * @returns {Number[]} The Ortho's projection matrix.
     */
    get matrix() {
        if (this._updateScheduled) {
            this._doUpdate();
        }
        return this._state.matrix;
    }

    /**
     * Gets the inverse of {@link Ortho#matrix}.
     *
     * @returns {Number[]} The inverse of {@link Ortho#matrix}.
     */
    get inverseMatrix() {
        if (this._updateScheduled) {
            this._doUpdate();
        }
        if (this._inverseMatrixDirty) {
            math.inverseMat4(this._state.matrix, this._state.inverseMatrix);
            this._inverseMatrixDirty = false;
        }
        return this._state.inverseMatrix;
    }

    /**
     * Gets the transpose of {@link Ortho#matrix}.
     *
     * @returns {Number[]} The transpose of {@link Ortho#matrix}.
     */
    get transposedMatrix() {
        if (this._updateScheduled) {
            this._doUpdate();
        }
        if (this._transposedMatrixDirty) {
            math.transposeMat4(this._state.matrix, this._state.transposedMatrix);
            this._transposedMatrixDirty = false;
        }
        return this._state.transposedMatrix;
    }

    /**
     * Un-projects the given Canvas-space coordinates, using this Ortho projection.
     *
     * @param {Number[]} canvasPos Inputs 2D Canvas-space coordinates.
     * @param {Number} screenZ Inputs Screen-space Z coordinate.
     * @param {Number[]} screenPos Outputs 3D Screen/Clip-space coordinates.
     * @param {Number[]} viewPos Outputs un-projected 3D View-space coordinates.
     * @param {Number[]} worldPos Outputs un-projected 3D World-space coordinates.
     */
    unproject(canvasPos, screenZ, screenPos, viewPos, worldPos) {

        const canvas = this.scene.canvas.canvas;

        const halfCanvasWidth = canvas.offsetWidth / 2.0;
        const halfCanvasHeight = canvas.offsetHeight / 2.0;

        screenPos[0] = (canvasPos[0] - halfCanvasWidth) / halfCanvasWidth;
        screenPos[1] = (canvasPos[1] - halfCanvasHeight) / halfCanvasHeight;
        screenPos[2] = screenZ;
        screenPos[3] = 1.0;

        math.mulMat4v4(this.inverseMatrix, screenPos, viewPos);
        math.mulVec3Scalar(viewPos, 1.0 / viewPos[3]);

        viewPos[3] = 1.0;
        viewPos[1] *= -1;

        math.mulMat4v4(this.camera.inverseViewMatrix, viewPos, worldPos);

        return worldPos;
    }

    /** @private
     *
     */
    destroy() {
        super.destroy();
        this._state.destroy();
        this.scene.canvas.off(this._onCanvasBoundary);
    }
}

/**
 * @desc Defines its {@link Camera}'s perspective projection as a frustum-shaped view volume.
 *
 * * Located at {@link Camera#frustum}.
 * * Allows to explicitly set the positions of the left, right, top, bottom, near and far planes, which is useful for asymmetrical view volumes, such as for stereo viewing.
 * * {@link Frustum#near} and {@link Frustum#far} specify the distances to the WebGL clipping planes.
 */
class Frustum extends Component {

    /**
     @private
     */
    get type() {
        return "Frustum";
    }

    /**
     * @constructor
     * @private
     */
    constructor(camera, cfg = {}) {

        super(camera, cfg);

        /**
         * The Camera this Frustum belongs to.
         *
         * @property camera
         * @type {Camera}
         * @final
         */
        this.camera = camera;

        this._state = new RenderState({
            matrix: math.mat4(),
            inverseMatrix: math.mat4(),
            transposedMatrix: math.mat4(),
            near: 0.1,
            far: 10000.0
        });

        this._left = -1;
        this._right = 1.0;
        this._bottom = -1;
        this._top = 1.0;

        this._inverseMatrixDirty = true;
        this._transposedMatrixDirty = true;

        // Set component properties

        this.left = cfg.left;
        this.right = cfg.right;
        this.bottom = cfg.bottom;
        this.top = cfg.top;
        this.near = cfg.near;
        this.far = cfg.far;
    }

    _update() {

        math.frustumMat4(this._left, this._right, this._bottom, this._top, this._state.near, this._state.far, this._state.matrix);

        this._inverseMatrixDirty = true;
        this._transposedMatrixDirty = true;

        this.glRedraw();

        this.fire("matrix", this._state.matrix);
    }

    /**
     * Sets the position of the Frustum's left plane on the View-space X-axis.
     *
     * Fires a {@link Frustum#left:emits} emits on change.
     *
     * @param {Number} value New left frustum plane position.
     */
    set left(value) {
        this._left = (value !== undefined && value !== null) ? value : -1;
        this._needUpdate(0);
        this.fire("left", this._left);
    }

    /**
     * Gets the position of the Frustum's left plane on the View-space X-axis.
     *
     * @return {Number} Left frustum plane position.
     */
    get left() {
        return this._left;
    }

    /**
     * Sets the position of the Frustum's right plane on the View-space X-axis.
     *
     * Fires a {@link Frustum#right:emits} emits on change.
     *
     * @param {Number} value New right frustum plane position.
     */
    set right(value) {
        this._right = (value !== undefined && value !== null) ? value : 1.0;
        this._needUpdate(0);
        this.fire("right", this._right);
    }

    /**
     * Gets the position of the Frustum's right plane on the View-space X-axis.
     *
     * Fires a {@link Frustum#right:emits} emits on change.
     *
     * @return {Number} Right frustum plane position.
     */
    get right() {
        return this._right;
    }

    /**
     * Sets the position of the Frustum's top plane on the View-space Y-axis.
     *
     * Fires a {@link Frustum#top:emits} emits on change.
     *
     * @param {Number} value New top frustum plane position.
     */
    set top(value) {
        this._top = (value !== undefined && value !== null) ? value : 1.0;
        this._needUpdate(0);
        this.fire("top", this._top);
    }

    /**
     * Gets the position of the Frustum's top plane on the View-space Y-axis.
     *
     * Fires a {@link Frustum#top:emits} emits on change.
     *
     * @return {Number} Top frustum plane position.
     */
    get top() {
        return this._top;
    }

    /**
     * Sets the position of the Frustum's bottom plane on the View-space Y-axis.
     *
     * Fires a {@link Frustum#bottom:emits} emits on change.
     *
     * @emits {"bottom"} event with the value of this property whenever it changes.
     *
     * @param {Number} value New bottom frustum plane position.
     */
    set bottom(value) {
        this._bottom = (value !== undefined && value !== null) ? value : -1;
        this._needUpdate(0);
        this.fire("bottom", this._bottom);
    }

    /**
     * Gets the position of the Frustum's bottom plane on the View-space Y-axis.
     *
     * Fires a {@link Frustum#bottom:emits} emits on change.
     *
     * @return {Number} Bottom frustum plane position.
     */
    get bottom() {
        return this._bottom;
    }

    /**
     * Sets the position of the Frustum's near plane on the positive View-space Z-axis.
     *
     * Fires a {@link Frustum#near:emits} emits on change.
     *
     * Default value is ````0.1````.
     *
     * @param {Number} value New Frustum near plane position.
     */
    set near(value) {
        this._state.near = (value !== undefined && value !== null) ? value : 0.1;
        this._needUpdate(0);
        this.fire("near", this._state.near);
    }

    /**
     * Gets the position of the Frustum's near plane on the positive View-space Z-axis.
     *
     * Fires a {@link Frustum#near:emits} emits on change.
     *
     * Default value is ````0.1````.
     *
     * @return {Number} Near frustum plane position.
     */
    get near() {
        return this._state.near;
    }

    /**
     * Sets the position of the Frustum's far plane on the positive View-space Z-axis.
     *
     * Fires a {@link Frustum#far:emits} emits on change.
     *
     * Default value is ````10000.0````.
     *
     * @param {Number} value New far frustum plane position.
     */
    set far(value) {
        this._state.far = (value !== undefined && value !== null) ? value : 10000.0;
        this._needUpdate(0);
        this.fire("far", this._state.far);
    }

    /**
     * Gets the position of the Frustum's far plane on the positive View-space Z-axis.
     *
     * Default value is ````10000.0````.
     *
     * @return {Number} Far frustum plane position.
     */
    get far() {
        return this._state.far;
    }

    /**
     * Gets the Frustum's projection transform matrix.
     *
     * Fires a {@link Frustum#matrix:emits} emits on change.
     *
     * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````.
     *
     * @returns {Number[]} The Frustum's projection matrix matrix.
     */
    get matrix() {
        if (this._updateScheduled) {
            this._doUpdate();
        }
        return this._state.matrix;
    }

    /**
     * Gets the inverse of {@link Frustum#matrix}.
     *
     * @returns {Number[]} The inverse orthographic projection matrix.
     */
    get inverseMatrix() {
        if (this._updateScheduled) {
            this._doUpdate();
        }
        if (this._inverseMatrixDirty) {
            math.inverseMat4(this._state.matrix, this._state.inverseMatrix);
            this._inverseMatrixDirty = false;
        }
        return this._state.inverseMatrix;
    }

    /**
     * Gets the transpose of {@link Frustum#matrix}.
     *
     * @returns {Number[]} The transpose of {@link Frustum#matrix}.
     */
    get transposedMatrix() {
        if (this._updateScheduled) {
            this._doUpdate();
        }
        if (this._transposedMatrixDirty) {
            math.transposeMat4(this._state.matrix, this._state.transposedMatrix);
            this._transposedMatrixDirty = false;
        }
        return this._state.transposedMatrix;
    }

    /**
     * Un-projects the given Canvas-space coordinates, using this Frustum projection.
     *
     * @param {Number[]} canvasPos Inputs 2D Canvas-space coordinates.
     * @param {Number} screenZ Inputs Screen-space Z coordinate.
     * @param {Number[]} screenPos Outputs 3D Screen/Clip-space coordinates.
     * @param {Number[]} viewPos Outputs un-projected 3D View-space coordinates.
     * @param {Number[]} worldPos Outputs un-projected 3D World-space coordinates.
     */
    unproject(canvasPos, screenZ, screenPos, viewPos, worldPos) {

        const canvas = this.scene.canvas.canvas;

        const halfCanvasWidth = canvas.offsetWidth / 2.0;
        const halfCanvasHeight = canvas.offsetHeight / 2.0;

        screenPos[0] = (canvasPos[0] - halfCanvasWidth) / halfCanvasWidth;
        screenPos[1] = (canvasPos[1] - halfCanvasHeight) / halfCanvasHeight;
        screenPos[2] = screenZ;
        screenPos[3] = 1.0;

        math.mulMat4v4(this.inverseMatrix, screenPos, viewPos);
        math.mulVec3Scalar(viewPos, 1.0 / viewPos[3]);

        viewPos[3] = 1.0;
        viewPos[1] *= -1;

        math.mulMat4v4(this.camera.inverseViewMatrix, viewPos, worldPos);

        return worldPos;
    }

    /** @private
     *
     */
    destroy() {
        super.destroy();
        this._state.destroy();
        super.destroy();
    }
}

/**
 * @desc Defines a custom projection for a {@link Camera} as a custom 4x4 matrix..
 *
 * Located at {@link Camera#customProjection}.
 */
class CustomProjection extends Component {

    /**
     * @private
     */
    get type() {
        return "CustomProjection";
    }

    /**
     * @constructor
     * @private
     */
    constructor(camera, cfg = {}) {

        super(camera, cfg);

        /**
         * The Camera this CustomProjection belongs to.
         *
         * @property camera
         * @type {Camera}
         * @final
         */
        this.camera = camera;

        this._state = new RenderState({
            matrix: math.mat4(),
            inverseMatrix: math.mat4(),
            transposedMatrix: math.mat4()
        });

        this._inverseMatrixDirty = true;
        this._transposedMatrixDirty = false;

        this.matrix = cfg.matrix;
    }

    /**
     * Sets the CustomProjection's projection transform matrix.
     *
     * Fires a "matrix" event on change.

     * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````.
     *
     * @param {Number[]} matrix New value for the CustomProjection's matrix.
     */
    set matrix(matrix) {
        this._state.matrix.set(matrix || [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
        this._inverseMatrixDirty = true;
        this._transposedMatrixDirty = true;
        this.glRedraw();
        this.fire("matrix", this._state.matrix);
    }

    /**
     * Gets the CustomProjection's projection transform matrix.
     *
     * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````.
     *
     * @return {Number[]} New value for the CustomProjection's matrix.
     */
    get matrix() {
        return this._state.matrix;
    }

    /**
     * Gets the inverse of {@link CustomProjection#matrix}.
     *
     * @returns {Number[]} The inverse of {@link CustomProjection#matrix}.
     */
    get inverseMatrix() {
        if (this._updateScheduled) {
            this._doUpdate();
        }
        if (this._inverseMatrixDirty) {
            math.inverseMat4(this._state.matrix, this._state.inverseMatrix);
            this._inverseMatrixDirty = false;
        }
        return this._state.inverseMatrix;
    }

    /**
     * Gets the transpose of {@link CustomProjection#matrix}.
     *
     * @returns {Number[]} The transpose of {@link CustomProjection#matrix}.
     */
    get transposedMatrix() {
        if (this._updateScheduled) {
            this._doUpdate();
        }
        if (this._transposedMatrixDirty) {
            math.transposeMat4(this._state.matrix, this._state.transposedMatrix);
            this._transposedMatrixDirty = false;
        }
        return this._state.transposedMatrix;
    }

    /**
     * Un-projects the given Canvas-space coordinates, using this CustomProjection.
     *
     * @param {Number[]} canvasPos Inputs 2D Canvas-space coordinates.
     * @param {Number} screenZ Inputs Screen-space Z coordinate.
     * @param {Number[]} screenPos Outputs 3D Screen/Clip-space coordinates.
     * @param {Number[]} viewPos Outputs un-projected 3D View-space coordinates.
     * @param {Number[]} worldPos Outputs un-projected 3D World-space coordinates.
     */
    unproject(canvasPos, screenZ, screenPos, viewPos, worldPos) {

        const canvas = this.scene.canvas.canvas;

        const halfCanvasWidth = canvas.offsetWidth / 2.0;
        const halfCanvasHeight = canvas.offsetHeight / 2.0;

        screenPos[0] = (canvasPos[0] - halfCanvasWidth) / halfCanvasWidth;
        screenPos[1] = (canvasPos[1] - halfCanvasHeight) / halfCanvasHeight;
        screenPos[2] = screenZ;
        screenPos[3] = 1.0;

        math.mulMat4v4(this.inverseMatrix, screenPos, viewPos);
        math.mulVec3Scalar(viewPos, 1.0 / viewPos[3]);

        viewPos[3] = 1.0;
        viewPos[1] *= -1;

        math.mulMat4v4(this.camera.inverseViewMatrix, viewPos, worldPos);

        return worldPos;
    }

    /** @private
     *
     */
    destroy() {
        super.destroy();
        this._state.destroy();
    }
}

const tempVec3$4$1 = math.vec3();
const tempVec3b$8 = math.vec3();
const tempVec3c$7 = math.vec3();
const tempVec3d$2 = math.vec3();
const tempVec3e = math.vec3();
const tempVec3f = math.vec3();
const tempVec4a$7 = math.vec4();
const tempVec4b$7 = math.vec4();
const tempVec4c$2 = math.vec4();
const tempMat = math.mat4();
const tempMatb = math.mat4();
const eyeLookVec = math.vec3();
const eyeLookVecNorm = math.vec3();
const eyeLookOffset = math.vec3();
const offsetEye = math.vec3();

/**
 * @desc Manages viewing and projection transforms for its {@link Scene}.
 *
 * * One Camera per {@link Scene}
 * * Scene is located at {@link Viewer#scene} and Camera is located at {@link Scene#camera}
 * * Controls viewing and projection transforms
 * * Has methods to pan, zoom and orbit (or first-person rotation)
 * * Dynamically configurable World-space axis
 * * Has {@link Perspective}, {@link Ortho} and {@link Frustum} and {@link CustomProjection}, which you can dynamically switch it between
 * * Switchable gimbal lock
 * * Can be "flown" to look at targets using a {@link CameraFlightAnimation}
 * * Can be animated along a path using a {@link CameraPathAnimation}
 *
 * ## Getting the Camera
 *
 * There is exactly one Camera per {@link Scene}:
 *
 * ````javascript
 * import {Viewer} from "xeokit-sdk.es.js";
 *
 * var camera = viewer.scene.camera;
 *
 * ````
 *
 * ## Setting the Camera Position
 *
 * Get and set the Camera's absolute position via {@link Camera#eye}, {@link Camera#look} and {@link Camera#up}:
 *
 * ````javascript
 * camera.eye = [-10,0,0];
 * camera.look = [-10,0,0];
 * camera.up = [0,1,0];
 * ````
 *
 * ## Camera View and Projection Matrices
 *
 * The Camera's view matrix transforms coordinates from World-space to View-space.
 *
 * Getting the view matrix:
 *
 * ````javascript
 * var viewMatrix = camera.viewMatrix;
 * var viewNormalMatrix = camera.normalMatrix;
 * ````
 *
 * The Camera's view normal matrix transforms normal vectors from World-space to View-space.
 *
 * Getting the view normal matrix:
 *
 * ````javascript
 * var viewNormalMatrix = camera.normalMatrix;
 * ````
 *
 * The Camera fires a ````"viewMatrix"```` event whenever the {@link Camera#viewMatrix} and {@link Camera#viewNormalMatrix} updates.
 *
 * Listen for view matrix updates:
 *
 * ````javascript
 * camera.on("viewMatrix", function(matrix) { ... });
 * ````
 *
 * ## Rotating the Camera
 *
 * Orbiting the {@link Camera#look} position:
 *
 * ````javascript
 * camera.orbitYaw(20.0);
 * camera.orbitPitch(10.0);
 * ````
 *
 * First-person rotation, rotates {@link Camera#look} and {@link Camera#up} about {@link Camera#eye}:
 *
 * ````javascript
 * camera.yaw(5.0);
 * camera.pitch(-10.0);
 * ````
 *
 * ## Panning the Camera
 *
 * Panning along the Camera's local axis (ie. left/right, up/down, forward/backward):
 *
 * ````javascript
 * camera.pan([-20, 0, 10]);
 * ````
 *
 * ## Zooming the Camera
 *
 * Zoom to vary distance between {@link Camera#eye} and {@link Camera#look}:
 *
 * ````javascript
 * camera.zoom(-5); // Move five units closer
 * ````
 *
 * Get the current distance between {@link Camera#eye} and {@link Camera#look}:
 *
 * ````javascript
 * var distance = camera.eyeLookDist;
 * ````
 *
 * ## Projection
 *
 * The Camera has a Component to manage each projection type, which are: {@link Perspective}, {@link Ortho}
 * and {@link Frustum} and {@link CustomProjection}.
 *
 * You can configure those components at any time, regardless of which is currently active:
 *
 * The Camera has a {@link Perspective} to manage perspective
 * ````javascript
 *
 * // Set some properties on Perspective
 * camera.perspective.near = 0.4;
 * camera.perspective.fov = 45;
 *
 * // Set some properties on Ortho
 * camera.ortho.near = 0.8;
 * camera.ortho.far = 1000;
 *
 * // Set some properties on Frustum
 * camera.frustum.left = -1.0;
 * camera.frustum.right = 1.0;
 * camera.frustum.far = 1000.0;
 *
 * // Set the matrix property on CustomProjection
 * camera.customProjection.matrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
 *
 * // Switch between the projection types
 * camera.projection = "perspective"; // Switch to perspective
 * camera.projection = "frustum"; // Switch to frustum
 * camera.projection = "ortho"; // Switch to ortho
 * camera.projection = "customProjection"; // Switch to custom
 * ````
 *
 * Camera provides the projection matrix for the currently active projection in {@link Camera#projMatrix}.
 *
 * Get the projection matrix:
 *
 * ````javascript
 * var projMatrix = camera.projMatrix;
 * ````
 *
 * Listen for projection matrix updates:
 *
 * ````javascript
 * camera.on("projMatrix", function(matrix) { ... });
 * ````
 *
 * ## Configuring World up direction
 *
 * We can dynamically configure the directions of the World-space coordinate system.
 *
 * Setting the +Y axis as World "up", +X as right and -Z as forwards (convention in some modeling software):
 *
 * ````javascript
 * camera.worldAxis = [
 *     1, 0, 0,    // Right
 *     0, 1, 0,    // Up
 *     0, 0,-1     // Forward
 * ];
 * ````
 *
 * Setting the +Z axis as World "up", +X as right and -Y as "up" (convention in most CAD and BIM viewers):
 *
 * ````javascript
 * camera.worldAxis = [
 *     1, 0, 0, // Right
 *     0, 0, 1, // Up
 *     0,-1, 0  // Forward
 * ];
 * ````
 *
 * The Camera has read-only convenience properties that provide each axis individually:
 *
 * ````javascript
 * var worldRight = camera.worldRight;
 * var worldForward = camera.worldForward;
 * var worldUp = camera.worldUp;
 * ````
 *
 * ### Gimbal locking
 *
 * By default, the Camera locks yaw rotation to pivot about the World-space "up" axis. We can dynamically lock and unlock that at any time:
 *
 * ````javascript
 * camera.gimbalLock = false; // Yaw rotation now happens about Camera's local Y-axis
 * camera.gimbalLock = true; // Yaw rotation now happens about World's "up" axis
 * ````
 *
 * See: <a href="https://en.wikipedia.org/wiki/Gimbal_lock">https://en.wikipedia.org/wiki/Gimbal_lock</a>
 */
class Camera extends Component {

    /**
     @private
     */
    get type() {
        return "Camera";
    }

    /**
     * @constructor
     * @private
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._state = new RenderState({
            offsetEyeFromLook: true, // TODO: treat as legacy and get rid of at some later point
            deviceMatrix: math.mat4(),
            hasDeviceMatrix: false, // True when deviceMatrix set to other than identity
            matrix: math.mat4(),
            normalMatrix: math.mat4(),
            inverseMatrix: math.mat4()
        });

        this._perspective = new Perspective(this);
        this._ortho = new Ortho(this);
        this._frustum = new Frustum(this);
        this._customProjection = new CustomProjection(this);
        this._project = this._perspective;

        this._eye = math.vec3([0, 0, 10.0]);
        this._look = math.vec3([0, 0, 0]);
        this._up = math.vec3([0, 1, 0]);

        this._worldUp = math.vec3([0, 1, 0]);
        this._worldRight = math.vec3([1, 0, 0]);
        this._worldForward = math.vec3([0, 0, -1]);

        this.deviceMatrix = cfg.deviceMatrix;
        this.eye = cfg.eye;
        this.look = cfg.look;
        this.up = cfg.up;
        this.worldAxis = cfg.worldAxis;
        this.gimbalLock = cfg.gimbalLock;
        this.constrainPitch = cfg.constrainPitch;

        this.projection = cfg.projection;

        this._perspective.on("matrix", () => {
            if (this._projectionType === "perspective") {
                this.fire("projMatrix", this._perspective.matrix);
            }
        });
        this._ortho.on("matrix", () => {
            if (this._projectionType === "ortho") {
                this.fire("projMatrix", this._ortho.matrix);
            }
        });
        this._frustum.on("matrix", () => {
            if (this._projectionType === "frustum") {
                this.fire("projMatrix", this._frustum.matrix);
            }
        });
        this._customProjection.on("matrix", () => {
            if (this._projectionType === "customProjection") {
                this.fire("projMatrix", this._customProjection.matrix);
            }
        });
    }

    _update() {
        const state = this._state;
        // In ortho mode, build the view matrix with an eye position that's translated
        // well back from look, so that the front sectionPlane plane doesn't unexpectedly cut
        // the front off the view (not a problem with perspective, since objects close enough
        // to be clipped by the front plane are usually too big to see anything of their cross-sections).
        let eye;
        if (this.projection === "ortho" && this._state.offsetEyeFromLook) {
            math.subVec3(this._eye, this._look, eyeLookVec);
            math.normalizeVec3(eyeLookVec, eyeLookVecNorm);
            math.mulVec3Scalar(eyeLookVecNorm, 1000.0, eyeLookOffset);
            math.addVec3(this._look, eyeLookOffset, offsetEye);
            eye = offsetEye;
        } else {
            eye = this._eye;
        }
        if (state.hasDeviceMatrix) {
            math.lookAtMat4v(eye, this._look, this._up, tempMatb);
            math.mulMat4(state.deviceMatrix, tempMatb, state.matrix);
            //state.matrix.set(state.deviceMatrix);
        } else {
            math.lookAtMat4v(eye, this._look, this._up, state.matrix);
        }
        math.inverseMat4(this._state.matrix, this._state.inverseMatrix);
        math.transposeMat4(this._state.inverseMatrix, this._state.normalMatrix);
        this.glRedraw();
        this.fire("matrix", this._state.matrix);
        this.fire("viewMatrix", this._state.matrix);
    }

    /**
     * Rotates {@link Camera#eye} about {@link Camera#look}, around the {@link Camera#up} vector
     *
     * @param {Number} angleInc Angle of rotation in degrees
     */
    orbitYaw(angleInc) {
        let lookEyeVec = math.subVec3(this._eye, this._look, tempVec3$4$1);
        math.rotationMat4v(angleInc * 0.0174532925, this._gimbalLock ? this._worldUp : this._up, tempMat);
        lookEyeVec = math.transformPoint3(tempMat, lookEyeVec, tempVec3b$8);
        this.eye = math.addVec3(this._look, lookEyeVec, tempVec3c$7); // Set eye position as 'look' plus 'eye' vector
        this.up = math.transformPoint3(tempMat, this._up, tempVec3d$2); // Rotate 'up' vector
    }

    /**
     * Rotates {@link Camera#eye} about {@link Camera#look} around the right axis (orthogonal to {@link Camera#up} and "look").
     *
     * @param {Number} angleInc Angle of rotation in degrees
     */
    orbitPitch(angleInc) {
        if (this._constrainPitch) {
            angleInc = math.dotVec3(this._up, this._worldUp) / math.DEGTORAD;
            if (angleInc < 1) {
                return;
            }
        }
        let eye2 = math.subVec3(this._eye, this._look, tempVec3$4$1);
        const left = math.cross3Vec3(math.normalizeVec3(eye2, tempVec3b$8), math.normalizeVec3(this._up, tempVec3c$7));
        math.rotationMat4v(angleInc * 0.0174532925, left, tempMat);
        eye2 = math.transformPoint3(tempMat, eye2, tempVec3d$2);
        this.up = math.transformPoint3(tempMat, this._up, tempVec3e);
        this.eye = math.addVec3(eye2, this._look, tempVec3f);
    }

    /**
     * Rotates {@link Camera#look} about {@link Camera#eye}, around the {@link Camera#up} vector.
     *
     * @param {Number} angleInc Angle of rotation in degrees
     */
    yaw(angleInc) {
        let look2 = math.subVec3(this._look, this._eye, tempVec3$4$1);
        math.rotationMat4v(angleInc * 0.0174532925, this._gimbalLock ? this._worldUp : this._up, tempMat);
        look2 = math.transformPoint3(tempMat, look2, tempVec3b$8);
        this.look = math.addVec3(look2, this._eye, tempVec3c$7);
        if (this._gimbalLock) {
            this.up = math.transformPoint3(tempMat, this._up, tempVec3d$2);
        }
    }

    /**
     * Rotates {@link Camera#look} about {@link Camera#eye}, around the right axis (orthogonal to {@link Camera#up} and "look").

     * @param {Number} angleInc Angle of rotation in degrees
     */
    pitch(angleInc) {
        if (this._constrainPitch) {
            angleInc = math.dotVec3(this._up, this._worldUp) / math.DEGTORAD;
            if (angleInc < 1) {
                return;
            }
        }
        let look2 = math.subVec3(this._look, this._eye, tempVec3$4$1);
        const left = math.cross3Vec3(math.normalizeVec3(look2, tempVec3b$8), math.normalizeVec3(this._up, tempVec3c$7));
        math.rotationMat4v(angleInc * 0.0174532925, left, tempMat);
        this.up = math.transformPoint3(tempMat, this._up, tempVec3f);
        look2 = math.transformPoint3(tempMat, look2, tempVec3d$2);
        this.look = math.addVec3(look2, this._eye, tempVec3e);
    }

    /**
     * Pans the Camera along its local X, Y and Z axis.
     *
     * @param pan The pan vector
     */
    pan(pan) {
        const eye2 = math.subVec3(this._eye, this._look, tempVec3$4$1);
        const vec = [0, 0, 0];
        let v;
        if (pan[0] !== 0) {
            const left = math.cross3Vec3(math.normalizeVec3(eye2, []), math.normalizeVec3(this._up, tempVec3b$8));
            v = math.mulVec3Scalar(left, pan[0]);
            vec[0] += v[0];
            vec[1] += v[1];
            vec[2] += v[2];
        }
        if (pan[1] !== 0) {
            v = math.mulVec3Scalar(math.normalizeVec3(this._up, tempVec3c$7), pan[1]);
            vec[0] += v[0];
            vec[1] += v[1];
            vec[2] += v[2];
        }
        if (pan[2] !== 0) {
            v = math.mulVec3Scalar(math.normalizeVec3(eye2, tempVec3d$2), pan[2]);
            vec[0] += v[0];
            vec[1] += v[1];
            vec[2] += v[2];
        }
        this.eye = math.addVec3(this._eye, vec, tempVec3e);
        this.look = math.addVec3(this._look, vec, tempVec3f);
    }

    /**
     * Increments/decrements the Camera's zoom factor, which is the distance between {@link Camera#eye} and {@link Camera#look}.
     *
     * @param {Number} delta Zoom factor increment.
     */
    zoom(delta) {
        const vec = math.subVec3(this._eye, this._look, tempVec3$4$1);
        const lenLook = Math.abs(math.lenVec3(vec, tempVec3b$8));
        const newLenLook = Math.abs(lenLook + delta);
        if (newLenLook < 0.5) {
            return;
        }
        const dir = math.normalizeVec3(vec, tempVec3c$7);
        this.eye = math.addVec3(this._look, math.mulVec3Scalar(dir, newLenLook), tempVec3d$2);
    }

    /**
     * Sets the position of the Camera's eye.
     *
     * Default value is ````[0,0,10]````.
     *
     * @emits "eye" event on change, with the value of this property.
     * @type {Number[]} New eye position.
     */
    set eye(eye) {
        this._eye.set(eye || [0, 0, 10]);
        this._needUpdate(0); // Ensure matrix built on next "tick"
        this.fire("eye", this._eye);
    }

    /**
     * Gets the position of the Camera's eye.
     *
     * Default vale is ````[0,0,10]````.
     *
     * @type {Number[]} New eye position.
     */
    get eye() {
        return this._eye;
    }

    /**
     * Sets the position of this Camera's point-of-interest.
     *
     * Default value is ````[0,0,0]````.
     *
     * @emits "look" event on change, with the value of this property.
     *
     * @param {Number[]} look Camera look position.
     */
    set look(look) {
        this._look.set(look || [0, 0, 0]);
        this._needUpdate(0); // Ensure matrix built on next "tick"
        this.fire("look", this._look);
    }

    /**
     * Gets the position of this Camera's point-of-interest.
     *
     * Default value is ````[0,0,0]````.
     *
     * @returns {Number[]} Camera look position.
     */
    get look() {
        return this._look;
    }

    /**
     * Sets the direction of this Camera's {@link Camera#up} vector.
     *
     * @emits "up" event on change, with the value of this property.
     *
     * @param {Number[]} up Direction of "up".
     */
    set up(up) {
        this._up.set(up || [0, 1, 0]);
        this._needUpdate(0);
        this.fire("up", this._up);
    }

    /**
     * Gets the direction of this Camera's {@link Camera#up} vector.
     *
     * @returns {Number[]} Direction of "up".
     */
    get up() {
        return this._up;
    }

    /**
     * Sets an optional matrix to premultiply into {@link Camera#matrix} matrix.
     *
     * This is intended to be used for stereo rendering with WebVR etc.
     *
     * @param {Number[]} matrix The matrix.
     */
    set deviceMatrix(matrix) {
        this._state.deviceMatrix.set(matrix || [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
        this._state.hasDeviceMatrix = !!matrix;
        this._needUpdate(0);
        this.fire("deviceMatrix", this._state.deviceMatrix);
    }

    /**
     * Gets an optional matrix to premultiply into {@link Camera#matrix} matrix.
     *
     * @returns {Number[]} The matrix.
     */
    get deviceMatrix() {
        return this._state.deviceMatrix;
    }

    /**
     * Sets the up, right and forward axis of the World coordinate system.
     *
     * Has format: ````[rightX, rightY, rightZ, upX, upY, upZ, forwardX, forwardY, forwardZ]````
     *
     * Default axis is ````[1, 0, 0, 0, 1, 0, 0, 0, 1]````
     *
     * @param {Number[]} axis The new Wworld coordinate axis.
     */
    set worldAxis(axis) {
        axis = axis || [1, 0, 0, 0, 1, 0, 0, 0, 1];
        if (!this._worldAxis) {
            this._worldAxis = math.vec3(axis);
        } else {
            this._worldAxis.set(axis);
        }
        this._worldRight[0] = this._worldAxis[0];
        this._worldRight[1] = this._worldAxis[1];
        this._worldRight[2] = this._worldAxis[2];
        this._worldUp[0] = this._worldAxis[3];
        this._worldUp[1] = this._worldAxis[4];
        this._worldUp[2] = this._worldAxis[5];
        this._worldForward[0] = this._worldAxis[6];
        this._worldForward[1] = this._worldAxis[7];
        this._worldForward[2] = this._worldAxis[8];
        this.fire("worldAxis", this._worldAxis);
    }

    /**
     * Gets the up, right and forward axis of the World coordinate system.
     *
     * Has format: ````[rightX, rightY, rightZ, upX, upY, upZ, forwardX, forwardY, forwardZ]````
     *
     * Default axis is ````[1, 0, 0, 0, 1, 0, 0, 0, 1]````
     *
     * @returns {Number[]} The current World coordinate axis.
     */
    get worldAxis() {
        return this._worldAxis;
    }

    /**
     * Gets the direction of World-space "up".
     *
     * This is set by {@link Camera#worldAxis}.
     *
     * Default value is ````[0,1,0]````.
     *
     * @returns {Number[]} The "up" vector.
     */
    get worldUp() {
        return this._worldUp;
    }

    /**
     * Gets if the World-space X-axis is "up".
     * @returns {Boolean}
     */
    get xUp() {
        return this._worldUp[0] > this._worldUp[1] && this._worldUp[0] > this._worldUp[2];
    }

    /**
     * Gets if the World-space Y-axis is "up".
     * @returns {Boolean}
     */
    get yUp() {
        return this._worldUp[1] > this._worldUp[0] && this._worldUp[1] > this._worldUp[2];
    }

    /**
     * Gets if the World-space Z-axis is "up".
     * @returns {Boolean}
     */
    get zUp() {
        return this._worldUp[2] > this._worldUp[0] && this._worldUp[2] > this._worldUp[1];
    }

    /**
     * Gets the direction of World-space "right".
     *
     * This is set by {@link Camera#worldAxis}.
     *
     * Default value is ````[1,0,0]````.
     *
     * @returns {Number[]} The "up" vector.
     */
    get worldRight() {
        return this._worldRight;
    }

    /**
     * Gets the direction of World-space "forwards".
     *
     * This is set by {@link Camera#worldAxis}.
     *
     * Default value is ````[0,0,1]````.
     *
     * @returns {Number[]} The "up" vector.
     */
    get worldForward() {
        return this._worldForward;
    }

    /**
     * Sets whether to lock yaw rotation to pivot about the World-space "up" axis.
     *
     * Fires a {@link Camera#gimbalLock:event} event on change.
     *
     * @params {Boolean} gimbalLock Set true to lock gimbal.
     */
    set gimbalLock(value) {
        this._gimbalLock = value !== false;
        this.fire("gimbalLock", this._gimbalLock);
    }

    /**
     * Gets whether to lock yaw rotation to pivot about the World-space "up" axis.
     *
     * @returns {Boolean} Returns ````true```` if gimbal is locked.
     */
    get gimbalLock() {
        return this._gimbalLock;
    }

    /**
     * Sets whether to prevent camera from being pitched upside down.
     *
     * The camera is upside down when the angle between {@link Camera#up} and {@link Camera#worldUp} is less than one degree.
     *
     * Fires a {@link Camera#constrainPitch:event} event on change.
     *
     * Default value is ````false````.
     *
     * @param {Boolean} value Set ````true```` to contrain pitch rotation.
     */
    set constrainPitch(value) {
        this._constrainPitch = !!value;
        this.fire("constrainPitch", this._constrainPitch);
    }

    /**
     * Gets whether to prevent camera from being pitched upside down.
     *
     * The camera is upside down when the angle between {@link Camera#up} and {@link Camera#worldUp} is less than one degree.
     *
     * Default value is ````false````.
     *
     * @returns {Boolean} ````true```` if pitch rotation is currently constrained.
     get constrainPitch() {
        return this._constrainPitch;
    }

     /**
     * Gets distance from {@link Camera#look} to {@link Camera#eye}.
     *
     * @returns {Number} The distance.
     */
    get eyeLookDist() {
        return math.lenVec3(math.subVec3(this._look, this._eye, tempVec3$4$1));
    }

    /**
     * Gets the Camera's viewing transformation matrix.
     *
     * Fires a {@link Camera#matrix:event} event on change.
     *
     * @returns {Number[]} The viewing transform matrix.
     */
    get matrix() {
        if (this._updateScheduled) {
            this._doUpdate();
        }
        return this._state.matrix;
    }

    /**
     * Gets the Camera's viewing transformation matrix.
     *
     * Fires a {@link Camera#matrix:event} event on change.
     *
     * @returns {Number[]} The viewing transform matrix.
     */
    get viewMatrix() {
        if (this._updateScheduled) {
            this._doUpdate();
        }
        return this._state.matrix;
    }

    /**
     * The Camera's viewing normal transformation matrix.
     *
     * Fires a {@link Camera#matrix:event} event on change.
     *
     * @returns {Number[]} The viewing normal transform matrix.
     */
    get normalMatrix() {
        if (this._updateScheduled) {
            this._doUpdate();
        }
        return this._state.normalMatrix;
    }

    /**
     * The Camera's viewing normal transformation matrix.
     *
     * Fires a {@link Camera#matrix:event} event on change.
     *
     * @returns {Number[]} The viewing normal transform matrix.
     */
    get viewNormalMatrix() {
        if (this._updateScheduled) {
            this._doUpdate();
        }
        return this._state.normalMatrix;
    }

    /**
     * Gets the inverse of the Camera's viewing transform matrix.
     *
     * This has the same value as {@link Camera#normalMatrix}.
     *
     * @returns {Number[]} The inverse viewing transform matrix.
     */
    get inverseViewMatrix() {
        if (this._updateScheduled) {
            this._doUpdate();
        }
        return this._state.inverseMatrix;
    }

    /**
     * Gets the Camera's projection transformation projMatrix.
     *
     * Fires a {@link Camera#projMatrix:event} event on change.
     *
     * @returns {Number[]} The projection matrix.
     */
    get projMatrix() {
        return this[this.projection].matrix;
    }

    /**
     * Gets the Camera's perspective projection.
     *
     * The Camera uses this while {@link Camera#projection} equals ````perspective````.
     *
     * @returns {Perspective} The Perspective component.
     */
    get perspective() {
        return this._perspective;
    }

    /**
     * Gets the Camera's orthographic projection.
     *
     * The Camera uses this while {@link Camera#projection} equals ````ortho````.
     *
     * @returns {Ortho} The Ortho component.
     */
    get ortho() {
        return this._ortho;
    }

    /**
     * Gets the Camera's frustum projection.
     *
     * The Camera uses this while {@link Camera#projection} equals ````frustum````.
     *
     * @returns {Frustum} The Ortho component.
     */
    get frustum() {
        return this._frustum;
    }

    /**
     * Gets the Camera's custom projection.
     *
     * This is used while {@link Camera#projection} equals "customProjection".
     *
     * @returns {CustomProjection} The custom projection.
     */
    get customProjection() {
        return this._customProjection;
    }

    /**
     * Sets the active projection type.
     *
     * Accepted values are ````"perspective"````, ````"ortho"````, ````"frustum"```` and ````"customProjection"````.
     *
     * Default value is ````"perspective"````.
     *
     * @param {String} value Identifies the active projection type.
     */
    set projection(value) {
        value = value || "perspective";
        if (this._projectionType === value) {
            return;
        }
        if (value === "perspective") {
            this._project = this._perspective;
        } else if (value === "ortho") {
            this._project = this._ortho;
        } else if (value === "frustum") {
            this._project = this._frustum;
        } else if (value === "customProjection") {
            this._project = this._customProjection;
        } else {
            this.error("Unsupported value for 'projection': " + value + " defaulting to 'perspective'");
            this._project = this._perspective;
            value = "perspective";
        }
        this._project._update();
        this._projectionType = value;
        this.glRedraw();
        this._update(); // Need to rebuild lookat matrix with full eye, look & up
        this.fire("dirty");
        this.fire("projection", this._projectionType);
        this.fire("projMatrix", this._project.matrix);
    }

    /**
     * Gets the active projection type.
     *
     * Possible values are ````"perspective"````, ````"ortho"````, ````"frustum"```` and ````"customProjection"````.
     *
     * Default value is ````"perspective"````.
     *
     * @returns {String} Identifies the active projection type.
     */
    get projection() {
        return this._projectionType;
    }

    /**
     * Gets the currently active projection for this Camera.
     *
     * The currently active project is selected with {@link Camera#projection}.
     *
     * @returns {Perspective|Ortho|Frustum|CustomProjection} The currently active projection is active.
     */
    get project() {
        return this._project;
    }

    /**
     * Gets whether to offset the ortho camera's effective eye from its look
     *
     * Default value for legacy reasons is ````true````.
     */
    get offsetEyeFromLook() {
        return this._state.offsetEyeFromLook;
    }

    /**
     * Offsets the ortho camera's effective eye from its look
     *
     * Default value for legacy reasons is ````true````.
     */
    set offsetEyeFromLook(v) {
        this._state.offsetEyeFromLook = v;
        this._needUpdate(0); // Ensure matrix built on next "tick"
    }

    /**
     * Get the 2D canvas position of a 3D world position.
     *
     * @param {[number, number, number]} worldPos
     * @returns {[number, number]} the canvas position
     */
    projectWorldPos(worldPos) {
        const _worldPos = tempVec4a$7;
        const viewPos = tempVec4b$7;
        const screenPos = tempVec4c$2;
        _worldPos[0] = worldPos[0];
        _worldPos[1] = worldPos[1];
        _worldPos[2] = worldPos[2];
        _worldPos[3] = 1;
        math.mulMat4v4(this.viewMatrix, _worldPos, viewPos);
        math.mulMat4v4(this.projMatrix, viewPos, screenPos);
        math.mulVec3Scalar(screenPos, 1.0 / screenPos[3]);
        screenPos[3] = 1.0;
        screenPos[1] *= -1;
        const canvas = this.scene.canvas.canvas;
        const halfCanvasWidth = canvas.offsetWidth / 2.0;
        const halfCanvasHeight = canvas.offsetHeight / 2.0;
        const canvasPos = [
            screenPos[0] * halfCanvasWidth + halfCanvasWidth,
            screenPos[1] * halfCanvasHeight + halfCanvasHeight,
        ];
        return canvasPos;
    }

    /**
     * Destroys this Camera.
     */
    destroy() {
        super.destroy();
        this._state.destroy();
    }
}

//----------------------------------------------------------------------------------------------------------------------

const unitsInfo = {
    meters: {
        abbrev: "m"
    },
    metres: {
        abbrev: "m"
    },
    centimeters: {
        abbrev: "cm"
    },
    centimetres: {
        abbrev: "cm"
    },
    millimeters: {
        abbrev: "mm"
    },
    millimetres: {
        abbrev: "mm"
    },
    yards: {
        abbrev: "yd"
    },
    feet: {
        abbrev: "ft"
    },
    inches: {
        abbrev: "in"
    }
};

/**
 * @desc Configures its {@link Scene}'s measurement unit and mapping between the Real-space and World-space 3D Cartesian coordinate systems.
 *
 *
 * ## Overview
 *
 * * Located at {@link Scene#metrics}.
 * * {@link Metrics#units} configures the Real-space unit type, which is ````"meters"```` by default.
 * * {@link Metrics#scale} configures the number of Real-space units represented by each unit within the World-space 3D coordinate system. This is ````1.0```` by default.
 * * {@link Metrics#origin} configures the 3D Real-space origin, in current Real-space units, at which this {@link Scene}'s World-space coordinate origin sits, This is ````[0,0,0]```` by default.
 *
 * ## Usage
 *
 * Let's load a model using an {@link XKTLoaderPlugin}, then configure the Real-space unit type and the coordinate
 * mapping between the Real-space and World-space 3D coordinate systems.
 *
 * ````JavaScript
 * import {Viewer, XKTLoaderPlugin} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas"
 * });
 *
 * viewer.scene.camera.eye = [-2.37, 18.97, -26.12];
 * viewer.scene.camera.look = [10.97, 5.82, -11.22];
 * viewer.scene.camera.up = [0.36, 0.83, 0.40];
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const model = xktLoader.load({
 *     src: "./models/xkt/duplex/duplex.xkt"
 * });
 *
 * const metrics = viewer.scene.metrics;
 *
 * metrics.units = "meters";
 * metrics.scale = 10.0;
 * metrics.origin = [100.0, 0.0, 200.0];
 * ````
 */
class Metrics extends Component {

    /**
     * @constructor
     * @private
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._units = "meters";
        this._scale = 1.0;
        this._origin = math.vec3([0, 0, 0]);

        this.units = cfg.units;
        this.scale = cfg.scale;
        this.origin = cfg.origin;
    }

    /**
     * Gets info about the supported Real-space unit types.
     *
     * This will be:
     *
     * ````javascript
     * {
     *      {
     *          meters: {
     *              abbrev: "m"
     *          },
     *          metres: {
     *              abbrev: "m"
     *          },
     *          centimeters: {
     *              abbrev: "cm"
     *          },
     *          centimetres: {
     *              abbrev: "cm"
     *          },
     *          millimeters: {
     *              abbrev: "mm"
     *          },
     *          millimetres: {
     *              abbrev: "mm"
     *          },
     *          yards: {
     *              abbrev: "yd"
     *          },
     *          feet: {
     *              abbrev: "ft"
     *          },
     *          inches: {
     *              abbrev: "in"
     *          }
     *      }
     * }
     * ````
     *
     * @type {*}
     */
    get unitsInfo() {
        return unitsInfo;
    }

    /**
     * Sets the {@link Scene}'s Real-space unit type.
     *
     * Accepted values are ````"meters"````, ````"centimeters"````, ````"millimeters"````, ````"metres"````, ````"centimetres"````, ````"millimetres"````, ````"yards"````, ````"feet"```` and ````"inches"````.
     *
     * @emits ````"units"```` event on change, with the value of this property.
     * @type {String}
     */
    set units(value) {
        if (!value) {
            value = "meters";
        }
        const info = unitsInfo[value];
        if (!info) {
            this.error("Unsupported value for 'units': " + value + " defaulting to 'meters'");
            value = "meters";
        }
        this._units = value;
        this.fire("units", this._units);
    }

    /**
     * Gets the {@link Scene}'s Real-space unit type.
     *
     * @type {String}
     */
    get units() {
        return this._units;
    }

    /**
     * Sets the number of Real-space units represented by each unit of the {@link Scene}'s World-space coordinate system.
     *
     * For example, if {@link Metrics#units} is ````"meters"````, and there are ten meters per World-space coordinate system unit, then ````scale```` would have a value of ````10.0````.
     *
     * @emits ````"scale"```` event on change, with the value of this property.
     * @type {Number}
     */
    set scale(value) {
        value = value || 1;
        if (value <= 0) {
            this.error("scale value should be larger than zero");
            return;
        }
        this._scale = value;
        this.fire("scale", this._scale);
    }

    /**
     * Gets the number of Real-space units represented by each unit of the {@link Scene}'s World-space coordinate system.
     *
     * @type {Number}
     */
    get scale() {
        return this._scale;
    }

    /**
     * Sets the Real-space 3D origin, in Real-space units, at which this {@link Scene}'s World-space coordinate origin ````[0,0,0]```` sits.
     *
     * @emits "origin" event on change, with the value of this property.
     * @type {Number[]}
     */
    set origin(value) {
        if (!value) {
            this._origin[0] = 0;
            this._origin[1] = 0;
            this._origin[2] = 0;
            return;
        }
        this._origin[0] = value[0];
        this._origin[1] = value[1];
        this._origin[2] = value[2];
        this.fire("origin", this._origin);
    }

    /**
     * Gets the 3D Real-space origin, in Real-space units, at which this {@link Scene}'s World-space coordinate origin ````[0,0,0]```` sits.
     *
     * @type {Number[]}
     */
    get origin() {
        return this._origin;
    }

    /**
     * Converts a 3D position from World-space to Real-space.
     *
     * This is equivalent to ````realPos = #origin + (worldPos * #scale)````.
     *
     * @param {Number[]} worldPos World-space 3D position, in World coordinate system units.
     * @param {Number[]} [realPos] Destination for Real-space 3D position.
     * @returns {Number[]} Real-space 3D position, in units indicated by {@link Metrics#units}.
     */
    worldToRealPos(worldPos, realPos = math.vec3(3)) {
        realPos[0] = this._origin[0] + (this._scale * worldPos[0]);
        realPos[1] = this._origin[1] + (this._scale * worldPos[1]);
        realPos[2] = this._origin[2] + (this._scale * worldPos[2]);
    }

    /**
     * Converts a 3D position from Real-space to World-space.
     *
     * This is equivalent to ````worldPos = (worldPos - #origin) / #scale````.
     *
     * @param {Number[]} realPos Real-space 3D position.
     * @param {Number[]} [worldPos] Destination for World-space 3D position.
     * @returns {Number[]} World-space 3D position.
     */
    realToWorldPos(realPos, worldPos = math.vec3(3)) {
        worldPos[0] = (realPos[0] - this._origin[0]) / this._scale;
        worldPos[1] = (realPos[1] - this._origin[1]) / this._scale;
        worldPos[2] = (realPos[2] - this._origin[2]) / this._scale;
        return worldPos;
    }
}

/**
 * @desc Configures Scalable Ambient Obscurance (SAO) for a {@link Scene}.
 *
 *  <a href="https://xeokit.github.io/xeokit-sdk/examples/index.html#postEffects_SAO_OTCConferenceCenter"><img src="http://xeokit.io/img/docs/SAO/saoEnabledDisabled.gif"></a>
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/viewer/#sao_ConferenceCenter)]
 *
 * ## Overview
 *
 * SAO approximates [Ambient Occlusion](https://en.wikipedia.org/wiki/Ambient_occlusion) in realtime. It darkens creases, cavities and surfaces
 * that are close to each other, which tend to be occluded from ambient light and appear darker.
 *
 * The animated GIF above shows the effect as we repeatedly enable and disable SAO. When SAO is enabled, we can see darkening
 * in regions such as the corners, and the crevices between stairs. This increases the amount of detail we can see when ambient
 * light is high, or when objects have uniform colors across their surfaces. Run the example to experiment with the various
 * SAO configurations.
 *
 * xeokit's implementation of SAO is based on the paper [Scalable Ambient Obscurance](https://research.nvidia.com/sites/default/files/pubs/2012-06_Scalable-Ambient-Obscurance/McGuire12SAO.pdf).
 *
 * ## Caveats
 *
 * Currently, SAO only works with perspective and orthographic projections. Therefore, to use SAO, make sure {@link Camera#projection} is
 * either "perspective" or "ortho".
 *
 * {@link SAO#scale} and {@link SAO#intensity} must be tuned to the distance
 * between {@link Perspective#near} and {@link Perspective#far}, or the distance
 * between {@link Ortho#near} and {@link Ortho#far}, depending on which of those two projections the {@link Camera} is currently
 * using. Use the [live example](https://xeokit.github.io/xeokit-sdk/examples/viewer/#sao_ConferenceCenter) to get a
 * feel for that.
 *
 * ## Usage
 *
 * In the example below, we'll start by logging a warning message to the console if SAO is not supported by the
 * system.
 *
 *Then we'll enable and configure SAO, position the camera, and configure the near and far perspective and orthographic
 * clipping planes. Finally, we'll use {@link XKTLoaderPlugin} to load the OTC Conference Center model.
 *
 * ````javascript
 * import {Viewer, XKTLoaderPlugin} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * const sao = viewer.scene.sao;
 *
 * if (!sao.supported) {
 *     sao.warn("SAO is not supported on this system - ignoring SAO configs")
 * }
 *
 * sao.enabled = true; // Enable SAO - only works if supported (see above)
 * sao.intensity = 0.15;
 * sao.bias = 0.5;
 * sao.scale = 1.0;
 * sao.minResolution = 0.0;
 * sao.numSamples = 10;
 * sao.kernelRadius = 100;
 * sao.blendCutoff = 0.1;
 *
 * const camera = viewer.scene.camera;
 *
 * camera.eye = [3.69, 5.83, -23.98];
 * camera.look = [84.31, -29.88, -116.21];
 * camera.up = [0.18, 0.96, -0.21];
 *
 * camera.perspective.near = 0.1;
 * camera.perspective.far = 2000.0;
 *
 * camera.ortho.near = 0.1;
 * camera.ortho.far = 2000.0;
 * camera.projection = "perspective";
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const model = xktLoader.load({
 *     id: "myModel",
 *     src: "./models/xkt/OTCConferenceCenter.xkt"
 *     edges: true
 * });
 * ````
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/viewer/#sao_ConferenceCenter)]
 *
 * ## Efficiency
 *
 * SAO can incur some rendering overhead, especially on objects that are viewed close to the camera. For this reason,
 * it's recommended to use a low value for {@link SAO#kernelRadius}.  A low radius will sample pixels that are close
 * to the source pixel, which will allow the GPU to efficiently cache those pixels. When {@link Camera#projection} is "perspective",
 * objects near to the viewpoint will use larger radii than farther pixels. Therefore, computing  SAO for close objects
 * is more expensive than for objects far away, that occupy fewer pixels on the canvas.
 *
 * ## Selectively enabling SAO for models
 *
 * When loading multiple models into a Scene, we sometimes only want SAO on the models that are actually going to
 * show it, such as the architecture or structure, and not show SAO on models that won't show it well, such as the
 * electrical wiring, or plumbing.
 *
 * To illustrate, lets load some of the models for the West Riverside Hospital. We'll enable SAO on the structure model,
 * but disable it on the electrical and plumbing.
 *
 * This will only apply SAO to those models if {@link SAO#supported} and {@link SAO#enabled} are both true.
 *
 * Note, by the way, how we load the models in sequence. Since XKTLoaderPlugin uses scratch memory as part of its loading
 * process, this allows the plugin to reuse that same memory across multiple loads, instead of having to create multiple
 * pools of scratch memory.
 *
 * ````javascript
 * const structure = xktLoader.load({
 *      id: "structure",
 *      src: "./models/xkt/WestRiverSideHospital/structure.xkt"
 *      edges: true,
 *      saoEnabled: true
 *  });
 *
 *  structure.on("loaded", () => {
 *
 *      const electrical = xktLoader.load({
 *          id: "electrical",
 *          src: "./models/xkt/WestRiverSideHospital/electrical.xkt",
 *          edges: true
 *      });
 *
 *      electrical.on("loaded", () => {
 *
 *          const plumbing = xktLoader.load({
 *              id: "plumbing",
 *              src: "./models/xkt/WestRiverSideHospital/plumbing.xkt",
 *              edges: true
 *          });
 *      });
 * });
 * ````
 *
 * ## Disabling SAO while camera is moving
 *
 * For smoother interaction with large models on low-power hardware, we can disable SAO while the {@link Camera} is moving:
 *
 * ````javascript
 * const timeoutDuration = 150; // Milliseconds
 * var timer = timeoutDuration;
 * var saoDisabled = false;
 *
 * const onCameraMatrix = scene.camera.on("matrix", () => {
 *     timer = timeoutDuration;
 *     if (!saoDisabled) {
 *         scene.sao.enabled = false;
 *         saoDisabled = true;
 *     }
 * });
 *
 * const onSceneTick = scene.on("tick", (tickEvent) => {
 *     if (!saoDisabled) {
 *         return;
 *     }
 *     timer -= tickEvent.deltaTime; // Milliseconds
 *     if (timer <= 0) {
 *         if (saoDisabled) {
 *             scene.sao.enabled = true;
 *             saoDisabled = false;
 *         }
 *     }
 * });
 * ````
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#techniques_nonInteractiveQuality)]
 */
class SAO extends Component {

    /** @private */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._supported = WEBGL_INFO.SUPPORTED_EXTENSIONS["OES_standard_derivatives"]; // For computing normals in SAO fragment shader

        this.enabled = cfg.enabled;
        this.kernelRadius = cfg.kernelRadius;
        this.intensity = cfg.intensity;
        this.bias = cfg.bias;
        this.scale = cfg.scale;
        this.minResolution = cfg.minResolution;
        this.numSamples = cfg.numSamples;
        this.blur = cfg.blur;
        this.blendCutoff = cfg.blendCutoff;
        this.blendFactor = cfg.blendFactor;
    }

    /**
     * Gets whether or not SAO is supported by this browser and GPU.
     *
     * Even when enabled, SAO will only work if supported.
     *
     * @type {Boolean}
     */
    get supported() {
        return this._supported;
    }

    /**
     * Sets whether SAO is enabled for the {@link Scene}.
     *
     * Even when enabled, SAO will only work if supported.
     *
     * Default value is ````false````.
     *
     * @type {Boolean}
     */
    set enabled(value) {
        value = !!value;
        if (this._enabled === value) {
            return;
        }
        this._enabled = value;
        this.glRedraw();
    }

    /**
     * Gets whether SAO is enabled for the {@link Scene}.
     *
     * Even when enabled, SAO will only apply if supported.
     *
     * Default value is ````false````.
     *
     * @type {Boolean}
     */
    get enabled() {
        return this._enabled;
    }

    /**
     * Returns true if SAO is currently possible, where it is supported, enabled, and the current scene state is compatible.
     * Called internally by renderer logic.
     * @private
     * @returns {Boolean}
     */
    get possible() {
        if (!this._supported) {
            return false;
        }
        if (!this._enabled) {
            return false;
        }
        const projection = this.scene.camera.projection;
        if (projection === "customProjection") {
            return false;
        }
        if (projection === "frustum") {
            return false;
        }
        return true;
    }

    /**
     * @private
     * @returns {boolean|*}
     */
    get active() {
        return this._active;
    }

    /**
     * Sets the maximum area that SAO takes into account when checking for possible occlusion for each fragment.
     *
     * Default value is ````100.0````.
     *
     * @type {Number}
     */
    set kernelRadius(value) {
        if (value === undefined || value === null) {
            value = 100.0;
        }
        if (this._kernelRadius === value) {
            return;
        }
        this._kernelRadius = value;
        this.glRedraw();
    }

    /**
     * Gets the maximum area that SAO takes into account when checking for possible occlusion for each fragment.
     *
     * Default value is ````100.0````.
     *
     * @type {Number}
     */
    get kernelRadius() {
        return this._kernelRadius;
    }

    /**
     * Sets the degree of darkening (ambient obscurance) produced by the SAO effect.
     *
     * Default value is ````0.15````.
     *
     * @type {Number}
     */
    set intensity(value) {
        if (value === undefined || value === null) {
            value = 0.15;
        }
        if (this._intensity === value) {
            return;
        }
        this._intensity = value;
        this.glRedraw();
    }

    /**
     * Gets the degree of darkening (ambient obscurance) produced by the SAO effect.
     *
     * Default value is ````0.15````.
     *
     * @type {Number}
     */
    get intensity() {
        return this._intensity;
    }

    /**
     * Sets the SAO bias.
     *
     * Default value is ````0.5````.
     *
     * @type {Number}
     */
    set bias(value) {
        if (value === undefined || value === null) {
            value = 0.5;
        }
        if (this._bias === value) {
            return;
        }
        this._bias = value;
        this.glRedraw();
    }

    /**
     * Gets the SAO bias.
     *
     * Default value is ````0.5````.
     *
     * @type {Number}
     */
    get bias() {
        return this._bias;
    }

    /**
     * Sets the SAO occlusion scale.
     *
     * Default value is ````1.0````.
     *
     * @type {Number}
     */
    set scale(value) {
        if (value === undefined || value === null) {
            value = 1.0;
        }
        if (this._scale === value) {
            return;
        }
        this._scale = value;
        this.glRedraw();
    }

    /**
     * Gets the SAO occlusion scale.
     *
     * Default value is ````1.0````.
     *
     * @type {Number}
     */
    get scale() {
        return this._scale;
    }

    /**
     * Sets the SAO minimum resolution.
     *
     * Default value is ````0.0````.
     *
     * @type {Number}
     */
    set minResolution(value) {
        if (value === undefined || value === null) {
            value = 0.0;
        }
        if (this._minResolution === value) {
            return;
        }
        this._minResolution = value;
        this.glRedraw();
    }

    /**
     * Gets the SAO minimum resolution.
     *
     * Default value is ````0.0````.
     *
     * @type {Number}
     */
    get minResolution() {
        return this._minResolution;
    }

    /**
     * Sets the number of SAO samples.
     *
     * Default value is ````10````.
     *
     * Update this sparingly, since it causes a shader recompile.
     *
     * @type {Number}
     */
    set numSamples(value) {
        if (value === undefined || value === null) {
            value = 10;
        }
        if (this._numSamples === value) {
            return;
        }
        this._numSamples = value;
        this.glRedraw();
    }

    /**
     * Gets the number of SAO samples.
     *
     * Default value is ````10````.
     *
     * @type {Number}
     */
    get numSamples() {
        return this._numSamples;
    }

    /**
     * Sets whether Guassian blur is enabled.
     *
     * Default value is ````true````.
     *
     * @type {Boolean}
     */
    set blur(value) {
        value = (value !== false);
        if (this._blur === value) {
            return;
        }
        this._blur = value;
        this.glRedraw();
    }

    /**
     * Gets whether Guassian blur is enabled.
     *
     * Default value is ````true````.
     *
     * @type {Boolean}
     */
    get blur() {
        return this._blur;
    }

    /**
     * Sets the SAO blend cutoff.
     *
     * Default value is ````0.3````.
     *
     * Normally you don't need to alter this.
     *
     * @type {Number}
     */
    set blendCutoff(value) {
        if (value === undefined || value === null) {
            value = 0.3;
        }
        if (this._blendCutoff === value) {
            return;
        }
        this._blendCutoff = value;
        this.glRedraw();
    }

    /**
     * Gets the SAO blend cutoff.
     *
     * Default value is ````0.3````.
     *
     * Normally you don't need to alter this.
     *
     * @type {Number}
     */
    get blendCutoff() {
        return this._blendCutoff;
    }

    /**
     * Sets the SAO blend factor.
     *
     * Default value is ````1.0````.
     *
     * Normally you don't need to alter this.
     *
     * @type {Number}
     */
    set blendFactor(value) {
        if (value === undefined || value === null) {
            value = 1.0;
        }
        if (this._blendFactor === value) {
            return;
        }
        this._blendFactor = value;
        this.glRedraw();
    }

    /**
     * Gets the SAO blend scale.
     *
     * Default value is ````1.0````.
     *
     * Normally you don't need to alter this.
     *
     * @type {Number}
     */
    get blendFactor() {
        return this._blendFactor;
    }

    /**
     * Destroys this component.
     */
    destroy() {
        super.destroy();
    }
}

/**
 * @desc Configures cross-section slices for a {@link Scene}.
 *
 * ## Overview
 *
 * Cross-sections allow to create an additional colored slice for used clipping planes. It is only a visual effect
 * calculated by shaders. It makes it easier to see the intersection between the model and the clipping plane this way.
 *
 * ## Usage
 *
 * In the example below, we'll configure CrossSections to manipulate the slice representation.
 *
 * ````javascript
 * //------------------------------------------------------------------------------------------------------------------
 * // Import the modules we need for this example
 * //------------------------------------------------------------------------------------------------------------------
 *
 * import {PhongMaterial, Viewer, math, SectionPlanesPlugin, XKTLoaderPlugin, Mesh, ReadableGeometry, buildPolylineGeometryFromCurve, SplineCurve} from "../../dist/xeokit-sdk.es.js";
 *
 * //------------------------------------------------------------------------------------------------------------------
 * // Create a Viewer and arrange the camera
 * //------------------------------------------------------------------------------------------------------------------
 *
 * const viewer = new Viewer({
 *   canvasId: "myCanvas",
 *   transparent: true
 * });
 *
 * viewer.camera.eye = [-2.341298674548419, 22.43987089731119, 7.236688436028655];
 * viewer.camera.look = [4.399999999999963, 3.7240000000000606, 8.899000000000006];
 * viewer.camera.up = [0.9102954845584759, 0.34781746407929504, 0.22446635042673466];
 *
 * const cameraControl = viewer.cameraControl;
 * cameraControl.navMode = "orbit";
 * cameraControl.followPointer = true;
 *
 * //----------------------------------------------------------------------------------------------------------------------
 * // Create a xeokit loader plugin, load a model, fit to view
 * //----------------------------------------------------------------------------------------------------------------------
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * var t0 = performance.now();
 *
 * document.getElementById("time").innerHTML = "Loading model...";
 *
 * const sceneModel = xktLoader.load({
 *   id: "myModel",
 *   src: "../../assets/models/xkt/v10/glTF-Embedded/Duplex_A_20110505.glTFEmbedded.xkt",
 *   edges: true
 * });
 *
 * sceneModel.on("loaded", () => {
 *   var t1 = performance.now();
 *   document.getElementById("time").innerHTML = "Model loaded in " + Math.floor(t1 - t0) / 1000.0 + " seconds<br>Objects: " + sceneModel.numEntities;
 *
 *   let path = new SplineCurve(viewer.scene, {
 *     points: [
 *       [0, 0, -10],
 *       [0, 0, -3],
 *       [10, 0, 10],
 *       [10, 0, 30],
 *     ],
 *   });
 *
 *   new Mesh(viewer.scene, {
 *     geometry: new ReadableGeometry(viewer.scene, buildPolylineGeometryFromCurve({
 *       id: "SplineCurve",
 *       curve: path,
 *       divisions: 50,
 *     })),
 *     material: new PhongMaterial(viewer.scene, {
 *       emissive: [1, 0, 0]
 *     })
 *   });
 *
 *   //------------------------------------------------------------------------------------------------------------------
 *   // Create a moving SectionPlane, that moves through the table models
 *   //------------------------------------------------------------------------------------------------------------------
 *
 *   const sectionPlanes = new SectionPlanesPlugin(viewer, {
 *     overviewCanvasId: "mySectionPlanesOverviewCanvas",
 *     overviewVisible: true
 *   });
 *
 *   let currentPoint = path.getPoint(0);
 *   let currentDirection = path.getTangent(0);
 *
 *   const sectionPlane = sectionPlanes.createSectionPlane({
 *     id: "mySectionPlane",
 *     pos: currentPoint,
 *     dir: currentDirection
 *   });
 *
 *   sectionPlanes.showControl(sectionPlane.id);
 *
 *   //------------------------------------------------------------------------------------------------------------------
 *   // Controlling SectionPlane position and direction
 *   //------------------------------------------------------------------------------------------------------------------
 *
 *   let currentT = 0.0;
 *   document.getElementById("section_path").oninput = function() {
 *     currentT = Number(document.getElementById("section_path").value);
 *     currentPoint = path.getPoint(currentT);
 *     currentDirection = path.getTangent(currentT);
 *     sectionPlane.pos = currentPoint;
 *     sectionPlane.dir = currentDirection;
 *   };
 *
 *   window.viewer = viewer;
 *
 *   //------------------------------------------------------------------------------------------------------------------
 *   // Controlling CrossSections settings
 *   //------------------------------------------------------------------------------------------------------------------
 *
 *   viewer.scene.crossSections.sliceThickness = 0.05;
 *   viewer.scene.crossSections.sliceColor = [0.0, 0.0, 0.0, 1.0];
 * });
 * ````
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/slicing/#SectionPlanesPlugin_Duplex_SectionPath_CrossSections)]
 *
 */
class CrossSections extends Component {

    /** @private */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this.sliceColor = cfg.sliceColor;
        this.sliceThickness  = cfg.sliceThickness;
    }

    /**
     * Sets the thickness of a slice created by a section.
     *
     * Default value is ````0.0````.
     *
     * @type {Number}
     */
    set sliceThickness(value) {
        if (value === undefined || value === null) {
            value = 0.0;
        }
        if (this._sliceThickness === value) {
            return;
        }
        this._sliceThickness = value;
        this.glRedraw();
    }

    /**
     * Gets the thickness of a slice created by a section.
     *
     * Default value is ````0.0````.
     *
     * @type {Number}
     */
    get sliceThickness() {
        return this._sliceThickness;
    }

    /**
     * Sets the color of a slice created by a section.
     *
     * Default value is ````[0.0, 0.0, 0.0, 1.0]````.
     *
     * @type {Number}
     */
    set sliceColor(value) {
        if (value === undefined || value === null) {
            value = [0.0, 0.0, 0.0, 1.0];
        }
        if (this._sliceColor === value) {
            return;
        }
        this._sliceColor = value;
        this.glRedraw();
    }

    /**
     * Gets the color of a slice created by a section.
     *
     * Default value is ````[0.0, 0.0, 0.0, 1.0]````.
     *
     * @type {Number}
     */
    get sliceColor() {
        return this._sliceColor;
    }

    /**
     * Destroys this component.
     */
    destroy() {
        super.destroy();
    }
}

const PRESETS$1 = {
    "default": {
        pointSize: 4,
        roundPoints: true,
        perspectivePoints: true
    },
    "square": {
        pointSize: 4,
        roundPoints: false,
        perspectivePoints: true
    },
    "round": {
        pointSize: 4,
        roundPoints: true,
        perspectivePoints: true
    }
};

/**
 * @desc Configures the size and shape of "points" geometry primitives.
 *
 * * Located at {@link Scene#pointsMaterial}.
 * * Supports round and square points.
 * * Optional perspective point scaling.
 * * Globally configures "points" primitives for all {@link VBOSceneModel}s.
 *
 * ## Usage
 *
 * In the example below, we'll customize the {@link Scene}'s global ````PointsMaterial````, then use
 * an {@link XKTLoaderPlugin} to load a model containing a point cloud.
 *
 * [[Run this example](/examples/index.html#materials_PointsMaterial)]
 *
 * ````javascript
 * import {Viewer, XKTLoaderPlugin} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.scene.camera.eye = [0, 0, 5];
 * viewer.scene.camera.look = [0, 0, 0];
 * viewer.scene.camera.up = [0, 1, 0];
 *
 * viewer.scene.pointsMaterial.pointSize = 2;
 * viewer.scene.pointsMaterial.roundPoints = true;
 * viewer.scene.pointsMaterial.perspectivePoints = true;
 * viewer.scene.pointsMaterial.minPerspectivePointSize = 1;
 * viewer.scene.pointsMaterial.maxPerspectivePointSize = 6;
 * viewer.scene.pointsMaterial.filterIntensity = true;
 * viewer.scene.pointsMaterial.minIntensity = 0.0;
 * viewer.scene.pointsMaterial.maxIntensity = 1.0;
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const model = xktLoader.load({
 *      id: "myModel",
 *      src: "../assets/models/xkt/MAP-PointCloud.xkt"
 * });
 * ````
 */
class PointsMaterial extends Material {

    /**
     @private
     */
    get type() {
        return "PointsMaterial";
    }

    /**
     * Gets available PointsMaterial presets.
     *
     * @type {Object}
     */
    get presets() {
        return PRESETS$1;
    };

    /**
     * @constructor
     * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well.
     * @param {*} [cfg] The PointsMaterial configuration
     * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted.
     * @param {Number} [cfg.pointSize=2] Point size in pixels.
     * @param {Boolean} [cfg.roundPoints=true] Whether points are round (````true````) or square (````false````).
     * @param {Boolean} [cfg.perspectivePoints=true] Whether apparent point size reduces with distance when {@link Camera#projection} is set to "perspective".
     * @param {Number} [cfg.minPerspectivePointSize=1] When ````perspectivePoints```` is ````true````, this is the minimum rendered size of each point in pixels.
     * @param {Number} [cfg.maxPerspectivePointSize=6] When ````perspectivePoints```` is ````true````, this is the maximum rendered size of each point in pixels.
     * @param {Boolean} [cfg.filterIntensity=false] When this is true, points are only rendered when their intensity value falls within the range given in {@link }
     * @param {Number} [cfg.minIntensity=0] When ````filterIntensity```` is ````true````, points with intensity below this value will not be rendered.
     * @param {Number} [cfg.maxIntensity=1] When ````filterIntensity```` is ````true````, points with intensity above this value will not be rendered.
     * @param {String} [cfg.preset] Selects a preset PointsMaterial configuration - see {@link PointsMaterial#presets}.
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._state = new RenderState({
            type: "PointsMaterial",
            pointSize: null,
            roundPoints: null,
            perspectivePoints: null,
            minPerspectivePointSize: null,
            maxPerspectivePointSize: null,
            filterIntensity: null,
            minIntensity: null,
            maxIntensity: null
        });

        if (cfg.preset) { // Apply preset then override with configs where provided
            this.preset = cfg.preset;
            if (cfg.pointSize !== undefined) {
                this.pointSize = cfg.pointSize;
            }
            if (cfg.roundPoints !== undefined) {
                this.roundPoints = cfg.roundPoints;
            }
            if (cfg.perspectivePoints !== undefined) {
                this.perspectivePoints = cfg.perspectivePoints;
            }
            if (cfg.minPerspectivePointSize !== undefined) {
                this.minPerspectivePointSize = cfg.minPerspectivePointSize;
            }
            if (cfg.maxPerspectivePointSize !== undefined) {
                this.maxPerspectivePointSize = cfg.minPerspectivePointSize;
            }
        } else {
            this._preset = "default";
            this.pointSize = cfg.pointSize;
            this.roundPoints = cfg.roundPoints;

            this.perspectivePoints = cfg.perspectivePoints;
            this.minPerspectivePointSize = cfg.minPerspectivePointSize;
            this.maxPerspectivePointSize = cfg.maxPerspectivePointSize;
        }

        this.filterIntensity = cfg.filterIntensity;
        this.minIntensity = cfg.minIntensity;
        this.maxIntensity = cfg.maxIntensity;
    }

    /**
     * Sets point size.
     *
     * Default value is ````2.0```` pixels.
     *
     * @type {Number}
     */
    set pointSize(value) {
        this._state.pointSize = value || 2.0;
        this.glRedraw();
    }

    /**
     * Gets point size.
     *
     * Default value is ````2.0```` pixels.
     *
     * @type {Number}
     */
    get pointSize() {
        return this._state.pointSize;
    }


    /**
     * Sets if points are round or square.
     *
     * Default is ````true```` to set points round.
     *
     * @type {Boolean}
     */
    set roundPoints(value) {
        value = (value !== false);
        if (this._state.roundPoints === value) {
            return;
        }
        this._state.roundPoints = value;
        this.scene._needRecompile = true;
        this.glRedraw();
    }

    /**
     * Gets if points are round or square.
     *
     * Default is ````true```` to set points round.
     *
     * @type {Boolean}
     */
    get roundPoints() {
        return this._state.roundPoints;
    }

    /**
     * Sets if rendered point size reduces with distance when {@link Camera#projection} is set to ````"perspective"````.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    set perspectivePoints(value) {
        value = (value !== false);
        if (this._state.perspectivePoints === value) {
            return;
        }
        this._state.perspectivePoints = value;
        this.scene._needRecompile = true;
        this.glRedraw();
    }

    /**
     * Gets if rendered point size reduces with distance when {@link Camera#projection} is set to "perspective".
     *
     * Default is ````false````.
     *
     * @type {Boolean}
     */
    get perspectivePoints() {
        return this._state.perspectivePoints;
    }

    /**
     * Sets the minimum rendered size of points when {@link PointsMaterial#perspectivePoints} is ````true````.
     *
     * Default value is ````1.0```` pixels.
     *
     * @type {Number}
     */
    set minPerspectivePointSize(value) {
        this._state.minPerspectivePointSize = value || 1.0;
        this.scene._needRecompile = true;
        this.glRedraw();
    }

    /**
     * Gets the minimum rendered size of points when {@link PointsMaterial#perspectivePoints} is ````true````.
     *
     * Default value is ````1.0```` pixels.
     *
     * @type {Number}
     */
    get minPerspectivePointSize() {
        return this._state.minPerspectivePointSize;
    }

    /**
     * Sets the maximum rendered size of points when {@link PointsMaterial#perspectivePoints} is ````true````.
     *
     * Default value is ````6```` pixels.
     *
     * @type {Number}
     */
    set maxPerspectivePointSize(value) {
        this._state.maxPerspectivePointSize = value || 6;
        this.scene._needRecompile = true;
        this.glRedraw();
    }

    /**
     * Gets the maximum rendered size of points when {@link PointsMaterial#perspectivePoints} is ````true````.
     *
     * Default value is ````6```` pixels.
     *
     * @type {Number}
     */
    get maxPerspectivePointSize() {
        return this._state.maxPerspectivePointSize;
    }

    /**
     * Sets if rendered point size reduces with distance when {@link Camera#projection} is set to ````"perspective"````.
     *
     * Default is ````false````.
     *
     * @type {Boolean}
     */
    set filterIntensity(value) {
        value = (value !== false);
        if (this._state.filterIntensity === value) {
            return;
        }
        this._state.filterIntensity = value;
        this.scene._needRecompile = true;
        this.glRedraw();
    }

    /**
     * Gets if rendered point size reduces with distance when {@link Camera#projection} is set to "perspective".
     *
     * Default is ````false````.
     *
     * @type {Boolean}
     */
    get filterIntensity() {
        return this._state.filterIntensity;
    }

    /**
     * Sets the minimum rendered size of points when {@link PointsMaterial#perspectivePoints} is ````true````.
     *
     * Default value is ````0````.
     *
     * @type {Number}
     */
    set minIntensity(value) {
        this._state.minIntensity = (value !== undefined && value !== null) ? value: 0.0;
        this.glRedraw();
    }

    /**
     * Gets the minimum rendered size of points when {@link PointsMaterial#filterIntensity} is ````true````.
     *
     * Default value is ````0````.
     *
     * @type {Number}
     */
    get minIntensity() {
        return this._state.minIntensity;
    }

    /**
     * Sets the maximum rendered size of points when {@link PointsMaterial#filterIntensity} is ````true````.
     *
     * Default value is ````1````.
     *
     * @type {Number}
     */
    set maxIntensity(value) {
        this._state.maxIntensity = (value !== undefined && value !== null) ? value: 1.0;
        this.glRedraw();
    }

    /**
     * Gets the maximum rendered size of points when {@link PointsMaterial#filterIntensity} is ````true````.
     *
     * Default value is ````1````.
     *
     * @type {Number}
     */
    get maxIntensity() {
        return this._state.maxIntensity;
    }

    /**
     * Selects a preset ````PointsMaterial```` configuration.
     *
     * Default value is ````"default"````.
     *
     * @type {String}
     */
    set preset(value) {
        value = value || "default";
        if (this._preset === value) {
            return;
        }
        const preset = PRESETS$1[value];
        if (!preset) {
            this.error("unsupported preset: '" + value + "' - supported values are " + Object.keys(PRESETS$1).join(", "));
            return;
        }
        this.pointSize = preset.pointSize;
        this.roundPoints = preset.roundPoints;
        this.perspectivePoints = preset.perspectivePoints;
        this.minPerspectivePointSize = preset.minPerspectivePointSize;
        this.maxPerspectivePointSize = preset.maxPerspectivePointSize;
        this._preset = value;
    }

    /**
     * The current preset ````PointsMaterial```` configuration.
     *
     * Default value is ````"default"````.
     *
     * @type {String}
     */
    get preset() {
        return this._preset;
    }

    /**
     * @private
     * @return {string}
     */
    get hash() {
        return [
            this.pointSize,
            this.roundPoints,
            this.perspectivePoints,
            this.minPerspectivePointSize,
            this.maxPerspectivePointSize,
            this.filterIntensity
        ].join((";"));
    }

    /**
     * Destroys this ````PointsMaterial````.
     */
    destroy() {
        super.destroy();
        this._state.destroy();
    }
}

const PRESETS = {
    "default": {
        lineWidth: 1
    },
    "thick": {
        lineWidth: 2
    },
    "thicker": {
        lineWidth: 4
    }
};

/**
 * @desc Configures the shape of "lines" geometry primitives.
 *
 * * Located at {@link Scene#linesMaterial}.
 * * Globally configures "lines" primitives for all {@link VBOSceneModel}s.
 *
 * ## Usage
 *
 * In the example below, we'll customize the {@link Scene}'s global ````LinesMaterial````, then use
 * an {@link XKTLoaderPlugin} to load a model containing line segments.
 *
 * [[Run this example](/examples/index.html#materials_LinesMaterial)]
 *
 * ````javascript
 * import {Viewer, XKTLoaderPlugin} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.scene.camera.eye = [0, 0, 5];
 * viewer.scene.camera.look = [0, 0, 0];
 * viewer.scene.camera.up = [0, 1, 0];
 *
 * viewer.scene.linesMaterial.lineWidth = 3;
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const model = xktLoader.load({
 *      id: "myModel",
 *      src: "./models/xkt/Duplex.ifc.xkt"
 * });
 * ````
 */
class LinesMaterial extends Material {

    /**
     @private
     */
    get type() {
        return "LinesMaterial";
    }

    /**
     * Gets available LinesMaterial presets.
     *
     * @type {Object}
     */
    get presets() {
        return PRESETS;
    };

    /**
     * @constructor
     * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well.
     * @param {*} [cfg] The LinesMaterial configuration
     * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted.
     * @param {Number} [cfg.lineWidth=1] Line width in pixels.
     * @param {String} [cfg.preset] Selects a preset LinesMaterial configuration - see {@link LinesMaterial#presets}.
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        this._state = new RenderState({
            type: "LinesMaterial",
            lineWidth: null
        });

        if (cfg.preset) { // Apply preset then override with configs where provided
            this.preset = cfg.preset;
            if (cfg.lineWidth !== undefined) {
                this.lineWidth = cfg.lineWidth;
            }
        } else {
            this._preset = "default";
            this.lineWidth = cfg.lineWidth;
        }
    }

    /**
     * Sets line width.
     *
     * Default value is ````1```` pixels.
     *
     * @type {Number}
     */
    set lineWidth(value) {
        this._state.lineWidth = value || 1;
        this.glRedraw();
    }

    /**
     * Gets the line width.
     *
     * Default value is ````1```` pixels.
     *
     * @type {Number}
     */
    get lineWidth() {
        return this._state.lineWidth;
    }

    /**
     * Selects a preset LinesMaterial configuration.
     *
     * Default value is ````"default"````.
     *
     * @type {String}
     */
    set preset(value) {
        value = value || "default";
        if (this._preset === value) {
            return;
        }
        const preset = PRESETS[value];
        if (!preset) {
            this.error("unsupported preset: '" + value + "' - supported values are " + Object.keys(PRESETS).join(", "));
            return;
        }
        this.lineWidth = preset.lineWidth;
        this._preset = value;
    }

    /**
     * The current preset LinesMaterial configuration.
     *
     * Default value is ````"default"````.
     *
     * @type {String}
     */
    get preset() {
        return this._preset;
    }

    /**
     * @private
     * @return {string}
     */
    get hash() {
        return ["" + this.lineWidth].join((";"));
    }

    /**
     * Destroys this LinesMaterial.
     */
    destroy() {
        super.destroy();
        this._state.destroy();
    }
}

function earcut(data, holeIndices, dim = 2) {

    const hasHoles = holeIndices && holeIndices.length;
    const outerLen = hasHoles ? holeIndices[0] * dim : data.length;
    let outerNode = linkedList(data, 0, outerLen, dim, true);
    const triangles = [];

    if (!outerNode || outerNode.next === outerNode.prev) return triangles;

    let minX, minY, invSize;

    if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim);

    // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
    if (data.length > 80 * dim) {
        minX = Infinity;
        minY = Infinity;
        let maxX = -Infinity;
        let maxY = -Infinity;

        for (let i = dim; i < outerLen; i += dim) {
            const x = data[i];
            const y = data[i + 1];
            if (x < minX) minX = x;
            if (y < minY) minY = y;
            if (x > maxX) maxX = x;
            if (y > maxY) maxY = y;
        }

        // minX, minY and invSize are later used to transform coords into integers for z-order calculation
        invSize = Math.max(maxX - minX, maxY - minY);
        invSize = invSize !== 0 ? 32767 / invSize : 0;
    }

    earcutLinked(outerNode, triangles, dim, minX, minY, invSize, 0);

    return triangles;
}

// create a circular doubly linked list from polygon points in the specified winding order
function linkedList(data, start, end, dim, clockwise) {
    let last;

    if (clockwise === (signedArea(data, start, end, dim) > 0)) {
        for (let i = start; i < end; i += dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last);
    } else {
        for (let i = end - dim; i >= start; i -= dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last);
    }

    if (last && equals$1(last, last.next)) {
        removeNode(last);
        last = last.next;
    }

    return last;
}

// eliminate colinear or duplicate points
function filterPoints(start, end) {
    if (!start) return start;
    if (!end) end = start;

    let p = start,
        again;
    do {
        again = false;

        if (!p.steiner && (equals$1(p, p.next) || area(p.prev, p, p.next) === 0)) {
            removeNode(p);
            p = end = p.prev;
            if (p === p.next) break;
            again = true;

        } else {
            p = p.next;
        }
    } while (again || p !== end);

    return end;
}

// main ear slicing loop which triangulates a polygon (given as a linked list)
function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {
    if (!ear) return;

    // interlink polygon nodes in z-order
    if (!pass && invSize) indexCurve(ear, minX, minY, invSize);

    let stop = ear;

    // iterate through ears, slicing them one by one
    while (ear.prev !== ear.next) {
        const prev = ear.prev;
        const next = ear.next;

        if (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear)) {
            triangles.push(prev.i, ear.i, next.i); // cut off the triangle

            removeNode(ear);

            // skipping the next vertex leads to less sliver triangles
            ear = next.next;
            stop = next.next;

            continue;
        }

        ear = next;

        // if we looped through the whole remaining polygon and can't find any more ears
        if (ear === stop) {
            // try filtering points and slicing again
            if (!pass) {
                earcutLinked(filterPoints(ear), triangles, dim, minX, minY, invSize, 1);

            // if this didn't work, try curing all small self-intersections locally
            } else if (pass === 1) {
                ear = cureLocalIntersections(filterPoints(ear), triangles);
                earcutLinked(ear, triangles, dim, minX, minY, invSize, 2);

            // as a last resort, try splitting the remaining polygon into two
            } else if (pass === 2) {
                splitEarcut(ear, triangles, dim, minX, minY, invSize);
            }

            break;
        }
    }
}

// check whether a polygon node forms a valid ear with adjacent nodes
function isEar(ear) {
    const a = ear.prev,
        b = ear,
        c = ear.next;

    if (area(a, b, c) >= 0) return false; // reflex, can't be an ear

    // now make sure we don't have other points inside the potential ear
    const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;

    // triangle bbox
    const x0 = Math.min(ax, bx, cx),
        y0 = Math.min(ay, by, cy),
        x1 = Math.max(ax, bx, cx),
        y1 = Math.max(ay, by, cy);

    let p = c.next;
    while (p !== a) {
        if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 &&
            pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) &&
            area(p.prev, p, p.next) >= 0) return false;
        p = p.next;
    }

    return true;
}

function isEarHashed(ear, minX, minY, invSize) {
    const a = ear.prev,
        b = ear,
        c = ear.next;

    if (area(a, b, c) >= 0) return false; // reflex, can't be an ear

    const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;

    // triangle bbox
    const x0 = Math.min(ax, bx, cx),
        y0 = Math.min(ay, by, cy),
        x1 = Math.max(ax, bx, cx),
        y1 = Math.max(ay, by, cy);

    // z-order range for the current triangle bbox;
    const minZ = zOrder(x0, y0, minX, minY, invSize),
        maxZ = zOrder(x1, y1, minX, minY, invSize);

    let p = ear.prevZ,
        n = ear.nextZ;

    // look for points inside the triangle in both directions
    while (p && p.z >= minZ && n && n.z <= maxZ) {
        if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
            pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
        p = p.prevZ;

        if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
            pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
        n = n.nextZ;
    }

    // look for remaining points in decreasing z-order
    while (p && p.z >= minZ) {
        if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
            pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
        p = p.prevZ;
    }

    // look for remaining points in increasing z-order
    while (n && n.z <= maxZ) {
        if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
            pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
        n = n.nextZ;
    }

    return true;
}

// go through all polygon nodes and cure small local self-intersections
function cureLocalIntersections(start, triangles) {
    let p = start;
    do {
        const a = p.prev,
            b = p.next.next;

        if (!equals$1(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {

            triangles.push(a.i, p.i, b.i);

            // remove two nodes involved
            removeNode(p);
            removeNode(p.next);

            p = start = b;
        }
        p = p.next;
    } while (p !== start);

    return filterPoints(p);
}

// try splitting polygon into two and triangulate them independently
function splitEarcut(start, triangles, dim, minX, minY, invSize) {
    // look for a valid diagonal that divides the polygon into two
    let a = start;
    do {
        let b = a.next.next;
        while (b !== a.prev) {
            if (a.i !== b.i && isValidDiagonal(a, b)) {
                // split the polygon in two by the diagonal
                let c = splitPolygon(a, b);

                // filter colinear points around the cuts
                a = filterPoints(a, a.next);
                c = filterPoints(c, c.next);

                // run earcut on each half
                earcutLinked(a, triangles, dim, minX, minY, invSize, 0);
                earcutLinked(c, triangles, dim, minX, minY, invSize, 0);
                return;
            }
            b = b.next;
        }
        a = a.next;
    } while (a !== start);
}

// link every hole into the outer loop, producing a single-ring polygon without holes
function eliminateHoles(data, holeIndices, outerNode, dim) {
    const queue = [];

    for (let i = 0, len = holeIndices.length; i < len; i++) {
        const start = holeIndices[i] * dim;
        const end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
        const list = linkedList(data, start, end, dim, false);
        if (list === list.next) list.steiner = true;
        queue.push(getLeftmost(list));
    }

    queue.sort(compareXYSlope);

    // process holes from left to right
    for (let i = 0; i < queue.length; i++) {
        outerNode = eliminateHole(queue[i], outerNode);
    }

    return outerNode;
}

function compareXYSlope(a, b) {
    let result = a.x - b.x;
    // when the left-most point of 2 holes meet at a vertex, sort the holes counterclockwise so that when we find
    // the bridge to the outer shell is always the point that they meet at.
    if (result === 0) {
        result = a.y - b.y;
        if (result === 0) {
            const aSlope = (a.next.y - a.y) / (a.next.x - a.x);
            const bSlope = (b.next.y - b.y) / (b.next.x - b.x);
            result = aSlope - bSlope;
        }
    }
    return result;
}

// find a bridge between vertices that connects hole with an outer ring and and link it
function eliminateHole(hole, outerNode) {
    const bridge = findHoleBridge(hole, outerNode);
    if (!bridge) {
        return outerNode;
    }

    const bridgeReverse = splitPolygon(bridge, hole);

    // filter collinear points around the cuts
    filterPoints(bridgeReverse, bridgeReverse.next);
    return filterPoints(bridge, bridge.next);
}

// David Eberly's algorithm for finding a bridge between hole and outer polygon
function findHoleBridge(hole, outerNode) {
    let p = outerNode;
    const hx = hole.x;
    const hy = hole.y;
    let qx = -Infinity;
    let m;

    // find a segment intersected by a ray from the hole's leftmost point to the left;
    // segment's endpoint with lesser x will be potential connection point
    // unless they intersect at a vertex, then choose the vertex
    if (equals$1(hole, p)) return p;
    do {
        if (equals$1(hole, p.next)) return p.next;
        else if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
            const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
            if (x <= hx && x > qx) {
                qx = x;
                m = p.x < p.next.x ? p : p.next;
                if (x === hx) return m; // hole touches outer segment; pick leftmost endpoint
            }
        }
        p = p.next;
    } while (p !== outerNode);

    if (!m) return null;

    // look for points inside the triangle of hole point, segment intersection and endpoint;
    // if there are no points found, we have a valid connection;
    // otherwise choose the point of the minimum angle with the ray as connection point

    const stop = m;
    const mx = m.x;
    const my = m.y;
    let tanMin = Infinity;

    p = m;

    do {
        if (hx >= p.x && p.x >= mx && hx !== p.x &&
                pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {

            const tan = Math.abs(hy - p.y) / (hx - p.x); // tangential

            if (locallyInside(p, hole) &&
                (tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) {
                m = p;
                tanMin = tan;
            }
        }

        p = p.next;
    } while (p !== stop);

    return m;
}

// whether sector in vertex m contains sector in vertex p in the same coordinates
function sectorContainsSector(m, p) {
    return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0;
}

// interlink polygon nodes in z-order
function indexCurve(start, minX, minY, invSize) {
    let p = start;
    do {
        if (p.z === 0) p.z = zOrder(p.x, p.y, minX, minY, invSize);
        p.prevZ = p.prev;
        p.nextZ = p.next;
        p = p.next;
    } while (p !== start);

    p.prevZ.nextZ = null;
    p.prevZ = null;

    sortLinked(p);
}

// Simon Tatham's linked list merge sort algorithm
// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
function sortLinked(list) {
    let numMerges;
    let inSize = 1;

    do {
        let p = list;
        let e;
        list = null;
        let tail = null;
        numMerges = 0;

        while (p) {
            numMerges++;
            let q = p;
            let pSize = 0;
            for (let i = 0; i < inSize; i++) {
                pSize++;
                q = q.nextZ;
                if (!q) break;
            }
            let qSize = inSize;

            while (pSize > 0 || (qSize > 0 && q)) {

                if (pSize !== 0 && (qSize === 0 || !q || p.z <= q.z)) {
                    e = p;
                    p = p.nextZ;
                    pSize--;
                } else {
                    e = q;
                    q = q.nextZ;
                    qSize--;
                }

                if (tail) tail.nextZ = e;
                else list = e;

                e.prevZ = tail;
                tail = e;
            }

            p = q;
        }

        tail.nextZ = null;
        inSize *= 2;

    } while (numMerges > 1);

    return list;
}

// z-order of a point given coords and inverse of the longer side of data bbox
function zOrder(x, y, minX, minY, invSize) {
    // coords are transformed into non-negative 15-bit integer range
    x = (x - minX) * invSize | 0;
    y = (y - minY) * invSize | 0;

    x = (x | (x << 8)) & 0x00FF00FF;
    x = (x | (x << 4)) & 0x0F0F0F0F;
    x = (x | (x << 2)) & 0x33333333;
    x = (x | (x << 1)) & 0x55555555;

    y = (y | (y << 8)) & 0x00FF00FF;
    y = (y | (y << 4)) & 0x0F0F0F0F;
    y = (y | (y << 2)) & 0x33333333;
    y = (y | (y << 1)) & 0x55555555;

    return x | (y << 1);
}

// find the leftmost node of a polygon ring
function getLeftmost(start) {
    let p = start,
        leftmost = start;
    do {
        if (p.x < leftmost.x || (p.x === leftmost.x && p.y < leftmost.y)) leftmost = p;
        p = p.next;
    } while (p !== start);

    return leftmost;
}

// check if a point lies within a convex triangle
function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {
    return (cx - px) * (ay - py) >= (ax - px) * (cy - py) &&
           (ax - px) * (by - py) >= (bx - px) * (ay - py) &&
           (bx - px) * (cy - py) >= (cx - px) * (by - py);
}

// check if a point lies within a convex triangle but false if its equal to the first point of the triangle
function pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, px, py) {
    return !(ax === px && ay === py) && pointInTriangle(ax, ay, bx, by, cx, cy, px, py);
}

// check if a diagonal between two polygon nodes is valid (lies in polygon interior)
function isValidDiagonal(a, b) {
    return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && // dones't intersect other edges
           (locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible
            (area(a.prev, a, b.prev) || area(a, b.prev, b)) || // does not create opposite-facing sectors
            equals$1(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0); // special zero-length case
}

// signed area of a triangle
function area(p, q, r) {
    return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
}

// check if two points are equal
function equals$1(p1, p2) {
    return p1.x === p2.x && p1.y === p2.y;
}

// check if two segments intersect
function intersects(p1, q1, p2, q2) {
    const o1 = sign(area(p1, q1, p2));
    const o2 = sign(area(p1, q1, q2));
    const o3 = sign(area(p2, q2, p1));
    const o4 = sign(area(p2, q2, q1));

    if (o1 !== o2 && o3 !== o4) return true; // general case

    if (o1 === 0 && onSegment(p1, p2, q1)) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1
    if (o2 === 0 && onSegment(p1, q2, q1)) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1
    if (o3 === 0 && onSegment(p2, p1, q2)) return true; // p2, q2 and p1 are collinear and p1 lies on p2q2
    if (o4 === 0 && onSegment(p2, q1, q2)) return true; // p2, q2 and q1 are collinear and q1 lies on p2q2

    return false;
}

// for collinear points p, q, r, check if point q lies on segment pr
function onSegment(p, q, r) {
    return q.x <= Math.max(p.x, r.x) && q.x >= Math.min(p.x, r.x) && q.y <= Math.max(p.y, r.y) && q.y >= Math.min(p.y, r.y);
}

function sign(num) {
    return num > 0 ? 1 : num < 0 ? -1 : 0;
}

// check if a polygon diagonal intersects any polygon segments
function intersectsPolygon(a, b) {
    let p = a;
    do {
        if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&
                intersects(p, p.next, a, b)) return true;
        p = p.next;
    } while (p !== a);

    return false;
}

// check if a polygon diagonal is locally inside the polygon
function locallyInside(a, b) {
    return area(a.prev, a, a.next) < 0 ?
        area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 :
        area(a, b, a.prev) < 0 || area(a, a.next, b) < 0;
}

// check if the middle point of a polygon diagonal is inside the polygon
function middleInside(a, b) {
    let p = a;
    let inside = false;
    const px = (a.x + b.x) / 2;
    const py = (a.y + b.y) / 2;
    do {
        if (((p.y > py) !== (p.next.y > py)) && p.next.y !== p.y &&
                (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x))
            inside = !inside;
        p = p.next;
    } while (p !== a);

    return inside;
}

// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;
// if one belongs to the outer ring and another to a hole, it merges it into a single ring
function splitPolygon(a, b) {
    const a2 = createNode(a.i, a.x, a.y),
        b2 = createNode(b.i, b.x, b.y),
        an = a.next,
        bp = b.prev;

    a.next = b;
    b.prev = a;

    a2.next = an;
    an.prev = a2;

    b2.next = a2;
    a2.prev = b2;

    bp.next = b2;
    b2.prev = bp;

    return b2;
}

// create a node and optionally link it with previous one (in a circular doubly linked list)
function insertNode(i, x, y, last) {
    const p = createNode(i, x, y);

    if (!last) {
        p.prev = p;
        p.next = p;

    } else {
        p.next = last.next;
        p.prev = last;
        last.next.prev = p;
        last.next = p;
    }
    return p;
}

function removeNode(p) {
    p.next.prev = p.prev;
    p.prev.next = p.next;

    if (p.prevZ) p.prevZ.nextZ = p.nextZ;
    if (p.nextZ) p.nextZ.prevZ = p.prevZ;
}

function createNode(i, x, y) {
    return {
        i, // vertex index in coordinates array
        x, y, // vertex coordinates
        prev: null, // previous and next vertex nodes in a polygon ring
        next: null,
        z: 0, // z-order curve value
        prevZ: null, // previous and next nodes in z-order
        nextZ: null,
        steiner: false // indicates whether this is a steiner point
    };
}

function signedArea(data, start, end, dim) {
    let sum = 0;
    for (let i = start, j = end - dim; i < end; i += dim) {
        sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]);
        j = i;
    }
    return sum;
}

const epsilon = 1e-6;
const worldUp    = [0, 1, 0];
const worldRight = [1, 0, 0];
const tempVec3a$a = math.vec3();
const tempVec3b$7 = math.vec3();
const tempVec3c$6 = math.vec3();
const tempVec3d$1 = math.vec3();
const planeOff  = math.vec3();

function pointsEqual(p1, p2) {
    return (
        Math.abs(p1[0] - p2[0]) < epsilon &&
        Math.abs(p1[1] - p2[1]) < epsilon &&
        Math.abs(p1[2] - p2[2]) < epsilon
    );
}

/**
 * @desc Implements hatching for Solid objects on a {@link Scene}.
 * 
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/slicing/SectionPlanesPlugin_Duplex_SectionCaps.html)]
 * 
 * ##Overview
 * 
 * The WebGL implementation for capping sliced 3D objects works by first calculating intersection segments where a cutting 
 * plane meets the object's edges. These segments form a contour that is triangulated using the Earcut algorithm, which 
 * handles any internal holes efficiently. The resulting triangulated cap is then integrated into the original mesh with 
 * appropriate normals and UVs.
 * 
 * ##Usage
 * 
 * In the example, we'll start by enabling readable geometry on the viewer.
 * 
 * Then we'll position the camera, and configure the near and far perspective and orthographic
 * clipping planes. Finally, we'll use {@link XKTLoaderPlugin} to load the Duplex model.
 * 
 * ````javascript
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true,
 *     readableGeometryEnabled: true
 * });
 * 
 * viewer.camera.eye = [-2.341298674548419, 22.43987089731119, 7.236688436028655];
 * viewer.camera.look = [4.399999999999963, 3.7240000000000606, 8.899000000000006];
 * viewer.camera.up = [0.9102954845584759, 0.34781746407929504, 0.22446635042673466];
 * 
 * const cameraControl = viewer.cameraControl;
 * cameraControl.navMode = "orbit";
 * cameraControl.followPointer = true;
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 * 
 * var t0 = performance.now();
 * 
 * document.getElementById("time").innerHTML = "Loading model...";
 * 
 * const sceneModel = xktLoader.load({
 *     id: "myModel",
 *     src: "../../assets/models/xkt/v10/glTF-Embedded/Duplex_A_20110505.glTFEmbedded.xkt",
 *     edges: true
 * });
 * 
 * sceneModel.on("loaded", () => {
 * 
 *     var t1 = performance.now();
 *     document.getElementById("time").innerHTML = "Model loaded in " + Math.floor(t1 - t0) / 1000.0 + " seconds<br>Objects: " + sceneModel.numEntities;
 * 
 *     //------------------------------------------------------------------------------------------------------------------
 *     // Add caps materials to all objects inside the loaded model that have an opacity equal to or above 0.7
 *     //------------------------------------------------------------------------------------------------------------------        
 *     const opacityThreshold = 0.7;
 *     const material = new PhongMaterial(viewer.scene,{
 *         diffuse: [1.0, 0.0, 0.0],
 *         backfaces: true
 *     });
 *     addCapsMaterialsToAllObjects(sceneModel, opacityThreshold, material);
 * 
 *     //------------------------------------------------------------------------------------------------------------------
 *     // Create a moving SectionPlane, that moves through the table models
 *     //------------------------------------------------------------------------------------------------------------------
 * 
 *     const sectionPlanes = new SectionPlanesPlugin(viewer, {
 *         overviewCanvasId: "mySectionPlanesOverviewCanvas",
 *         overviewVisible: true,
 *     });
 * 
 *     const sectionPlane = sectionPlanes.createSectionPlane({
 *         id: "mySectionPlane",
 *         pos: [0.5, 2.5, 5.0],
 *         dir: math.normalizeVec3([1.0, 0.01, 1])
 *     });
 * 
 *     sectionPlanes.showControl(sectionPlane.id);
 * 
 *     window.viewer = viewer;
 * 
 * });
 * 
 * function addCapsMaterialsToAllObjects(sceneModel, opacityThreshold, material) {
 *     const allObjects = sceneModel.objects;
 *     for(const key in allObjects){
 *         const object = allObjects[key];
 *         if(object.opacity >= opacityThreshold)
 *             object.capMaterial = material;
 *     }
 * }
 * ````
 */

class SectionCaps {
    /**
     * @constructor
     */
    constructor(scene) {
        this.scene = scene;
        this._resourcesAllocated = false;
    }

    _onCapMaterialUpdated(entityId, modelId) {
        if(!this._resourcesAllocated) {
            this._resourcesAllocated = true;
            this._sectionPlanes = [];
            this._sceneModelsData = {};
            this._dirtyMap = {};
            this._prevIntersectionModelsMap = {};
            this._sectionPlaneTimeout = null;
            this._updateTimeout = null;

            const handleSectionPlane = (sectionPlane) => {

                const onSectionPlaneUpdated = () => {
                    this._setAllDirty(true);
                    this._update();
                };
                this._sectionPlanes.push(sectionPlane);
                sectionPlane.on('pos', onSectionPlaneUpdated);
                sectionPlane.on('dir', onSectionPlaneUpdated);
                sectionPlane.on('active', onSectionPlaneUpdated);
                sectionPlane.once('destroyed', (() => {
                    const sectionPlaneId = sectionPlane.id;
                    if (sectionPlaneId) {
                        this._sectionPlanes = this._sectionPlanes.filter((sectionPlane) => sectionPlane.id !== sectionPlaneId);
                        this._update();
                    }
                }).bind(this));
            };

            for(const key in this.scene.sectionPlanes){
                handleSectionPlane(this.scene.sectionPlanes[key]);
            }

            this._onSectionPlaneCreated = this.scene.on('sectionPlaneCreated', handleSectionPlane);

            this._onTick = this.scene.on("tick", () => {
                //on ticks we only check if there is a model that we have saved vertices for,
                //but it's no more available on the scene, or if its visibility changed
                let dirty = false;
                for(const sceneModelId in this._sceneModelsData) {
                    if(!this.scene.models[sceneModelId]){
                        delete this._sceneModelsData[sceneModelId];
                        dirty = true;
                    } else if (this._sceneModelsData[sceneModelId].visible !== (!!this.scene.models[sceneModelId].visible)) {
                        this._sceneModelsData[sceneModelId].visible = !!this.scene.models[sceneModelId].visible;
                        dirty = true;
                    }
                }
                if (dirty) {
                    this._update();
                }
            });
        }

        if(!this._dirtyMap[modelId])
            this._dirtyMap[modelId] = new Map();

        this._dirtyMap[modelId].set(entityId, true);
        this._update();
    }

    _update() {
        clearTimeout(this._updateTimeout);
        this._deletePreviousModels();
        this._updateTimeout = setTimeout(() => {
            clearTimeout(this._updateTimeout);
            const sceneModels = Object.values(this.scene.models).filter(sceneModel => sceneModel.visible);
            this._addHatches(sceneModels, this._sectionPlanes.filter(sectionPlane => sectionPlane.active));
            this._setAllDirty(false);
        }, 100);
    }

    _setAllDirty(value) {
        for(const key in this._dirty) {
            this._dirtyMap[key].forEach((_, key2) => this._dirtyMap[key].set(key2, value));
        }
    }

    _addHatches(sceneModels, planes) {

        planes.forEach((plane) => {
            sceneModels.forEach((sceneModel) => {
                if(!this._doesPlaneIntersectBoundingBox(sceneModel.aabb, plane)) return;

                if(!this._dirtyMap[sceneModel.id]) return;

                //#region calculating segments in unsorted way
                //we calculate the segments by intersecting plane with each triangle
                const unsortedSegments = new Map();
                const objects = sceneModel.objects;
                // Preallocate arrays for triangle vertices to avoid repeated allocation
                const triangle = [
                    math.vec3(),
                    math.vec3(),
                    math.vec3()
                ];

                this._dirtyMap[sceneModel.id].forEach((isDirty, objectId) => {
                    if (!isDirty) {
                        return;
                    }

                    const object = objects[objectId];

                    if(!this._doesPlaneIntersectBoundingBox(object.aabb, plane)) return;

                    if(!this._sceneModelsData[sceneModel.id]) {
                        const aabb = sceneModel.aabb;
                        this._sceneModelsData[sceneModel.id] = {
                            verticesMap: new Map(),
                            indicesMap:  new Map(),
                            // modelOrigin is critical to use when handling models with large coordinates.
                            // See XCD-306 and examples/slicing/SectionCaps_at_distance.html for more details.
                            modelOrigin: math.vec3([
                                (aabb[0] + aabb[3]) / 2,
                                (aabb[1] + aabb[4]) / 2,
                                (aabb[2] + aabb[5]) / 2
                            ])
                        };
                    }

                    const sceneModelData = this._sceneModelsData[sceneModel.id];
                    const modelOrigin = sceneModelData.modelOrigin;

                    if(!sceneModelData.verticesMap.has(objectId)) {
                        const isSolid = object.meshes[0].isSolid();
                        const vertices = [ ];
                        const indices  = [ ];
                        if(isSolid && object.capMaterial) {
                            object.getEachVertex(v => vertices.push(v[0]-modelOrigin[0], v[1]-modelOrigin[1], v[2]-modelOrigin[2]));
                            object.getEachIndex(i  => indices.push(i));
                        }
                        sceneModelData.verticesMap.set(objectId, vertices);
                        sceneModelData.indicesMap.set(objectId, indices);
                    }

                    const vertices = sceneModelData.verticesMap.get(objectId);
                    const indices  = sceneModelData.indicesMap.get(objectId);
                    const planeDist = -math.dotVec3(math.subVec3(plane.pos, modelOrigin, tempVec3a$a), plane.dir);

                    const capSegments = [];
                    const vertCount = indices.length;

                    for (let i = 0; i < vertCount; i += 3) {
                        // Reuse triangle buffer instead of creating new arrays
                        for (let j = 0; j < 3; j++) {
                            const idx = indices[i + j] * 3;
                            triangle[j][0] = vertices[idx];
                            triangle[j][1] = vertices[idx + 1];
                            triangle[j][2] = vertices[idx + 2];
                        }
                        
                        // Early null check
                        if (!triangle[0][0] && !triangle[0][1] && !triangle[0][2]) continue;
                        
                        const intersections = [];
                        for (let i = 0; i < 3; i++) {
                            const p1 = triangle[i];
                            const p2 = triangle[(i + 1) % 3];

                            const d1 = planeDist + math.dotVec3(plane.dir, p1);
                            const d2 = planeDist + math.dotVec3(plane.dir, p2);

                            if (d1 * d2 > 0) continue;

                            const t = -d1 / (d2 - d1);

                            intersections.push(math.lerpVec3(t, 0, 1, p1, p2, math.vec3()));
                        }

                        if(intersections.length === 2) capSegments.push(intersections);
                    }

                    if (capSegments.length > 0) {
                        unsortedSegments.set(objectId, capSegments);
                    }
                });
                //#endregion

                //#region sorting the segments
                const orderedSegments = new Map();
                unsortedSegments.forEach((unsortedSegment, segmentedId) => {
                    orderedSegments.set(segmentedId, [
                        [
                            unsortedSegment[0] //this is also an array of two vectors
                        ]
                    ]);
                    unsortedSegment.splice(0, 1);
                    let index = 0;
                    while (unsortedSegment.length > 0) {
                        const lastPoint = orderedSegments.get(segmentedId)[index][orderedSegments.get(segmentedId)[index].length - 1][1];
                        let found = false;

                        for (let i = 0; i < unsortedSegment.length; i++) {
                            const [start, end] = unsortedSegment[i];
                            if (pointsEqual(lastPoint, start)) {
                                orderedSegments.get(segmentedId)[index].push(unsortedSegment[i]);
                                unsortedSegment.splice(i, 1);
                                found = true;
                                break;
                            } else if (pointsEqual(lastPoint, end)) {
                                orderedSegments.get(segmentedId)[index].push([end, start]);
                                unsortedSegment.splice(i, 1);
                                found = true;
                                break;
                            }
                        }

                        if (!found) {
                            if (pointsEqual(lastPoint, orderedSegments.get(segmentedId)[index][0][0])) {
                                if (unsortedSegment.length > 1) {
                                    orderedSegments.get(segmentedId).push([
                                        unsortedSegments.get(segmentedId)[0]
                                    ]);
                                    unsortedSegment.splice(0, 1);
                                    index++;
                                    continue;
                                }

                            }
                        }

                        if (!found) {
                            // console.error(`Could not find a matching segment. Loop may not be closed. Key: ${key}`);
                            break;
                        }
                    }
                });
                //#endregion
                
                //#region projecting the segments to 2D
                const projectedSegments = new Map();
                orderedSegments.forEach((orderedSegment, key) => {
                    const arr = [];
                    for (let i = 0; i < orderedSegment.length; i++) {
                        arr.push([]);
                        orderedSegment[i].forEach((segment) => {
                            arr[i].push([
                                this._projectTo2D(segment[0], plane.dir),
                                this._projectTo2D(segment[1], plane.dir)
                            ]);
                        });
                    }
                    projectedSegments.set(key, arr);
                });
                //#endregion
                
                //#region creating caps using earcut and then projecting them back to 3D
                const caps = new Map();
                let arr;
                projectedSegments.forEach((segment, segmentId) => {
                    const modelOrigin = this._sceneModelsData[sceneModel.id].modelOrigin;
                    arr = [];
                    const loops = segment;

                    // Group related loops (outer boundaries with their holes)

                    const groupedLoops = [];
                    const used = new Set();

                    for (let i = 0; i < loops.length; i++) {
                        if (used.has(i)) continue;

                        const group = [loops[i]];
                        used.add(i);

                        // Check remaining loops
                        for (let j = i + 1; j < loops.length; j++) {
                            if (used.has(j)) continue;

                            if (this._isLoopInside(loops[i], loops[j]) ||
                                this._isLoopInside(loops[j], loops[i])) {
                                group.push(loops[j]);
                                used.add(j);
                            }
                        }

                        groupedLoops.push(group);
                    }

                    // Process each group separately
                    groupedLoops.forEach(group => {
                        // Convert the segments into a flat array of vertices and find holes
                        const vertices = [];
                        const holes = [];
                        let currentIndex = 0;

                        // First, determine which loop has the largest area - this will be our outer boundary
                        const areas = group.map(loop => {
                            let area = 0;
                            for (let i = 0; i < loop.length; i++) {
                                const j = (i + 1) % loop.length;
                                area += loop[i][0][0] * loop[j][0][1];
                                area -= loop[j][0][0] * loop[i][0][1];
                            }
                            return Math.abs(area) / 2;
                        });

                        // Find index of the loop with maximum area
                        const outerLoopIndex = areas.indexOf(Math.max(...areas));

                        // Add the outer boundary first
                        group[outerLoopIndex].forEach(segment => {
                            vertices.push(segment[0][0], segment[0][1]);
                            currentIndex += 2;
                        });

                        // Then add all other loops as holes
                        for (let i = 0; i < group.length; i++) {
                            if (i !== outerLoopIndex) {
                                // Store the starting vertex index for this hole
                                holes.push(currentIndex / 2);

                                group[i].forEach(segment => {
                                    vertices.push(segment[0][0], segment[0][1]);
                                    currentIndex += 2;
                                });
                            }
                        }

                        // Triangulate using earcut
                        const triangles = earcut(vertices, holes);

                        // // Convert triangulated 2D points back to 3D
                        const cap3D = [];

                        // Process each triangle
                        for (let i = 0; i < triangles.length; i += 3) {
                            const triangle = [];

                            // Convert each vertex
                            for (let j = 0; j < 3; j++) {
                                const idx = triangles[i + j] * 2;
                                const point2D = [vertices[idx], vertices[idx + 1]];
                                const point3D = this._convertTo3D(point2D, plane, modelOrigin);
                                triangle.push(point3D);
                            }

                            cap3D.push(triangle);
                        }
                        arr.push(cap3D);
                    });
                    caps.set(segmentId, arr);
                });
                //#endregion

                //#region converting caps to geometry
                const geometryData = new Map();

                caps.forEach((cap, capId) => {
                    arr = [];
                    cap.forEach(capTriangles => {
                        // Create a vertex map to reuse vertices
                        const vertexMap = new Map();
                        const vertices = [];
                        const indices = [];
                        let currentIndex = 0;

                        capTriangles.forEach(triangle => {
                            const triangleIndices = [];

                            // Process each vertex of the triangle
                            triangle.forEach(vertex => {
                                // Create a key for the vertex to check for duplicates
                                const vertexKey = `${vertex[0].toFixed(6)},${vertex[1].toFixed(6)},${vertex[2].toFixed(6)}`;

                                if (vertexMap.has(vertexKey)) {
                                    // Reuse existing vertex
                                    triangleIndices.push(vertexMap.get(vertexKey));
                                } else {
                                    // Add new vertex
                                    vertices.push(vertex[0], vertex[1], vertex[2]);
                                    vertexMap.set(vertexKey, currentIndex);
                                    triangleIndices.push(currentIndex);
                                    currentIndex++;
                                }
                            });

                            // Add triangle indices
                            indices.push(...triangleIndices);
                        });

                        arr.push({
                            positions: vertices,
                            indices: indices
                        });
                    });

                    geometryData.set(capId, arr);
                });
                //#endregion

                //#region adding meshes to the scene
                if(!this._prevIntersectionModelsMap[sceneModel.id])
                    this._prevIntersectionModelsMap[sceneModel.id] = new Map();

                // Cache plane direction values
                math.mulVec3Scalar(plane.dir, 0.001, planeOff); // Use dedicated planeOff, as tempVec* are overwritten by _createUVs

                geometryData.forEach((geometries, objectId) => {
                    const modelOrigin = this._sceneModelsData[sceneModel.id].modelOrigin;
                    const meshArray = new Array(geometries.size); // Pre-allocate array with known size
                    let meshIndex = 0;

                    geometries.forEach((geometry, index) => {
                        const vertices = geometry.positions;
                        const indices = geometry.indices;
                        vertices.length;

                        // Build normals and UVs in parallel if possible
                        const meshNormals = math.buildNormals(vertices, indices);
                        const uvs = this._createUVs(vertices, plane, modelOrigin);

                        // Create mesh with transformed vertices
                        meshArray[meshIndex++] = new Mesh(this.scene, {
                            id: `${plane.id}-${objectId}-${index}`,
                            geometry: new ReadableGeometry(this.scene, {
                                primitive: 'triangles',
                                positions: vertices, // Only copy what we need
                                indices,
                                normals: meshNormals,
                                uv: uvs
                            }),
                            origin:   math.addVec3(modelOrigin, planeOff, tempVec3a$a),
                            position: [0, 0, 0],
                            rotation: [0, 0, 0],
                            material: sceneModel.objects[objectId].capMaterial
                        });
                    });
                    if(this._prevIntersectionModelsMap[sceneModel.id].has(objectId)) {
                        this._prevIntersectionModelsMap[sceneModel.id].get(objectId).push(...meshArray);
                    }
                    else
                        this._prevIntersectionModelsMap[sceneModel.id].set(objectId, meshArray);
                });
                //#endregion
            });
            
        });

    }

    _doesPlaneIntersectBoundingBox(bb, plane) {
        const min = [bb[0], bb[1], bb[2]];
        const max = [bb[3], bb[4], bb[5]];

        const corners = [
            [min[0], min[1], min[2]], // 000
            [max[0], min[1], min[2]], // 100
            [min[0], max[1], min[2]], // 010
            [max[0], max[1], min[2]], // 110
            [min[0], min[1], max[2]], // 001
            [max[0], min[1], max[2]], // 101
            [min[0], max[1], max[2]], // 011
            [max[0], max[1], max[2]]  // 111
        ];

        // Calculate distance from each corner to the plane
        let hasPositive = false;
        let hasNegative = false;

        for (const corner of corners) {
            const distance = plane.dist + math.dotVec3(plane.dir, corner);

            if (distance > 0) hasPositive = true;
            if (distance < 0) hasNegative = true;

            // If we found points on both sides, the plane intersects the box
            if (hasPositive && hasNegative) return true;
        }

        // If all points are on the same side, no intersection
        return false;
    }

    //not used but kept for debugging
    _buildLines(sortedSegments) {
        for (const key in sortedSegments) {
            for (let i = 0; i < sortedSegments[key].length; i++) {
                const segments = sortedSegments[key][i];
                if (segments.length <= 0) continue;
                segments.forEach((segment, index) => {
                    new Mesh(this.scene, {
                        clippable: false,
                        geometry: new ReadableGeometry(this.scene, buildLineGeometry({
                            startPoint: segment[0],
                            endPoint: segment[1],
                        })),
                        material: new PhongMaterial(this.scene, {
                            emissive: [1, 0, 0]
                        })
                    });
                });
            }

        }
    }

    _projectTo2D(point, normal) {
        let u;
        if (Math.abs(normal[0]) > Math.abs(normal[1]))
            u = [-normal[2], 0, normal[0]];
        else
            u = [0, normal[2], -normal[1]];

        u = math.normalizeVec3(u);
        const normalTemp = math.vec3(normal);
        const cross = math.cross3Vec3(normalTemp, u);
        const v = math.normalizeVec3(cross);
        const x = math.dotVec3(point, u);
        const y = math.dotVec3(point, v);

        return [x, y]

    }

    _isLoopInside(loop1, loop2) {
        // Simple point-in-polygon test using the first point of loop1
        const point = loop1[0][0];  // First point of first segment
        let inside = false;
        for (let i = 0, j = loop2.length - 1; i < loop2.length; j = i++) {
            const xi = loop2[i][0][0], yi = loop2[i][0][1];
            const xj = loop2[j][0][0], yj = loop2[j][0][1];

            const intersect = ((yi > point[1]) !== (yj > point[1]))
                && (point[0] < (xj - xi) * (point[1] - yi) / (yj - yi) + xi);

            if (intersect) inside = !inside;
        }

        return inside;
    }

    _convertTo3D(point2D, plane, origin) {
        // Reconstruct the same basis vectors used in _projectTo2D
        let u, normal = plane.dir, planePosition = plane.pos;
        if (Math.abs(normal[0]) > Math.abs(normal[1])) {
            u = [-normal[2], 0, normal[0]];
        } else {
            u = [0, normal[2], -normal[1]];
        }

        u = math.normalizeVec3(u);
        const normalTemp = math.vec3(normal);
        const cross = math.cross3Vec3(normalTemp, u);
        const v = math.normalizeVec3(cross);

        // Reconstruct 3D point using the basis vectors
        const x = point2D[0];
        const y = point2D[1];
        const result = [
            u[0] * x + v[0] * y,
            u[1] * x + v[1] * y,
            u[2] * x + v[2] * y
        ];

        // Project the point onto the cutting plane

        const t = math.dotVec3(normal, [
            planePosition[0] - result[0] - origin[0],
            planePosition[1] - result[1] - origin[1],
            planePosition[2] - result[2] - origin[2]
        ]);

        return [
            result[0] + normal[0] * t,
            result[1] + normal[1] * t,
            result[2] + normal[2] * t
        ];
    }

    _deletePreviousModels() {

        for(const sceneModelId in this._prevIntersectionModelsMap) {
            const objects = this._prevIntersectionModelsMap[sceneModelId];
            objects.forEach((value, objectId) => {
                if(this._dirtyMap[sceneModelId].get(objectId)) {
                    value.forEach((mesh) => {
                        mesh.destroy();
                    });
                    this._prevIntersectionModelsMap[sceneModelId].delete(objectId);
                }
            });
            if(this._prevIntersectionModelsMap[sceneModelId].size <= 0)
                delete this._prevIntersectionModelsMap[sceneModelId];

        }

    }

    _createUVs(vertices, plane, origin) {
        const O = plane.pos;
        const D = tempVec3a$a;
        D.set(plane.dir);
        math.normalizeVec3(D);
        const P = tempVec3b$7;

        const uvs = [ ];
        for (let i = 0; i < vertices.length; i += 3) {
            P[0] = vertices[i]     + origin[0];
            P[1] = vertices[i + 1] + origin[1];
            P[2] = vertices[i + 2] + origin[2];

            // Project P onto the plane
            const OP = math.subVec3(P, O, tempVec3c$6);
            const dist = math.dotVec3(OP, D);
            math.subVec3(P, math.mulVec3Scalar(D, dist, tempVec3c$6), P);

            const right = ((Math.abs(math.dotVec3(D, worldUp)) < 0.999)
                        ? math.cross3Vec3(D, worldUp, tempVec3c$6)
                        : worldRight);
            const v = math.cross3Vec3(D, right, tempVec3c$6);
            math.normalizeVec3(v, v);

            const OP_proj = math.subVec3(P, O, P);
            uvs.push(
                math.dotVec3(OP_proj, math.normalizeVec3(math.cross3Vec3(v, D, tempVec3d$1))),
                math.dotVec3(OP_proj, v));
        }
        return uvs;
    }

    destroy() {
        this._deletePreviousModels();
        if(this._resourcesAllocated) {
            this.scene.off(this._onModelLoaded);
            this.scene.off(this._onModelUnloaded);
            this.scene.off(this._onSectionPlaneCreated);
            this.scene.off(this._onTick);
        }
        
    }
}

// Cached vars to avoid garbage collection

function getEntityIDMap(scene, entityIds) {
    const map = {};
    let entityId;
    let entity;
    for (let i = 0, len = entityIds.length; i < len; i++) {
        entityId = entityIds[i];
        entity = scene.components[entityId];
        if (!entity) {
            scene.warn("pick(): Component not found: " + entityId);
            continue;
        }
        if (!entity.isEntity) {
            scene.warn("pick(): Component is not an Entity: " + entityId);
            continue;
        }
        map[entityId] = true;
    }
    return map;
}

/**
 * Fired whenever a debug message is logged on a component within this Scene.
 * @event log
 * @param {String} value The debug message
 */

/**
 * Fired whenever an error is logged on a component within this Scene.
 * @event error
 * @param {String} value The error message
 */

/**
 * Fired whenever a warning is logged on a component within this Scene.
 * @event warn
 * @param {String} value The warning message
 */

/**
 * @desc Contains the components that comprise a 3D scene.
 *
 * * A {@link Viewer} has a single Scene, which it provides in {@link Viewer#scene}.
 * * Plugins like {@link AxisGizmoPlugin} also have their own private Scenes.
 * * Each Scene has a corresponding {@link MetaScene}, which the Viewer provides in {@link Viewer#metaScene}.
 *
 * ## Getting a Viewer's Scene
 *
 * ````javascript
 * var scene = viewer.scene;
 * ````
 *
 * ## Creating and accessing Scene components
 *
 * As a brief introduction to creating Scene components, we'll create a {@link Mesh} that has a
 * {@link buildTorusGeometry} and a {@link PhongMaterial}:
 *
 * ````javascript
 * var teapotMesh = new Mesh(scene, {
 *     id: "myMesh",                               // <<---------- ID automatically generated if not provided
 *     geometry: new TorusGeometry(scene),
 *     material: new PhongMaterial(scene, {
 *         id: "myMaterial",
 *         diffuse: [0.2, 0.2, 1.0]
 *     })
 * });
 *
 * teapotMesh.scene.camera.eye = [45, 45, 45];
 * ````
 *
 * Find components by ID in their Scene's {@link Scene#components} map:
 *
 * ````javascript
 * var teapotMesh = scene.components["myMesh"];
 * teapotMesh.visible = false;
 *
 * var teapotMaterial = scene.components["myMaterial"];
 * teapotMaterial.diffuse = [1,0,0]; // Change to red
 * ````
 *
 * A Scene also has a map of component instances for each {@link Component} subtype:
 *
 * ````javascript
 * var meshes = scene.types["Mesh"];
 * var teapotMesh = meshes["myMesh"];
 * teapotMesh.xrayed = true;
 *
 * var phongMaterials = scene.types["PhongMaterial"];
 * var teapotMaterial = phongMaterials["myMaterial"];
 * teapotMaterial.diffuse = [0,1,0]; // Change to green
 * ````
 *
 * See {@link Node}, {@link Node} and {@link Model} for how to create and access more sophisticated content.
 *
 * ## Controlling the camera
 *
 * Use the Scene's {@link Camera} to control the current viewpoint and projection:
 *
 * ````javascript
 * var camera = myScene.camera;
 *
 * camera.eye = [-10,0,0];
 * camera.look = [-10,0,0];
 * camera.up = [0,1,0];
 *
 * camera.projection = "perspective";
 * camera.perspective.fov = 45;
 * //...
 * ````
 *
 * ## Managing the canvas
 *
 * The Scene's {@link Canvas} component provides various conveniences relevant to the WebGL canvas, such
 * as firing resize events etc:
 *
 * ````javascript
 * var canvas = scene.canvas;
 *
 * canvas.on("boundary", function(boundary) {
 *     //...
 * });
 * ````
 *
 * ## Picking
 *
 * Use {@link Scene#pick} to pick and raycast entites.
 *
 * For example, to pick a point on the surface of the closest entity at the given canvas coordinates:
 *
 * ````javascript
 * var pickResult = scene.pick({
 *      pickSurface: true,
 *      canvasPos: [23, 131]
 * });
 *
 * if (pickResult) { // Picked an entity
 *
 *     var entity = pickResult.entity;
 *
 *     var primitive = pickResult.primitive; // Type of primitive that was picked, usually "triangles"
 *     var primIndex = pickResult.primIndex; // Position of triangle's first index in the picked Mesh's Geometry's indices array
 *     var indices = pickResult.indices; // UInt32Array containing the triangle's vertex indices
 *     var localPos = pickResult.localPos; // Float64Array containing the picked Local-space position on the triangle
 *     var worldPos = pickResult.worldPos; // Float64Array containing the picked World-space position on the triangle
 *     var viewPos = pickResult.viewPos; // Float64Array containing the picked View-space position on the triangle
 *     var bary = pickResult.bary; // Float64Array containing the picked barycentric position within the triangle
 *     var normal = pickResult.normal; // Float64Array containing the interpolated normal vector at the picked position on the triangle
 *     var uv = pickResult.uv; // Float64Array containing the interpolated UV coordinates at the picked position on the triangle
 * }
 * ````
 *
 * ## Pick masking
 *
 * We can use {@link Scene#pick}'s ````includeEntities```` and ````excludeEntities````  options to mask which {@link Mesh}es we attempt to pick.
 *
 * This is useful for picking through things, to pick only the Entities of interest.
 *
 * To pick only Entities ````"gearbox#77.0"```` and ````"gearbox#79.0"````, picking through any other Entities that are
 * in the way, as if they weren't there:
 *
 * ````javascript
 * var pickResult = scene.pick({
 *      canvasPos: [23, 131],
 *      includeEntities: ["gearbox#77.0", "gearbox#79.0"]
 * });
 *
 * if (pickResult) {
 *       // Entity will always be either "gearbox#77.0" or "gearbox#79.0"
 *       var entity = pickResult.entity;
 * }
 * ````
 *
 * To pick any pickable Entity, except for ````"gearbox#77.0"```` and ````"gearbox#79.0"````, picking through those
 * Entities if they happen to be in the way:
 *
 * ````javascript
 * var pickResult = scene.pick({
 *      canvasPos: [23, 131],
 *      excludeEntities: ["gearbox#77.0", "gearbox#79.0"]
 * });
 *
 * if (pickResult) {
 *       // Entity will never be "gearbox#77.0" or "gearbox#79.0"
 *       var entity = pickResult.entity;
 * }
 * ````
 *
 * See {@link Scene#pick} for more info on picking.
 *
 * ## Querying and tracking boundaries
 *
 * Getting a Scene's World-space axis-aligned boundary (AABB):
 *
 * ````javascript
 * var aabb = scene.aabb; // [xmin, ymin, zmin, xmax, ymax, zmax]
 * ````
 *
 * Subscribing to updates to the AABB, which occur whenever {@link Entity}s are transformed, their
 * {@link ReadableGeometry}s have been updated, or the {@link Camera} has moved:
 *
 * ````javascript
 * scene.on("boundary", function() {
 *      var aabb = scene.aabb;
 * });
 * ````
 *
 * Getting the AABB of the {@link Entity}s with the given IDs:
 *
 * ````JavaScript
 * scene.getAABB(); // Gets collective boundary of all Entities in the scene
 * scene.getAABB("saw"); // Gets boundary of an Object
 * scene.getAABB(["saw", "gearbox"]); // Gets collective boundary of two Objects
 * ````
 *
 * See {@link Scene#getAABB} and {@link Entity} for more info on querying and tracking boundaries.
 *
 * ## Managing the viewport
 *
 * The Scene's {@link Viewport} component manages the WebGL viewport:
 *
 * ````javascript
 * var viewport = scene.viewport
 * viewport.boundary = [0, 0, 500, 400];;
 * ````
 *
 * ## Controlling rendering
 *
 * You can configure a Scene to perform multiple "passes" (renders) per frame. This is useful when we want to render the
 * scene to multiple viewports, such as for stereo effects.
 *
 * In the example, below, we'll configure the Scene to render twice on each frame, each time to different viewport. We'll do this
 * with a callback that intercepts the Scene before each render and sets its {@link Viewport} to a
 * different portion of the canvas. By default, the Scene will clear the canvas only before the first render, allowing the
 * two views to be shown on the canvas at the same time.
 *
 * ````Javascript
 * var viewport = scene.viewport;
 *
 * // Configure Scene to render twice for each frame
 * scene.passes = 2; // Default is 1
 * scene.clearEachPass = false; // Default is false
 *
 * // Render to a separate viewport on each render
 *
 * var viewport = scene.viewport;
 * viewport.autoBoundary = false;
 *
 * scene.on("rendering", function (e) {
 *      switch (e.pass) {
 *          case 0:
 *              viewport.boundary = [0, 0, 200, 200]; // xmin, ymin, width, height
 *              break;
 *
 *          case 1:
 *              viewport.boundary = [200, 0, 200, 200];
 *              break;
 *      }
 * });
 *
 * // We can also intercept the Scene after each render,
 * // (though we're not using this for anything here)
 * scene.on("rendered", function (e) {
 *      switch (e.pass) {
 *          case 0:
 *              break;
 *
 *          case 1:
 *              break;
 *      }
 * });
 * ````
 *
 * ## Gamma correction
 *
 * Within its shaders, xeokit performs shading calculations in linear space.
 *
 * By default, the Scene expects color textures (eg. {@link PhongMaterial#diffuseMap},
 * {@link MetallicMaterial#baseColorMap} and {@link SpecularMaterial#diffuseMap}) to
 * be in pre-multipled gamma space, so will convert those to linear space before they are used in shaders. Other textures are
 * always expected to be in linear space.
 *
 * By default, the Scene will also gamma-correct its rendered output.
 *
 * You can configure the Scene to expect all those color textures to be linear space, so that it does not gamma-correct them:
 *
 * ````javascript
 * scene.gammaInput = false;
 * ````
 *
 * You would still need to gamma-correct the output, though, if it's going straight to the canvas, so normally we would
 * leave that enabled:
 *
 * ````javascript
 * scene.gammaOutput = true;
 * ````
 *
 * See {@link Texture} for more information on texture encoding and gamma.
 *
 * @class Scene
 */
class Scene extends Component {

    /**
     @private
     */
    get type() {
        return "Scene";
    }

    /**
     * @private
     * @constructor
     * @param {Viewer} viewer The Viewer this Scene belongs to.
     * @param {Object} cfg Scene configuration.
     * @param {String} [cfg.canvasId]  ID of an existing HTML canvas for the {@link Scene#canvas} - either this or canvasElement is mandatory. When both values are given, the element reference is always preferred to the ID.
     * @param {HTMLCanvasElement} [cfg.canvasElement] Reference of an existing HTML canvas for the {@link Scene#canvas} - either this or canvasId is mandatory. When both values are given, the element reference is always preferred to the ID.
     * @param {HTMLElement} [cfg.keyboardEventsElement] Optional reference to HTML element on which key events should be handled. Defaults to the HTML Document.
     * @param {number} [cfg.numCachedSectionPlanes=0] Enhances the efficiency of SectionPlane creation by proactively allocating Viewer resources for a specified quantity
     * of SectionPlanes. Introducing this parameter streamlines the initial creation speed of SectionPlanes, particularly up to the designated quantity. This parameter internally
     * configures renderer logic for the specified number of SectionPlanes, eliminating the need for setting up logic with each SectionPlane creation and thereby enhancing
     * responsiveness. It is important to consider that each SectionPlane imposes rendering performance, so it is recommended to set this value to a quantity that aligns with
     * your expected usage.
     * @throws {String} Throws an exception when  canvasId or canvasElement are missing or they aren't pointing to a valid HTMLCanvasElement.
     */
    constructor(viewer, cfg = {}) {

        super(null, cfg);
        const canvas = cfg.canvasElement || document.querySelector(`#${cfg.canvasId}`);

        if (!(canvas instanceof HTMLCanvasElement)) {
            throw "Mandatory config expected: valid canvasId or canvasElement";
        }

        /**
         * @type {{[key: string]: {wrapperFunc: Function, tickSubId: string}}}
         */
        this._tickifiedFunctions = {};

        const transparent = (!!cfg.transparent);
        const alphaDepthMask = (!!cfg.alphaDepthMask);

        this._aabbDirty = true;

        this._readableGeometry = !!cfg.readableGeometryEnabled;

        /**
         * The {@link Viewer} this Scene belongs to.
         * @type {Viewer}
         */
        this.viewer = viewer;

        /** Decremented each frame, triggers occlusion test for occludable {@link Marker}s when zero.
         * @private
         * @type {number}
         */
        this.occlusionTestCountdown = 0;

        /**
         The number of models currently loading.

         @property loading
         @final
         @type {Number}
         */
        this.loading = 0;

        /**
         The epoch time (in milliseconds since 1970) when this Scene was instantiated.

         @property timeCreated
         @final
         @type {Number}
         */
        this.startTime = (new Date()).getTime();

        /**
         * Map of {@link Entity}s that represent models.
         *
         * Each {@link Entity} is mapped here by {@link Entity#id} when {@link Entity#isModel} is ````true````.
         *
         * @property models
         * @final
         * @type {{String:Entity}}
         */
        this.models = {};

        /**
         * Map of {@link Entity}s that represents objects.
         *
         * Each {@link Entity} is mapped here by {@link Entity#id} when {@link Entity#isObject} is ````true````.
         *
         * @property objects
         * @final
         * @type {{String:Entity}}
         */
        this.objects = {};
        this._numObjects = 0;

        /**
         * Map of currently visible {@link Entity}s that represent objects.
         *
         * An Entity represents an object if {@link Entity#isObject} is ````true````, and is visible when {@link Entity#visible} is true.
         *
         * @property visibleObjects
         * @final
         * @type {{String:Object}}
         */
        this.visibleObjects = {};
        this._numVisibleObjects = 0;

        /**
         * Map of currently xrayed {@link Entity}s that represent objects.
         *
         * An Entity represents an object if {@link Entity#isObject} is ````true````, and is xrayed when {@link Entity#xrayed} is true.
         *
         * Each {@link Entity} is mapped here by {@link Entity#id}.
         *
         * @property xrayedObjects
         * @final
         * @type {{String:Object}}
         */
        this.xrayedObjects = {};
        this._numXRayedObjects = 0;

        /**
         * Map of currently highlighted {@link Entity}s that represent objects.
         *
         * An Entity represents an object if {@link Entity#isObject} is ````true```` is true, and is highlighted when {@link Entity#highlighted} is true.
         *
         * Each {@link Entity} is mapped here by {@link Entity#id}.
         *
         * @property highlightedObjects
         * @final
         * @type {{String:Object}}
         */
        this.highlightedObjects = {};
        this._numHighlightedObjects = 0;

        /**
         * Map of currently selected {@link Entity}s that represent objects.
         *
         * An Entity represents an object if {@link Entity#isObject} is true, and is selected while {@link Entity#selected} is true.
         *
         * Each {@link Entity} is mapped here by {@link Entity#id}.
         *
         * @property selectedObjects
         * @final
         * @type {{String:Object}}
         */
        this.selectedObjects = {};
        this._numSelectedObjects = 0;

        /**
         * Map of currently colorized {@link Entity}s that represent objects.
         *
         * An Entity represents an object if {@link Entity#isObject} is ````true````.
         *
         * Each {@link Entity} is mapped here by {@link Entity#id}.
         *
         * @property colorizedObjects
         * @final
         * @type {{String:Object}}
         */
        this.colorizedObjects = {};
        this._numColorizedObjects = 0;

        /**
         * Map of {@link Entity}s that represent objects whose opacity was updated.
         *
         * An Entity represents an object if {@link Entity#isObject} is ````true````.
         *
         * Each {@link Entity} is mapped here by {@link Entity#id}.
         *
         * @property opacityObjects
         * @final
         * @type {{String:Object}}
         */
        this.opacityObjects = {};
        this._numOpacityObjects = 0;

        /**
         * Map of {@link Entity}s that represent objects whose {@link Entity#offset}s were updated.
         *
         * An Entity represents an object if {@link Entity#isObject} is ````true````.
         *
         * Each {@link Entity} is mapped here by {@link Entity#id}.
         *
         * @property offsetObjects
         * @final
         * @type {{String:Object}}
         */
        this.offsetObjects = {};
        this._numOffsetObjects = 0;

        // Cached ID arrays, lazy-rebuilt as needed when stale after map updates

        /**
         Lazy-regenerated ID lists.
         */
        this._modelIds = null;
        this._objectIds = null;
        this._visibleObjectIds = null;
        this._xrayedObjectIds = null;
        this._highlightedObjectIds = null;
        this._selectedObjectIds = null;
        this._colorizedObjectIds = null;
        this._opacityObjectIds = null;
        this._offsetObjectIds = null;

        this._collidables = {}; // Components that contribute to the Scene AABB
        this._compilables = {}; // Components that require shader compilation

        this._needRecompile = false;

        /**
         * For each {@link Component} type, a map of IDs to {@link Component} instances of that type.
         *
         * @type {{String:{String:Component}}}
         */
        this.types = {};

        /**
         * The {@link Component}s within this Scene, each mapped to its {@link Component#id}.
         *
         * *@type {{String:Component}}
         */
        this.components = {};

        /**
         * The {@link SectionPlane}s in this Scene, each mapped to its {@link SectionPlane#id}.
         *
         * @type {{String:SectionPlane}}
         */
        this.sectionPlanes = {};

        /**
         * The {@link Light}s in this Scene, each mapped to its {@link Light#id}.
         *
         * @type {{String:Light}}
         */
        this.lights = {};

        /**
         * The {@link LightMap}s in this Scene, each mapped to its {@link LightMap#id}.
         *
         * @type {{String:LightMap}}
         */
        this.lightMaps = {};

        /**
         * The {@link ReflectionMap}s in this Scene, each mapped to its {@link ReflectionMap#id}.
         *
         * @type {{String:ReflectionMap}}
         */
        this.reflectionMaps = {};

        /**
         * The {@link Bitmap}s in this Scene, each mapped to its {@link Bitmap#id}.
         *
         * @type {{String:Bitmap}}
         */
        this.bitmaps = {};

        /**
         * The {@link LineSet}s in this Scene, each mapped to its {@link LineSet#id}.
         *
         * @type {{String:LineSet}}
         */
        this.lineSets = {};

        /**
         * The real world offset for this Scene
         *
         * @type {Number[]}
         */
        this.realWorldOffset = cfg.realWorldOffset || new Float64Array([0, 0, 0]);

        /**
         * Manages the HTML5 canvas for this Scene.
         *
         * @type {Canvas}
         */
        this.canvas = new Canvas(this, {
            dontClear: true, // Never destroy this component with Scene#clear();
            canvas: canvas,
            spinnerElementId: cfg.spinnerElementId,
            transparent: transparent,
            webgl2: cfg.webgl2 !== false,
            contextAttr: cfg.contextAttr || {},
            backgroundColor: cfg.backgroundColor,
            backgroundColorFromAmbientLight: cfg.backgroundColorFromAmbientLight,
            premultipliedAlpha: cfg.premultipliedAlpha
        });

        this.canvas.on("boundary", () => {
            this.glRedraw();
        });

        this.canvas.on("webglContextFailed", () => {
            alert("xeokit failed to find WebGL!");
        });

        this._renderer = new Renderer$1(this, {
            transparent: transparent,
            alphaDepthMask: alphaDepthMask
        });

        this._sectionPlanesState = new (function () {

            this.sectionPlanes = [];

            this.clippingCaps = false;

            this._numCachedSectionPlanes = 0;

            let hash = null;

            this.getHash = function () {
                if (hash) {
                    return hash;
                }
                const numAllocatedSectionPlanes = this.getNumAllocatedSectionPlanes();
                this.sectionPlanes;
                if (numAllocatedSectionPlanes === 0) {
                    return this.hash = ";";
                }

                const hashParts = [];
                for (let i = 0, len = numAllocatedSectionPlanes; i < len; i++) {
                    hashParts.push("cp");
                }
                hashParts.push(";");
                hash = hashParts.join("");
                return hash;
            };

            this.addSectionPlane = function (sectionPlane) {
                this.sectionPlanes.push(sectionPlane);
                hash = null;
            };

            this.removeSectionPlane = function (sectionPlane) {
                for (let i = 0, len = this.sectionPlanes.length; i < len; i++) {
                    if (this.sectionPlanes[i].id === sectionPlane.id) {
                        this.sectionPlanes.splice(i, 1);
                        hash = null;
                        return;
                    }
                }
            };

            this.setNumCachedSectionPlanes = function (numCachedSectionPlanes) {
                this._numCachedSectionPlanes = numCachedSectionPlanes;
                hash = null;
            };

            this.getNumCachedSectionPlanes = function () {
                return this._numCachedSectionPlanes;
            };

            this.getNumAllocatedSectionPlanes = function () {
                const num = this.sectionPlanes.length;
                return (num > this._numCachedSectionPlanes) ? num : this._numCachedSectionPlanes;
            };
        })();

        this._sectionPlanesState.setNumCachedSectionPlanes(cfg.numCachedSectionPlanes || 0);

        this._lightsState = new (function () {

            const DEFAULT_AMBIENT = math.vec4([0, 0, 0, 0]);
            const ambientColorIntensity = math.vec4();

            this.lights = [];
            this.reflectionMaps = [];
            this.lightMaps = [];

            let hash = null;
            let ambientLight = null;

            this.getHash = function () {
                if (hash) {
                    return hash;
                }
                const hashParts = [];
                const lights = this.lights;
                let light;
                for (let i = 0, len = lights.length; i < len; i++) {
                    light = lights[i];
                    hashParts.push("/");
                    hashParts.push(light.type);
                    hashParts.push((light.space === "world") ? "w" : "v");
                    if (light.castsShadow) {
                        hashParts.push("sh");
                    }
                }
                if (this.lightMaps.length > 0) {
                    hashParts.push("/lm");
                }
                if (this.reflectionMaps.length > 0) {
                    hashParts.push("/rm");
                }
                hashParts.push(";");
                hash = hashParts.join("");
                return hash;
            };

            this.addLight = function (state) {
                this.lights.push(state);
                ambientLight = null;
                hash = null;
            };

            this.removeLight = function (state) {
                for (let i = 0, len = this.lights.length; i < len; i++) {
                    const light = this.lights[i];
                    if (light.id === state.id) {
                        this.lights.splice(i, 1);
                        if (ambientLight && ambientLight.id === state.id) {
                            ambientLight = null;
                        }
                        hash = null;
                        return;
                    }
                }
            };

            this.addReflectionMap = function (state) {
                this.reflectionMaps.push(state);
                hash = null;
            };

            this.removeReflectionMap = function (state) {
                for (let i = 0, len = this.reflectionMaps.length; i < len; i++) {
                    if (this.reflectionMaps[i].id === state.id) {
                        this.reflectionMaps.splice(i, 1);
                        hash = null;
                        return;
                    }
                }
            };

            this.addLightMap = function (state) {
                this.lightMaps.push(state);
                hash = null;
            };

            this.removeLightMap = function (state) {
                for (let i = 0, len = this.lightMaps.length; i < len; i++) {
                    if (this.lightMaps[i].id === state.id) {
                        this.lightMaps.splice(i, 1);
                        hash = null;
                        return;
                    }
                }
            };

            this.getAmbientColorAndIntensity = function () {
                if (!ambientLight) {
                    for (let i = 0, len = this.lights.length; i < len; i++) {
                        const light = this.lights[i];
                        if (light.type === "ambient") {
                            ambientLight = light;
                            break;
                        }
                    }
                }
                if (ambientLight) {
                    const color = ambientLight.color;
                    const intensity = ambientLight.intensity;
                    ambientColorIntensity[0] = color[0];
                    ambientColorIntensity[1] = color[1];
                    ambientColorIntensity[2] = color[2];
                    ambientColorIntensity[3] = intensity;
                    return ambientColorIntensity;
                } else {
                    return DEFAULT_AMBIENT;
                }
            };

        })();

        /**
         * Publishes input events that occur on this Scene's canvas.
         *
         * @property input
         * @type {Input}
         * @final
         */
        this.input = new Input(this, {
            dontClear: true, // Never destroy this component with Scene#clear();
            element: this.canvas.canvas,
            keyboardEventsElement: cfg.keyboardEventsElement
        });

        /**
         * Configures this Scene's units of measurement and coordinate mapping between Real-space and World-space 3D coordinate systems.
         *
         * @property metrics
         * @type {Metrics}
         * @final
         */
        this.metrics = new Metrics(this, {
            units: cfg.units,
            scale: cfg.scale,
            origin: cfg.origin
        });

        /** Configures Scalable Ambient Obscurance (SAO) for this Scene.
         * @type {SAO}
         * @final
         */
        this.sao = new SAO(this, {
            enabled: cfg.saoEnabled
        });

        /** Configures Cross Sections for this Scene.
         * @type {CrossSections}
         * @final
         */
        this.crossSections = new CrossSections(this, {

        });

        this.ticksPerRender = cfg.ticksPerRender;
        this.ticksPerOcclusionTest = cfg.ticksPerOcclusionTest;
        this.passes = cfg.passes;
        this.clearEachPass = cfg.clearEachPass;
        this.gammaInput = cfg.gammaInput;
        this.gammaOutput = cfg.gammaOutput;
        this.gammaFactor = cfg.gammaFactor;

        this._entityOffsetsEnabled = !!cfg.entityOffsetsEnabled;
        this._logarithmicDepthBufferEnabled = !!cfg.logarithmicDepthBufferEnabled;

        this._dtxEnabled = (cfg.dtxEnabled !== false);
        this._pbrEnabled = !!cfg.pbrEnabled;
        this._colorTextureEnabled = (cfg.colorTextureEnabled !== false);
        this._dtxEnabled = !!cfg.dtxEnabled;
        this._markerZOffset = cfg.markerZOffset;

        // Register Scene on xeokit
        // Do this BEFORE we add components below
        core._addScene(this);

        this._initDefaults();

        // Global components

        this._viewport = new Viewport(this, {
            id: "default.viewport",
            autoBoundary: true,
            dontClear: true // Never destroy this component with Scene#clear();
        });

        this._camera = new Camera(this, {
            id: "default.camera",
            dontClear: true // Never destroy this component with Scene#clear();
        });

        this._sectionCaps = new SectionCaps(this);

        // Default lights

        new AmbientLight(this, {
            color: [1.0, 1.0, 1.0],
            intensity: 0.7
        });

        new DirLight(this, {
            dir: [0.8, -0.5, -0.5],
            color: [0.67, 0.67, 1.0],
            intensity: 0.7,
            space: "world"
        });

        new DirLight(this, {
            dir: [-0.8, -1, 0.5],
            color: [1, 1, .9],
            intensity: 0.9,
            space: "world"
        });

        this._camera.on("dirty", () => {
            this._renderer.imageDirty();
        });
    }

    _initDefaults() {
    }

    _addComponent(component) {
        if (component.id) { // Manual ID
            if (this.components[component.id]) {
                this.error("Component " + utils.inQuotes(component.id) + " already exists in Scene - ignoring ID, will randomly-generate instead");
                component.id = null;
            }
        }
        if (!component.id) { // Auto ID
            if (window.nextID === undefined) {
                window.nextID = 0;
            }
            //component.id = math.createUUID();
            component.id = "__" + window.nextID++;
            while (this.components[component.id]) {
                component.id = math.createUUID();
            }
        }
        this.components[component.id] = component;

        // Register for class type
        const type = component.type;
        let types = this.types[component.type];
        if (!types) {
            types = this.types[type] = {};
        }
        types[component.id] = component;

        if (component.compile) {
            this._compilables[component.id] = component;
        }
        if (component.isDrawable) {
            this._renderer.addDrawable(component.id, component);
            this._collidables[component.id] = component;
        }
    }

    _removeComponent(component) {
        var id = component.id;
        var type = component.type;
        delete this.components[id];
        // Unregister for types
        const types = this.types[type];
        if (types) {
            delete types[id];
            if (utils.isEmptyObject(types)) {
                delete this.types[type];
            }
        }
        if (component.compile) {
            delete this._compilables[component.id];
        }
        if (component.isDrawable) {
            this._renderer.removeDrawable(component.id);
            delete this._collidables[component.id];
        }
    }

    // Methods below are called by various component types to register themselves on their
    // Scene. Violates Hollywood Principle, where we could just filter on type in _addComponent,
    // but this is faster than checking the type of each component in such a filter.

    _capMaterialUpdated(entityId, modelId) {
        this._sectionCaps._onCapMaterialUpdated(entityId, modelId);
    }

    _sectionPlaneCreated(sectionPlane) {
        this.sectionPlanes[sectionPlane.id] = sectionPlane;
        this.scene._sectionPlanesState.addSectionPlane(sectionPlane._state);
        this.scene.fire("sectionPlaneCreated", sectionPlane, true /* Don't retain event */);
        this._needRecompile = true;
    }

    _bitmapCreated(bitmap) {
        this.bitmaps[bitmap.id] = bitmap;
        this.scene.fire("bitmapCreated", bitmap, true /* Don't retain event */);
    }

    _lineSetCreated(lineSet) {
        this.lineSets[lineSet.id] = lineSet;
        this.scene.fire("lineSetCreated", lineSet, true /* Don't retain event */);
    }

    _lightCreated(light) {
        this.lights[light.id] = light;
        this.scene._lightsState.addLight(light._state);
        this._needRecompile = true;
    }

    _lightMapCreated(lightMap) {
        this.lightMaps[lightMap.id] = lightMap;
        this.scene._lightsState.addLightMap(lightMap._state);
        this._needRecompile = true;
    }

    _reflectionMapCreated(reflectionMap) {
        this.reflectionMaps[reflectionMap.id] = reflectionMap;
        this.scene._lightsState.addReflectionMap(reflectionMap._state);
        this._needRecompile = true;
    }

    _sectionPlaneDestroyed(sectionPlane) {
        delete this.sectionPlanes[sectionPlane.id];
        this.scene._sectionPlanesState.removeSectionPlane(sectionPlane._state);
        this.scene.fire("sectionPlaneDestroyed", sectionPlane, true /* Don't retain event */);
        this._needRecompile = true;
    }

    _bitmapDestroyed(bitmap) {
        delete this.bitmaps[bitmap.id];
        this.scene.fire("bitmapDestroyed", bitmap, true /* Don't retain event */);
    }

    _lineSetDestroyed(lineSet) {
        delete this.lineSets[lineSet.id];
        this.scene.fire("lineSetDestroyed", lineSet, true /* Don't retain event */);
    }

    _lightDestroyed(light) {
        delete this.lights[light.id];
        this.scene._lightsState.removeLight(light._state);
        this._needRecompile = true;
    }

    _lightMapDestroyed(lightMap) {
        delete this.lightMaps[lightMap.id];
        this.scene._lightsState.removeLightMap(lightMap._state);
        this._needRecompile = true;
    }

    _reflectionMapDestroyed(reflectionMap) {
        delete this.reflectionMaps[reflectionMap.id];
        this.scene._lightsState.removeReflectionMap(reflectionMap._state);
        this._needRecompile = true;
    }

    _registerModel(entity) {
        this.models[entity.id] = entity;
        this._modelIds = null; // Lazy regenerate
    }

    _deregisterModel(entity) {
        const modelId = entity.id;
        delete this.models[modelId];
        this._modelIds = null; // Lazy regenerate
        this.fire("modelUnloaded", modelId);
    }

    _registerObject(entity) {
        this.objects[entity.id] = entity;
        this._numObjects++;
        this._objectIds = null; // Lazy regenerate
    }

    _deregisterObject(entity) {
        delete this.objects[entity.id];
        this._numObjects--;
        this._objectIds = null; // Lazy regenerate
    }

    _objectVisibilityUpdated(entity, notify = true) {
        if (entity.visible) {
            this.visibleObjects[entity.id] = entity;
            this._numVisibleObjects++;
        } else {
            delete this.visibleObjects[entity.id];
            this._numVisibleObjects--;
        }
        this._visibleObjectIds = null; // Lazy regenerate
        if (notify) {
            this.fire("objectVisibility", entity, true);
        }
    }

    _deRegisterVisibleObject(entity) {
        delete this.visibleObjects[entity.id];
        this._numVisibleObjects--;
        this._visibleObjectIds = null; // Lazy regenerate
    }

    _objectXRayedUpdated(entity, notify = true) {
        if (entity.xrayed) {
            this.xrayedObjects[entity.id] = entity;
            this._numXRayedObjects++;
        } else {
            delete this.xrayedObjects[entity.id];
            this._numXRayedObjects--;
        }
        this._xrayedObjectIds = null; // Lazy regenerate
        if (notify) {
            this.fire("objectXRayed", entity, true);
        }
    }

    _deRegisterXRayedObject(entity) {
        delete this.xrayedObjects[entity.id];
        this._numXRayedObjects--;
        this._xrayedObjectIds = null; // Lazy regenerate
    }

    _objectHighlightedUpdated(entity) {
        if (entity.highlighted) {
            this.highlightedObjects[entity.id] = entity;
            this._numHighlightedObjects++;
        } else {
            delete this.highlightedObjects[entity.id];
            this._numHighlightedObjects--;
        }
        this._highlightedObjectIds = null; // Lazy regenerate
    }

    _deRegisterHighlightedObject(entity) {
        delete this.highlightedObjects[entity.id];
        this._numHighlightedObjects--;
        this._highlightedObjectIds = null; // Lazy regenerate
    }

    _objectSelectedUpdated(entity, notify = true) {
        if (entity.selected) {
            this.selectedObjects[entity.id] = entity;
            this._numSelectedObjects++;
        } else {
            delete this.selectedObjects[entity.id];
            this._numSelectedObjects--;
        }
        this._selectedObjectIds = null; // Lazy regenerate
        if (notify) {
            this.fire("objectSelected", entity, true);
        }
    }

    _deRegisterSelectedObject(entity) {
        delete this.selectedObjects[entity.id];
        this._numSelectedObjects--;
        this._selectedObjectIds = null; // Lazy regenerate
    }


    _objectColorizeUpdated(entity, colorized) {
        if (colorized) {
            this.colorizedObjects[entity.id] = entity;
            this._numColorizedObjects++;
        } else {
            delete this.colorizedObjects[entity.id];
            this._numColorizedObjects--;
        }
        this._colorizedObjectIds = null; // Lazy regenerate
    }

    _deRegisterColorizedObject(entity) {
        delete this.colorizedObjects[entity.id];
        this._numColorizedObjects--;
        this._colorizedObjectIds = null; // Lazy regenerate
    }

    _objectOpacityUpdated(entity, opacityUpdated) {
        if (opacityUpdated) {
            this.opacityObjects[entity.id] = entity;
            this._numOpacityObjects++;
        } else {
            delete this.opacityObjects[entity.id];
            this._numOpacityObjects--;
        }
        this._opacityObjectIds = null; // Lazy regenerate
    }

    _deRegisterOpacityObject(entity) {
        delete this.opacityObjects[entity.id];
        this._numOpacityObjects--;
        this._opacityObjectIds = null; // Lazy regenerate
    }

    _objectOffsetUpdated(entity, offset) {
        if (!offset || offset[0] === 0 && offset[1] === 0 && offset[2] === 0) {
            this.offsetObjects[entity.id] = entity;
            this._numOffsetObjects++;
        } else {
            delete this.offsetObjects[entity.id];
            this._numOffsetObjects--;
        }
        this._offsetObjectIds = null; // Lazy regenerate
    }

    _deRegisterOffsetObject(entity) {
        delete this.offsetObjects[entity.id];
        this._numOffsetObjects--;
        this._offsetObjectIds = null; // Lazy regenerate
    }

    _webglContextLost() {
        //  this.loading++;
        this.canvas.spinner.processes++;
        for (const id in this.components) {
            if (this.components.hasOwnProperty(id)) {
                const component = this.components[id];
                if (component._webglContextLost) {
                    component._webglContextLost();
                }
            }
        }
        this._renderer.webglContextLost();
    }

    _webglContextRestored() {
        const gl = this.canvas.gl;
        for (const id in this.components) {
            if (this.components.hasOwnProperty(id)) {
                const component = this.components[id];
                if (component._webglContextRestored) {
                    component._webglContextRestored(gl);
                }
            }
        }
        this._renderer.webglContextRestored(gl);
        //this.loading--;
        this.canvas.spinner.processes--;
    }

    /**
     * Returns the capabilities of this Scene.
     *
     * @private
     * @returns {{astcSupported: boolean, etc1Supported: boolean, pvrtcSupported: boolean, etc2Supported: boolean, dxtSupported: boolean, bptcSupported: boolean}}
     */
    get capabilities() {
        return this._renderer.capabilities;
    }

    /**
     * Whether {@link Entity#offset} is enabled.
     *
     * This is set via the {@link Viewer} constructor and is ````false```` by default.
     *
     * @returns {Boolean} True if {@link Entity#offset} is enabled.
     */
    get entityOffsetsEnabled() {
        return this._entityOffsetsEnabled;
    }

    /**
     * Whether geometry is readable.
     *
     * This is set via the {@link Viewer} constructor and is ````false```` by default.
     *
     * The ````readableGeometryEnabled```` option for ````Scene#pick```` only works if this is set ````true````.
     *
     * Note that when ````true````, this configuration will increase the amount of browser memory used by the Viewer.
     *
     * @returns {Boolean} True if geometry is readable.
     */
    get readableGeometryEnabled() {
        return this._readableGeometry;
    }

    /**
     * Whether precision surface picking is enabled.
     * @deprecated
     * @returns {*|boolean}
     */
    get pickSurfacePrecisionEnabled() {
        return this._readableGeometry;
    }

    /**
     * Whether logarithmic depth buffer is enabled.
     *
     * This is set via the {@link Viewer} constructor and is ````false```` by default.
     *
     * @returns {Boolean} True if logarithmic depth buffer is enabled.
     */
    get logarithmicDepthBufferEnabled() {
        return this._logarithmicDepthBufferEnabled;
    }

    /**
     * Sets the number of {@link SectionPlane}s for which this Scene pre-caches resources.
     *
     * This property enhances the efficiency of SectionPlane creation by proactively allocating and caching Viewer resources for a specified quantity
     * of SectionPlanes. Introducing this parameter streamlines the initial creation speed of SectionPlanes, particularly up to the designated quantity. This parameter internally
     * configures renderer logic for the specified number of SectionPlanes, eliminating the need for setting up logic with each SectionPlane creation and thereby enhancing
     * responsiveness. It is important to consider that each SectionPlane impacts rendering performance, so it is recommended to set this value to a quantity that aligns with
     * your expected usage.
     *
     * Default is ````0````.
     */
    set numCachedSectionPlanes(numCachedSectionPlanes) {
        numCachedSectionPlanes = numCachedSectionPlanes || 0;
        if (this._sectionPlanesState.getNumCachedSectionPlanes() !== numCachedSectionPlanes) {
            this._sectionPlanesState.setNumCachedSectionPlanes(numCachedSectionPlanes);
            this._needRecompile = true;
            this.glRedraw();
        }
    }

    /**
     * Gets the number of {@link SectionPlane}s for which this Scene pre-caches resources.
     *
     * This property enhances the efficiency of SectionPlane creation by proactively allocating and caching Viewer resources for a specified quantity
     * of SectionPlanes. Introducing this parameter streamlines the initial creation speed of SectionPlanes, particularly up to the designated quantity. This parameter internally
     * configures renderer logic for the specified number of SectionPlanes, eliminating the need for setting up logic with each SectionPlane creation and thereby enhancing
     * responsiveness. It is important to consider that each SectionPlane impacts rendering performance, so it is recommended to set this value to a quantity that aligns with
     * your expected usage.
     *
     * Default is ````0````.
     *
     * @returns {number} The number of {@link SectionPlane}s for which this Scene pre-caches resources.
     */
    get numCachedSectionPlanes() {
        return this._sectionPlanesState.getNumCachedSectionPlanes();
    }

    /**
     * Sets whether physically-based rendering is enabled.
     *
     * Default is ````false````.
     */
    set pbrEnabled(pbrEnabled) {
        this._pbrEnabled = !!pbrEnabled;
        this.glRedraw();
    }

    /**
     * Gets whether physically-based rendering is enabled.
     *
     * Default is ````false````.
     *
     * @returns {Boolean} True if quality rendering is enabled.
     */
    get pbrEnabled() {
        return this._pbrEnabled;
    }

    /**
     * Sets whether data texture scene representation (DTX) is enabled for the {@link Scene}.
     *
     * Even when enabled, DTX will only work if supported.
     *
     * Default value is ````false````.
     *
     * @type {Boolean}
     */
    set dtxEnabled(value) {
        value = !!value;
        if (this._dtxEnabled === value) {
            return;
        }
        this._dtxEnabled = value;
    }

    /**
     * Gets whether data texture-based scene representation (DTX) is enabled for the {@link Scene}.
     *
     * Even when enabled, DTX will only apply if supported.
     *
     * Default value is ````false````.
     *
     * @type {Boolean}
     */
    get dtxEnabled() {
        return this._dtxEnabled;
    }

    /**
     * Sets whether basic color texture rendering is enabled.
     *
     * Default is ````true````.
     *
     * @returns {Boolean} True if basic color texture rendering is enabled.
     */
    set colorTextureEnabled(colorTextureEnabled) {
        this._colorTextureEnabled = !!colorTextureEnabled;
        this.glRedraw();
    }

    /**
     * Gets whether basic color texture rendering is enabled.
     *
     * Default is ````true````.
     *
     * @returns {Boolean} True if basic color texture rendering is enabled.
     */
    get colorTextureEnabled() {
        return this._colorTextureEnabled;
    }

    /**
     * Gets the Z value of offset for Marker's OcclusionTester.
     * The closest the value is to 0.000 the more precise OcclusionTester will be, but at the same time the less
     * precise it will behave for Markers that are located exactly on the Surface.
     *
     * Default is ````-0.001````.
     *
     * @returns {Number} Z offset for Marker
     */
    get markerZOffset() {
        if (this._markerZOffset == null) {
            return -1e-3;
        }
        return this._markerZOffset;
    }

    /**
     * Performs an occlusion test on all {@link Marker}s in this {@link Scene}.
     *
     * Sets each {@link Marker#visible} ````true```` if the Marker is currently not occluded by any opaque {@link Entity}s
     * in the Scene, or ````false```` if an Entity is occluding it.
     */
    doOcclusionTest() {
        if (this._needRecompile) {
            this._recompile();
            this._needRecompile = false;
        }
        this._renderer.doOcclusionTest();
    }

    /**
     * Renders a single frame of this Scene.
     *
     * The Scene will periodically render itself after any updates, but you can call this method to force a render
     * if required.
     *
     * @param {Boolean} [forceRender=false] Forces a render when true, otherwise only renders if something has changed in this Scene
     * since the last render.
     */
    render(forceRender) {

        if (forceRender) {
            core.runTasks();
        }

        const renderEvent = {
            sceneId: null,
            pass: 0
        };

        if (this._needRecompile) {
            this._recompile();
            this._renderer.imageDirty();
            this._needRecompile = false;
        }

        if (!forceRender && !this._renderer.needsRender()) {
            return;
        }

        renderEvent.sceneId = this.id;

        const passes = this._passes;
        const clearEachPass = this._clearEachPass;
        let pass;
        let clear;

        for (pass = 0; pass < passes; pass++) {

            renderEvent.pass = pass;

            /**
             * Fired when about to render a frame for a Scene.
             *
             * @event rendering
             * @param {String} sceneID The ID of this Scene.
             * @param {Number} pass Index of the pass we are about to render (see {@link Scene#passes}).
             */
            this.fire("rendering", renderEvent, true);

            clear = clearEachPass || (pass === 0);

            this._renderer.render({pass: pass, clear: clear, force: forceRender});

            /**
             * Fired when we have just rendered a frame for a Scene.
             *
             * @event rendering
             * @param {String} sceneID The ID of this Scene.
             * @param {Number} pass Index of the pass we rendered (see {@link Scene#passes}).
             */
            this.fire("rendered", renderEvent, true);
        }

        this._saveAmbientColor();
    }


    /**
     * @private
     */
    compile() {
        if (this._needRecompile) {
            this._recompile();
            this._renderer.imageDirty();
            this._needRecompile = false;
        }
    }

    _recompile() {
        for (const id in this._compilables) {
            if (this._compilables.hasOwnProperty(id)) {
                this._compilables[id].compile();
            }
        }
        this._renderer.shadowsDirty();
        this.fire("compile", this, true);
    }

    _saveAmbientColor() {
        const canvas = this.canvas;
        if (!canvas.transparent && !canvas.backgroundImage && !canvas.backgroundColor) {
            const ambientColorIntensity = this._lightsState.getAmbientColorAndIntensity();
            if (!this._lastAmbientColor ||
                this._lastAmbientColor[0] !== ambientColorIntensity[0] ||
                this._lastAmbientColor[1] !== ambientColorIntensity[1] ||
                this._lastAmbientColor[2] !== ambientColorIntensity[2] ||
                this._lastAmbientColor[3] !== ambientColorIntensity[3]) {
                canvas.backgroundColor = ambientColorIntensity;
                if (!this._lastAmbientColor) {
                    this._lastAmbientColor = math.vec4([0, 0, 0, 1]);
                }
                this._lastAmbientColor.set(ambientColorIntensity);
            }
        } else {
            this._lastAmbientColor = null;
        }
    }

    /**
     * Gets the IDs of the {@link Entity}s in {@link Scene#models}.
     *
     * @type {String[]}
     */
    get modelIds() {
        if (!this._modelIds) {
            this._modelIds = Object.keys(this.models);
        }
        return this._modelIds;
    }

    /**
     * Gets the number of {@link Entity}s in {@link Scene#objects}.
     *
     * @type {Number}
     */
    get numObjects() {
        return this._numObjects;
    }

    /**
     * Gets the IDs of the {@link Entity}s in {@link Scene#objects}.
     *
     * @type {String[]}
     */
    get objectIds() {
        if (!this._objectIds) {
            this._objectIds = Object.keys(this.objects);
        }
        return this._objectIds;
    }

    /**
     * Gets the number of {@link Entity}s in {@link Scene#visibleObjects}.
     *
     * @type {Number}
     */
    get numVisibleObjects() {
        return this._numVisibleObjects;
    }

    /**
     * Gets the IDs of the {@link Entity}s in {@link Scene#visibleObjects}.
     *
     * @type {String[]}
     */
    get visibleObjectIds() {
        if (!this._visibleObjectIds) {
            this._visibleObjectIds = Object.keys(this.visibleObjects);
        }
        return this._visibleObjectIds;
    }

    /**
     * Gets the number of {@link Entity}s in {@link Scene#xrayedObjects}.
     *
     * @type {Number}
     */
    get numXRayedObjects() {
        return this._numXRayedObjects;
    }

    /**
     * Gets the IDs of the {@link Entity}s in {@link Scene#xrayedObjects}.
     *
     * @type {String[]}
     */
    get xrayedObjectIds() {
        if (!this._xrayedObjectIds) {
            this._xrayedObjectIds = Object.keys(this.xrayedObjects);
        }
        return this._xrayedObjectIds;
    }

    /**
     * Gets the number of {@link Entity}s in {@link Scene#highlightedObjects}.
     *
     * @type {Number}
     */
    get numHighlightedObjects() {
        return this._numHighlightedObjects;
    }

    /**
     * Gets the IDs of the {@link Entity}s in {@link Scene#highlightedObjects}.
     *
     * @type {String[]}
     */
    get highlightedObjectIds() {
        if (!this._highlightedObjectIds) {
            this._highlightedObjectIds = Object.keys(this.highlightedObjects);
        }
        return this._highlightedObjectIds;
    }

    /**
     * Gets the number of {@link Entity}s in {@link Scene#selectedObjects}.
     *
     * @type {Number}
     */
    get numSelectedObjects() {
        return this._numSelectedObjects;
    }

    /**
     * Gets the IDs of the {@link Entity}s in {@link Scene#selectedObjects}.
     *
     * @type {String[]}
     */
    get selectedObjectIds() {
        if (!this._selectedObjectIds) {
            this._selectedObjectIds = Object.keys(this.selectedObjects);
        }
        return this._selectedObjectIds;
    }

    /**
     * Gets the number of {@link Entity}s in {@link Scene#colorizedObjects}.
     *
     * @type {Number}
     */
    get numColorizedObjects() {
        return this._numColorizedObjects;
    }

    /**
     * Gets the IDs of the {@link Entity}s in {@link Scene#colorizedObjects}.
     *
     * @type {String[]}
     */
    get colorizedObjectIds() {
        if (!this._colorizedObjectIds) {
            this._colorizedObjectIds = Object.keys(this.colorizedObjects);
        }
        return this._colorizedObjectIds;
    }

    /**
     * Gets the IDs of the {@link Entity}s in {@link Scene#opacityObjects}.
     *
     * @type {String[]}
     */
    get opacityObjectIds() {
        if (!this._opacityObjectIds) {
            this._opacityObjectIds = Object.keys(this.opacityObjects);
        }
        return this._opacityObjectIds;
    }

    /**
     * Gets the IDs of the {@link Entity}s in {@link Scene#offsetObjects}.
     *
     * @type {String[]}
     */
    get offsetObjectIds() {
        if (!this._offsetObjectIds) {
            this._offsetObjectIds = Object.keys(this.offsetObjects);
        }
        return this._offsetObjectIds;
    }

    /**
     * Sets the number of "ticks" that happen between each render or this Scene.
     *
     * Default value is ````1````.
     *
     * @type {Number}
     */
    set ticksPerRender(value) {
        if (value === undefined || value === null) {
            value = 1;
        } else if (!utils.isNumeric(value) || value <= 0) {
            this.error("Unsupported value for 'ticksPerRender': '" + value +
                "' - should be an integer greater than zero.");
            value = 1;
        }
        if (value === this._ticksPerRender) {
            return;
        }
        this._ticksPerRender = value;
    }

    /**
     * Gets the number of "ticks" that happen between each render or this Scene.
     *
     * Default value is ````1````.
     *
     * @type {Number}
     */
    get ticksPerRender() {
        return this._ticksPerRender;
    }

    /**
     * Sets the number of "ticks" that happen between occlusion testing for {@link Marker}s.
     *
     * Default value is ````20````.
     *
     * @type {Number}
     */
    set ticksPerOcclusionTest(value) {
        if (value === undefined || value === null) {
            value = 20;
        } else if (!utils.isNumeric(value) || value <= 0) {
            this.error("Unsupported value for 'ticksPerOcclusionTest': '" + value +
                "' - should be an integer greater than zero.");
            value = 20;
        }
        if (value === this._ticksPerOcclusionTest) {
            return;
        }
        this._ticksPerOcclusionTest = value;
    }

    /**
     * Gets the number of "ticks" that happen between each render of this Scene.
     *
     * Default value is ````1````.
     *
     * @type {Number}
     */
    get ticksPerOcclusionTest() {
        return this._ticksPerOcclusionTest;
    }

    /**
     * Sets the number of times this Scene renders per frame.
     *
     * Default value is ````1````.
     *
     * @type {Number}
     */
    set passes(value) {
        if (value === undefined || value === null) {
            value = 1;
        } else if (!utils.isNumeric(value) || value <= 0) {
            this.error("Unsupported value for 'passes': '" + value +
                "' - should be an integer greater than zero.");
            value = 1;
        }
        if (value === this._passes) {
            return;
        }
        this._passes = value;
        this.glRedraw();
    }

    /**
     * Gets the number of times this Scene renders per frame.
     *
     * Default value is ````1````.
     *
     * @type {Number}
     */
    get passes() {
        return this._passes;
    }

    /**
     * When {@link Scene#passes} is greater than ````1````, indicates whether or not to clear the canvas before each pass (````true````) or just before the first pass (````false````).
     *
     * Default value is ````false````.
     *
     * @type {Boolean}
     */
    set clearEachPass(value) {
        value = !!value;
        if (value === this._clearEachPass) {
            return;
        }
        this._clearEachPass = value;
        this.glRedraw();
    }

    /**
     * When {@link Scene#passes} is greater than ````1````, indicates whether or not to clear the canvas before each pass (````true````) or just before the first pass (````false````).
     *
     * Default value is ````false````.
     *
     * @type {Boolean}
     */
    get clearEachPass() {
        return this._clearEachPass;
    }

    /**
     * Sets whether or not {@link Scene} should expect all {@link Texture}s and colors to have pre-multiplied gamma.
     *
     * Default value is ````false````.
     *
     * @type {Boolean}
     */
    set gammaInput(value) {
        value = value !== false;
        if (value === this._renderer.gammaInput) {
            return;
        }
        this._renderer.gammaInput = value;
        this._needRecompile = true;
        this.glRedraw();
    }

    /**
     * Gets whether or not {@link Scene} should expect all {@link Texture}s and colors to have pre-multiplied gamma.
     *
     * Default value is ````false````.
     *
     * @type {Boolean}
     */
    get gammaInput() {
        return this._renderer.gammaInput;
    }

    /**
     * Sets whether or not to render pixels with pre-multiplied gama.
     *
     * Default value is ````false````.
     *
     * @type {Boolean}
     */
    set gammaOutput(value) {
        value = !!value;
        if (value === this._renderer.gammaOutput) {
            return;
        }
        this._renderer.gammaOutput = value;
        this._needRecompile = true;
        this.glRedraw();
    }

    /**
     * Gets whether or not to render pixels with pre-multiplied gama.
     *
     * Default value is ````true````.
     *
     * @type {Boolean}
     */
    get gammaOutput() {
        return this._renderer.gammaOutput;
    }

    /**
     * Sets the gamma factor to use when {@link Scene#gammaOutput} is set true.
     *
     * Default value is ````2.2````.
     *
     * @type {Number}
     */
    set gammaFactor(value) {
        value = (value === undefined || value === null) ? 2.2 : value;
        if (value === this._renderer.gammaFactor) {
            return;
        }
        this._renderer.gammaFactor = value;
        this.glRedraw();
    }

    /**
     * Gets the gamma factor to use when {@link Scene#gammaOutput} is set true.
     *
     * Default value is ````2.2````.
     *
     * @type {Number}
     */
    get gammaFactor() {
        return this._renderer.gammaFactor;
    }

    /**
     * Gets the default {@link Geometry} for this Scene, which is a {@link ReadableGeometry} with a unit-sized box shape.
     *
     * Has {@link ReadableGeometry#id} set to "default.geometry".
     *
     * {@link Mesh}s in this Scene have {@link Mesh#geometry} set to this {@link ReadableGeometry} by default.
     *
     * @type {ReadableGeometry}
     */
    get geometry() {
        return this.components["default.geometry"] || buildBoxGeometry(ReadableGeometry);
    }

    /**
     * Gets the default {@link Material} for this Scene, which is a {@link PhongMaterial}.
     *
     * Has {@link PhongMaterial#id} set to "default.material".
     *
     * {@link Mesh}s in this Scene have {@link Mesh#material} set to this {@link PhongMaterial} by default.
     *
     * @type {PhongMaterial}
     */
    get material() {
        return this.components["default.material"] || new PhongMaterial(this, {
            id: "default.material",
            emissive: [0.4, 0.4, 0.4], // Visible by default on geometry without normals
            dontClear: true
        });
    }

    /**
     * Gets the default xraying {@link EmphasisMaterial} for this Scene.
     *
     * Has {@link EmphasisMaterial#id} set to "default.xrayMaterial".
     *
     * {@link Mesh}s in this Scene have {@link Mesh#xrayMaterial} set to this {@link EmphasisMaterial} by default.
     *
     * {@link Mesh}s are xrayed while {@link Mesh#xrayed} is ````true````.
     *
     * @type {EmphasisMaterial}
     */
    get xrayMaterial() {
        return this.components["default.xrayMaterial"] || new EmphasisMaterial(this, {
            id: "default.xrayMaterial",
            fill: true,
            fillColor: [0.970588207244873, 0.7965892553329468, 0.6660899519920349],
            fillAlpha: 0.4,
            edges: true,
            edgeColor: [0.529411792755127, 0.4577854573726654, 0.4100345969200134],
            edgeAlpha: 1.0,
            edgeWidth: 1,
            dontClear: true
        });
    }

    /**
     * Gets the default highlight {@link EmphasisMaterial} for this Scene.
     *
     * Has {@link EmphasisMaterial#id} set to "default.highlightMaterial".
     *
     * {@link Mesh}s in this Scene have {@link Mesh#highlightMaterial} set to this {@link EmphasisMaterial} by default.
     *
     * {@link Mesh}s are highlighted while {@link Mesh#highlighted} is ````true````.
     *
     * @type {EmphasisMaterial}
     */
    get highlightMaterial() {
        return this.components["default.highlightMaterial"] || new EmphasisMaterial(this, {
            id: "default.highlightMaterial",
            fill: true,
            fillColor: [1.0, 1.0, 0.0],
            fillAlpha: 0.5,
            edges: true,
            edgeColor: [1.0, 1.0, 1.0],
            edgeAlpha: 1.0,
            edgeWidth: 1,
            dontClear: true
        });
    }

    /**
     * Gets the default selection {@link EmphasisMaterial} for this Scene.
     *
     * Has {@link EmphasisMaterial#id} set to "default.selectedMaterial".
     *
     * {@link Mesh}s in this Scene have {@link Mesh#highlightMaterial} set to this {@link EmphasisMaterial} by default.
     *
     * {@link Mesh}s are highlighted while {@link Mesh#highlighted} is ````true````.
     *
     * @type {EmphasisMaterial}
     */
    get selectedMaterial() {
        return this.components["default.selectedMaterial"] || new EmphasisMaterial(this, {
            id: "default.selectedMaterial",
            fill: true,
            fillColor: [0.0, 1.0, 0.0],
            fillAlpha: 0.5,
            edges: true,
            edgeColor: [1.0, 1.0, 1.0],
            edgeAlpha: 1.0,
            edgeWidth: 1,
            dontClear: true
        });
    }

    /**
     * Gets the default {@link EdgeMaterial} for this Scene.
     *
     * Has {@link EdgeMaterial#id} set to "default.edgeMaterial".
     *
     * {@link Mesh}s in this Scene have {@link Mesh#edgeMaterial} set to this {@link EdgeMaterial} by default.
     *
     * {@link Mesh}s have their edges emphasized while {@link Mesh#edges} is ````true````.
     *
     * @type {EdgeMaterial}
     */
    get edgeMaterial() {
        return this.components["default.edgeMaterial"] || new EdgeMaterial(this, {
            id: "default.edgeMaterial",
            fill: true,
            fillColor: [0.4, 0.4, 0.4],
            fillAlpha: 0.2,
            edges: true,
            edgeColor: [0.0, 0.0, 0.0],
            edgeAlpha: 1.0,
            edgeWidth: 1,
            dontClear: true
        });
    }

    /**
     * Gets the {@link PointsMaterial} for this Scene.
     *
     * @type {PointsMaterial}
     */
    get pointsMaterial() {
        return this.components["default.pointsMaterial"] || new PointsMaterial(this, {
            id: "default.pointsMaterial",
            fill: true,
            fillColor: [0.4, 0.4, 0.4],
            fillAlpha: 0.2,
            edges: true,
            edgeColor: [0.2, 0.2, 0.2],
            edgeAlpha: 0.5,
            edgeWidth: 1,
            dontClear: true
        });
    }

    /**
     * Gets the {@link LinesMaterial} for this Scene.
     *
     * @type {LinesMaterial}
     */
    get linesMaterial() {
        return this.components["default.linesMaterial"] || new LinesMaterial(this, {
            id: "default.linesMaterial",
            fill: true,
            fillColor: [0.4, 0.4, 0.4],
            fillAlpha: 0.2,
            edges: true,
            edgeColor: [0.2, 0.2, 0.2],
            edgeAlpha: 0.5,
            edgeWidth: 1,
            dontClear: true
        });
    }

    /**
     * Gets the {@link Viewport} for this Scene.
     *
     * @type Viewport
     */
    get viewport() {
        return this._viewport;
    }

    /**
     * Gets the {@link Camera} for this Scene.
     *
     * @type {Camera}
     */
    get camera() {
        return this._camera;
    }

    /**
     * Gets the World-space 3D center of this Scene.
     *
     *@type {Number[]}
     */
    get center() {
        if (this._aabbDirty || !this._center) {
            const aabb = this.aabb;
            if (!this._center) {
                this._center = math.vec3();
            }
            this._center[0] = (aabb[0] + aabb[3]) / 2;
            this._center[1] = (aabb[1] + aabb[4]) / 2;
            this._center[2] = (aabb[2] + aabb[5]) / 2;
        }
        return this._center;
    }

    /**
     * Gets the World-space axis-aligned 3D boundary (AABB) of this Scene.
     *
     * The AABB is represented by a six-element Float64Array containing the min/max extents of the axis-aligned volume, ie. ````[xmin, ymin,zmin,xmax,ymax, zmax]````.
     *
     * When the Scene has no content, will be ````[-100,-100,-100,100,100,100]````.
     *
     * @type {Number[]}
     */
    get aabb() {
        if (this._aabbDirty) {
            if (!this._aabb) {
                this._aabb = math.AABB3();
            }
            let xmin = math.MAX_DOUBLE;
            let ymin = math.MAX_DOUBLE;
            let zmin = math.MAX_DOUBLE;
            let xmax = math.MIN_DOUBLE;
            let ymax = math.MIN_DOUBLE;
            let zmax = math.MIN_DOUBLE;
            let aabb;
            const collidables = this._collidables;
            let collidable;
            let valid = false;
            for (const collidableId in collidables) {
                if (collidables.hasOwnProperty(collidableId)) {
                    collidable = collidables[collidableId];
                    if (collidable.collidable === false) {
                        continue;
                    }
                    aabb = collidable.aabb;
                    if (aabb[0] < xmin) {
                        xmin = aabb[0];
                    }
                    if (aabb[1] < ymin) {
                        ymin = aabb[1];
                    }
                    if (aabb[2] < zmin) {
                        zmin = aabb[2];
                    }
                    if (aabb[3] > xmax) {
                        xmax = aabb[3];
                    }
                    if (aabb[4] > ymax) {
                        ymax = aabb[4];
                    }
                    if (aabb[5] > zmax) {
                        zmax = aabb[5];
                    }
                    valid = true;
                }
            }
            if (!valid) {
                xmin = -100;
                ymin = -100;
                zmin = -100;
                xmax = 100;
                ymax = 100;
                zmax = 100;
            }
            this._aabb[0] = xmin;
            this._aabb[1] = ymin;
            this._aabb[2] = zmin;
            this._aabb[3] = xmax;
            this._aabb[4] = ymax;
            this._aabb[5] = zmax;
            this._aabbDirty = false;
            this._center = null;
        }
        return this._aabb;
    }

    _setAABBDirty() {
        //if (!this._aabbDirty) {
        this._aabbDirty = true;
        this.fire("boundary");
        // }
    }

    /**
     * Attempts to pick an {@link Entity} in this Scene.
     *
     * Ignores {@link Entity}s with {@link Entity#pickable} set ````false````.
     *
     * When an {@link Entity} is picked, fires a "pick" event on the {@link Entity} with the pick result as parameters.
     *
     * Picking the {@link Entity} at the given canvas coordinates:

     * ````javascript
     * var pickResult = scene.pick({
     *          canvasPos: [23, 131]
     *       });
     *
     * if (pickResult) { // Picked an Entity
     *         var entity = pickResult.entity;
     *     }
     * ````
     *
     * Picking, with a ray cast through the canvas, hits an {@link Entity}:
     *
     * ````javascript
     * var pickResult = scene.pick({
     *         pickSurface: true,
     *         canvasPos: [23, 131]
     *      });
     *
     * if (pickResult) { // Picked an Entity
     *
     *     var entity = pickResult.entity;
     *
     *     if (pickResult.primitive === "triangle") {
     *
     *         // Picked a triangle on the entity surface
     *
     *         var primIndex = pickResult.primIndex; // Position of triangle's first index in the picked Entity's Geometry's indices array
     *         var indices = pickResult.indices; // UInt32Array containing the triangle's vertex indices
     *         var localPos = pickResult.localPos; // Float64Array containing the picked Local-space position on the triangle
     *         var worldPos = pickResult.worldPos; // Float64Array containing the picked World-space position on the triangle
     *         var viewPos = pickResult.viewPos; // Float64Array containing the picked View-space position on the triangle
     *         var bary = pickResult.bary; // Float64Array containing the picked barycentric position within the triangle
     *         var worldNormal = pickResult.worldNormal; // Float64Array containing the interpolated World-space normal vector at the picked position on the triangle
     *         var uv = pickResult.uv; // Float64Array containing the interpolated UV coordinates at the picked position on the triangle
     *
     *     } else if (pickResult.worldPos && pickResult.worldNormal) {
     *
     *         // Picked a point and normal on the entity surface
     *
     *         var worldPos = pickResult.worldPos; // Float64Array containing the picked World-space position on the Entity surface
     *         var worldNormal = pickResult.worldNormal; // Float64Array containing the picked World-space normal vector on the Entity Surface
     *     }
     * }
     * ````
     *
     * Picking the {@link Entity} that intersects an arbitrarily-aligned World-space ray:
     *
     * ````javascript
     * var pickResult = scene.pick({
     *       pickSurface: true,   // Picking with arbitrarily-positioned ray
     *       origin: [0,0,-5],    // Ray origin
     *       direction: [0,0,1]   // Ray direction
     * });
     *
     * if (pickResult) { // Picked an Entity with the ray
     *
     *       var entity = pickResult.entity;
     *
     *       if (pickResult.primitive == "triangle") {
     *
     *          // Picked a triangle on the entity surface
     *
     *           var primitive = pickResult.primitive; // Type of primitive that was picked, usually "triangles"
     *           var primIndex = pickResult.primIndex; // Position of triangle's first index in the picked Entity's Geometry's indices array
     *           var indices = pickResult.indices; // UInt32Array containing the triangle's vertex indices
     *           var localPos = pickResult.localPos; // Float64Array containing the picked Local-space position on the triangle
     *           var worldPos = pickResult.worldPos; // Float64Array containing the picked World-space position on the triangle
     *           var viewPos = pickResult.viewPos; // Float64Array containing the picked View-space position on the triangle
     *           var bary = pickResult.bary; // Float64Array containing the picked barycentric position within the triangle
     *           var worldNormal = pickResult.worldNormal; // Float64Array containing the interpolated World-space normal vector at the picked position on the triangle
     *           var uv = pickResult.uv; // Float64Array containing the interpolated UV coordinates at the picked position on the triangle
     *           var origin = pickResult.origin; // Float64Array containing the World-space ray origin
     *           var direction = pickResult.direction; // Float64Array containing the World-space ray direction
     *
     *     } else if (pickResult.worldPos && pickResult.worldNormal) {
     *
     *         // Picked a point and normal on the entity surface
     *
     *         var worldPos = pickResult.worldPos; // Float64Array containing the picked World-space position on the Entity surface
     *         var worldNormal = pickResult.worldNormal; // Float64Array containing the picked World-space normal vector on the Entity Surface
     *     }
     * }
     *  ````
     *
     * @param {*} params Picking parameters.
     * @param {Boolean} [params.pickSurface=false] Whether to find the picked position on the surface of the Entity.
     * @param {Boolean} [params.pickSurfacePrecision=false] When picking an Entity surface position, indicates whether or not we want full-precision {@link PickResult#worldPos}. Only works when {@link Scene#readableGeometryEnabled} is ````true````. If pick succeeds, the returned {@link PickResult} will have {@link PickResult#precision} set ````true````, to indicate that it contains full-precision surface pick results.
     * @param {Boolean} [params.pickSurfaceNormal=false] Whether to find the picked normal on the surface of the Entity. Only works if ````pickSurface```` is given.
     * @param {Number[]} [params.canvasPos] Canvas-space coordinates. When ray-picking, this will override the **origin** and ** direction** parameters and will cause the ray to be fired through the canvas at this position, directly along the negative View-space Z-axis.
     * @param {Number[]} [params.origin] World-space ray origin when ray-picking. Ignored when canvasPos given.
     * @param {Number[]} [params.direction] World-space ray direction when ray-picking. Also indicates the length of the ray. Ignored when canvasPos given.
     * @param {Number[]} [params.matrix] 4x4 transformation matrix to define the World-space ray origin and direction, as an alternative to ````origin```` and ````direction````.
     * @param {Number} [params.snapRadius=30] The snap radius, in canvas pixels.
     * @param {boolean} [params.snapToVertex=true] Whether to snap to vertex. Only works when `canvasPos` given.
     * @param {boolean} [params.snapToEdge=true] Whether to snap to edge. Only works when `canvasPos` given.
     * @param {PickResult} [pickResult] Holds the results of the pick attempt. Will use the Scene's singleton PickResult if you don't supply your own.
     * @returns {PickResult} Holds results of the pick attempt, returned when an {@link Entity} is picked, else null. See method comments for description.
     */
    pick(params, pickResult) {

        if (this.canvas.boundary[2] === 0 || this.canvas.boundary[3] === 0) {
            this.error("Picking not allowed while canvas has zero width or height");
            return null;
        }

        params = params || {};

        params.pickSurface = params.pickSurface || params.rayPick; // Backwards compatibility

        if (!params.canvasPos && !params.matrix && (!params.origin || !params.direction)) {
            this.warn("picking without canvasPos, matrix, or ray origin and direction");
        }

        const includeEntities = params.includeEntities || params.include; // Backwards compat
        if (includeEntities) {
            params.includeEntityIds = getEntityIDMap(this, includeEntities);
        }

        const excludeEntities = params.excludeEntities || params.exclude; // Backwards compat
        if (excludeEntities) {
            params.excludeEntityIds = getEntityIDMap(this, excludeEntities);
        }

        if (this._needRecompile) {
            this._recompile();
            this._renderer.imageDirty();
            this._needRecompile = false;
        }

        if (params.snapToEdge || params.snapToVertex) {
            pickResult = this._renderer.snapPick(params, pickResult);
        } else {
            pickResult = this._renderer.pick(params, pickResult);
        }

        if (pickResult) {
            if (pickResult.entity && pickResult.entity.fire) {
                pickResult.entity.fire("picked", pickResult); // TODO: SceneModelEntity doesn't fire events
            }
        }

        return pickResult;
    }

    /**
     * @param {Object} params Picking parameters.
     * @param {Number[]} params.canvasPos Canvas-space coordinates.
     * @param {Number} [params.snapRadius=30] The snap radius, in canvas pixels
     * @param {boolean} [params.snapToVertex=true] Whether to snap to vertex.
     * @param {boolean} [params.snapToEdge=true] Whether to snap to edge.
     * @deprecated
     */
    snapPick(params) {
        if (undefined === this._warnSnapPickDeprecated) {
            this._warnSnapPickDeprecated = true;
            this.warn("Scene.snapPick() is deprecated since v2.4.2 - use Scene.pick() instead");
        }
        if (!params.canvasPos) {
            this.error("Scene.snapPick() canvasPos parameter expected");
            return;
        }
        return this._renderer.snapPick(params);
    }

    /**
     * Destroys all non-default {@link Component}s in this Scene.
     */
    clear() {
        var component;
        for (const id in this.components) {
            if (this.components.hasOwnProperty(id)) {
                component = this.components[id];
                if (!component._dontClear) { // Don't destroy components like Camera, Input, Viewport etc.
                    component.destroy();
                }
            }
        }
    }

    /**
     * Destroys all {@link Light}s in this Scene..
     */
    clearLights() {
        const ids = Object.keys(this.lights);
        for (let i = 0, len = ids.length; i < len; i++) {
            this.lights[ids[i]].destroy();
        }
    }

    /**
     * Destroys all {@link SectionPlane}s in this Scene.
     */
    clearSectionPlanes() {
        const ids = Object.keys(this.sectionPlanes);
        for (let i = 0, len = ids.length; i < len; i++) {
            this.sectionPlanes[ids[i]].destroy();
        }
    }

    /**
     * Destroys all {@link Line}s in this Scene.
     */
    clearBitmaps() {
        const ids = Object.keys(this.bitmaps);
        for (let i = 0, len = ids.length; i < len; i++) {
            this.bitmaps[ids[i]].destroy();
        }
    }


    /**
     * Destroys all {@link Line}s in this Scene.
     */
    clearLines() {
        const ids = Object.keys(this.lineSets);
        for (let i = 0, len = ids.length; i < len; i++) {
            this.lineSets[ids[i]].destroy();
        }
    }

    /**
     * Gets the collective axis-aligned boundary (AABB) of a batch of {@link Entity}s that represent objects.
     *
     * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````.
     *
     * Each {@link Entity} on which {@link Entity#isObject} is registered by {@link Entity#id} in {@link Scene#visibleObjects}.
     *
     * Each {@link Entity} is only included in the AABB when {@link Entity#collidable} is ````true````.
     *
     * @param {String[]} ids Array of {@link Entity#id} values.
     * @returns {[Number, Number, Number, Number, Number, Number]} An axis-aligned World-space bounding box, given as elements ````[xmin, ymin, zmin, xmax, ymax, zmax]````.
     */
    getAABB(ids) {
        if (ids === undefined) {
            return this.aabb;
        }
        if (utils.isString(ids)) {
            const entity = this.objects[ids];
            if (entity && entity.aabb) { // A Component subclass with an AABB
                return entity.aabb;
            }
            ids = [ids]; // Must be an entity type
        }
        if (ids.length === 0) {
            return this.aabb;
        }
        let xmin = math.MAX_DOUBLE;
        let ymin = math.MAX_DOUBLE;
        let zmin = math.MAX_DOUBLE;
        let xmax = math.MIN_DOUBLE;
        let ymax = math.MIN_DOUBLE;
        let zmax = math.MIN_DOUBLE;
        let valid;
        this.withObjects(ids, entity => {
                if (entity.collidable) {
                    const aabb = entity.aabb;
                    if (aabb[0] < xmin) {
                        xmin = aabb[0];
                    }
                    if (aabb[1] < ymin) {
                        ymin = aabb[1];
                    }
                    if (aabb[2] < zmin) {
                        zmin = aabb[2];
                    }
                    if (aabb[3] > xmax) {
                        xmax = aabb[3];
                    }
                    if (aabb[4] > ymax) {
                        ymax = aabb[4];
                    }
                    if (aabb[5] > zmax) {
                        zmax = aabb[5];
                    }
                    valid = true;
                }
            }
        );
        if (valid) {
            const aabb2 = math.AABB3();
            aabb2[0] = xmin;
            aabb2[1] = ymin;
            aabb2[2] = zmin;
            aabb2[3] = xmax;
            aabb2[4] = ymax;
            aabb2[5] = zmax;
            return aabb2;
        } else {
            return this.aabb; // Scene AABB
        }
    }

    /**
     * Batch-updates {@link Entity#visible} on {@link Entity}s that represent objects.
     *
     * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````.
     *
     * Each {@link Entity} on which both {@link Entity#isObject} and {@link Entity#visible} are ````true```` is
     * registered by {@link Entity#id} in {@link Scene#visibleObjects}.
     *
     * @param {String[]} ids Array of {@link Entity#id} values.
     * @param {Boolean} visible Whether or not to set visible.
     * @returns {Boolean} True if any {@link Entity}s were updated, else false if all updates were redundant and not applied.
     */
    setObjectsVisible(ids, visible) {
        return this.withObjects(ids, entity => {
            const changed = (entity.visible !== visible);
            entity.visible = visible;
            return changed;
        });
    }

    /**
     * Batch-updates {@link Entity#collidable} on {@link Entity}s that represent objects.
     *
     * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````.
     *
     * @param {String[]} ids Array of {@link Entity#id} values.
     * @param {Boolean} collidable Whether or not to set collidable.
     * @returns {Boolean} True if any {@link Entity}s were updated, else false if all updates were redundant and not applied.
     */
    setObjectsCollidable(ids, collidable) {
        return this.withObjects(ids, entity => {
            const changed = (entity.collidable !== collidable);
            entity.collidable = collidable;
            return changed;
        });
    }

    /**
     * Batch-updates {@link Entity#culled} on {@link Entity}s that represent objects.
     *
     * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````.
     *
     * @param {String[]} ids Array of {@link Entity#id} values.
     * @param {Boolean} culled Whether or not to cull.
     * @returns {Boolean} True if any {@link Entity}s were updated, else false if all updates were redundant and not applied.
     */
    setObjectsCulled(ids, culled) {
        return this.withObjects(ids, entity => {
            const changed = (entity.culled !== culled);
            entity.culled = culled;
            return changed;
        });
    }

    /**
     * Batch-updates {@link Entity#selected} on {@link Entity}s that represent objects.
     *
     * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````.
     *
     * Each {@link Entity} on which both {@link Entity#isObject} and {@link Entity#selected} are ````true```` is
     * registered by {@link Entity#id} in {@link Scene#selectedObjects}.
     *
     * @param {String[]} ids Array of {@link Entity#id} values.
     * @param {Boolean} selected Whether or not to select.
     * @returns {Boolean} True if any {@link Entity}s were updated, else false if all updates were redundant and not applied.
     */
    setObjectsSelected(ids, selected) {
        return this.withObjects(ids, entity => {
            const changed = (entity.selected !== selected);
            entity.selected = selected;
            return changed;
        });
    }

    /**
     * Batch-updates {@link Entity#highlighted} on {@link Entity}s that represent objects.
     *
     * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````.
     *
     * Each {@link Entity} on which both {@link Entity#isObject} and {@link Entity#highlighted} are ````true```` is
     * registered by {@link Entity#id} in {@link Scene#highlightedObjects}.
     *
     * @param {String[]} ids Array of {@link Entity#id} values.
     * @param {Boolean} highlighted Whether or not to highlight.
     * @returns {Boolean} True if any {@link Entity}s were updated, else false if all updates were redundant and not applied.
     */
    setObjectsHighlighted(ids, highlighted) {
        return this.withObjects(ids, entity => {
            const changed = (entity.highlighted !== highlighted);
            entity.highlighted = highlighted;
            return changed;
        });
    }

    /**
     * Batch-updates {@link Entity#xrayed} on {@link Entity}s that represent objects.
     *
     * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````.
     *
     * @param {String[]} ids Array of {@link Entity#id} values.
     * @param {Boolean} xrayed Whether or not to xray.
     * @returns {Boolean} True if any {@link Entity}s were updated, else false if all updates were redundant and not applied.
     */
    setObjectsXRayed(ids, xrayed) {
        return this.withObjects(ids, entity => {
            const changed = (entity.xrayed !== xrayed);
            entity.xrayed = xrayed;
            return changed;
        });
    }

    /**
     * Batch-updates {@link Entity#edges} on {@link Entity}s that represent objects.
     *
     * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````.
     *
     * @param {String[]} ids Array of {@link Entity#id} values.
     * @param {Boolean} edges Whether or not to show edges.
     * @returns {Boolean} True if any {@link Entity}s were updated, else false if all updates were redundant and not applied.
     */
    setObjectsEdges(ids, edges) {
        return this.withObjects(ids, entity => {
            const changed = (entity.edges !== edges);
            entity.edges = edges;
            return changed;
        });
    }

    /**
     * Batch-updates {@link Entity#colorize} on {@link Entity}s that represent objects.
     *
     * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````.
     *
     * @param {String[]} ids Array of {@link Entity#id} values.
     * @param {Number[]} [colorize=(1,1,1)] RGB colorize factors, multiplied by the rendered pixel colors.
     * @returns {Boolean} True if any {@link Entity}s changed opacity, else false if all updates were redundant and not applied.
     */
    setObjectsColorized(ids, colorize) {
        return this.withObjects(ids, entity => {
            entity.colorize = colorize;
        });
    }

    /**
     * Batch-updates {@link Entity#opacity} on {@link Entity}s that represent objects.
     *
     * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````.
     *
     * @param {String[]} ids Array of {@link Entity#id} values.
     * @param {Number} [opacity=1.0] Opacity factor, multiplied by the rendered pixel alphas.
     * @returns {Boolean} True if any {@link Entity}s changed opacity, else false if all updates were redundant and not applied.
     */
    setObjectsOpacity(ids, opacity) {
        return this.withObjects(ids, entity => {
            const changed = (entity.opacity !== opacity);
            entity.opacity = opacity;
            return changed;
        });
    }

    /**
     * Batch-updates {@link Entity#pickable} on {@link Entity}s that represent objects.
     *
     * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````.
     *
     * @param {String[]} ids Array of {@link Entity#id} values.
     * @param {Boolean} pickable Whether or not to set pickable.
     * @returns {Boolean} True if any {@link Entity}s were updated, else false if all updates were redundant and not applied.
     */
    setObjectsPickable(ids, pickable) {
        return this.withObjects(ids, entity => {
            const changed = (entity.pickable !== pickable);
            entity.pickable = pickable;
            return changed;
        });
    }

    /**
     * Batch-updates {@link Entity#offset} on {@link Entity}s that represent objects.
     *
     * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````.
     *
     * @param {String[]} ids Array of {@link Entity#id} values.
     * @param {Number[]} [offset] 3D offset vector.
     */
    setObjectsOffset(ids, offset) {
        this.withObjects(ids, entity => {
            entity.offset = offset;
        });
    }

    /**
     * Iterates with a callback over {@link Entity}s that represent objects.
     *
     * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````.
     *
     * @param {String[]} ids Array of {@link Entity#id} values.
     * @param {Function} callback Callback to execute on eacn {@link Entity}.
     * @returns {Boolean} True if any {@link Entity}s were updated, else false if all updates were redundant and not applied.
     */
    withObjects(ids, callback) {
        if (utils.isString(ids)) {
            ids = [ids];
        }
        let changed = false;
        for (let i = 0, len = ids.length; i < len; i++) {
            const id = ids[i];
            let entity = this.objects[id];
            if (entity) {
                changed = callback(entity) || changed;
            } else {
                const modelIds = this.modelIds;
                for (let i = 0, len = modelIds.length; i < len; i++) {
                    const modelId = modelIds[i];
                    const globalObjectId = math.globalizeObjectId(modelId, id);
                    entity = this.objects[globalObjectId];
                    if (entity) {
                        changed = callback(entity) || changed;
                    }
                }
            }
        }
        return changed;
    }

    /**
     * This method will "tickify" the provided `cb` function.
     *
     * This means, the function will be wrapped so:
     *
     * - it runs time-aligned to scene ticks
     * - it runs maximum once per scene-tick
     *
     * @param {Function} cb The function to tickify
     * @returns {Function}
     */
    tickify(cb) {
        const cbString = cb.toString();

        /**
         * Check if the function is already tickified, and if so return the cached one.
         */
        if (cbString in this._tickifiedFunctions) {
            return this._tickifiedFunctions[cbString].wrapperFunc;
        }

        let alreadyRun = 0;
        let needToRun = 0;

        let lastArgs;

        /**
         * The provided `cb` function is replaced with a "set-dirty" function
         *
         * @type {Function}
         */
        const wrapperFunc = function (...args) {
            lastArgs = args;
            needToRun++;
        };

        /**
         * An each scene tick, if the "dirty-flag" is set, run the `cb` function.
         *
         * This will make it run time-aligned to the scene tick.
         */
        const tickSubId = this.on("tick", () => {
            const tmp = needToRun;
            if (tmp > alreadyRun) {
                alreadyRun = tmp;
                cb(...lastArgs);
            }
        });

        /**
         * And, store the list of subscribers.
         */
        this._tickifiedFunctions[cbString] = {tickSubId, wrapperFunc};

        return wrapperFunc;
    }

    /**
     * Destroys this Scene.
     */
    destroy() {

        super.destroy();

        for (const id in this.components) {
            if (this.components.hasOwnProperty(id)) {
                this.components[id].destroy();
            }
        }

        //destroy section caps separately because it's not a component
        this._sectionCaps.destroy();

        this.canvas.gl = null;

        // Memory leak prevention
        this.components = null;
        this.models = null;
        this.objects = null;
        this.visibleObjects = null;
        this.xrayedObjects = null;
        this.highlightedObjects = null;
        this.selectedObjects = null;
        this.colorizedObjects = null;
        this.opacityObjects = null;
        this.sectionPlanes = null;
        this.lights = null;
        this.lightMaps = null;
        this.reflectionMaps = null;
        this._objectIds = null;
        this._visibleObjectIds = null;
        this._xrayedObjectIds = null;
        this._highlightedObjectIds = null;
        this._selectedObjectIds = null;
        this._colorizedObjectIds = null;
        this._sectionCaps = null;
        this.types = null;
        this.components = null;
        this.canvas = null;
        this._renderer = null;
        this.input = null;
        this._viewport = null;
        this._camera = null;
    }
}

const screenPos = math.vec4();
const viewPos = math.vec4();

const tempVec3a$9 = math.vec3();
const tempVec3b$6 = math.vec3();
const tempVec3c$5 = math.vec3();

const tempVec4a$6 = math.vec4();
const tempVec4b$6 = math.vec4();
const tempVec4c$1 = math.vec4();

/**
 * @private
 */
class PanController {

    constructor(scene) {
        this._scene = scene;
    }

    /**
     * Dollys the Camera towards the given target 2D canvas position.
     *
     * When the target's corresponding World-space position is also provided, then this function will also test if we've
     * dollied past the target, and will return ````true```` if that's the case.
     *
     * @param [optionalTargetWorldPos] Optional world position of the target
     * @param targetCanvasPos Canvas position of the target
     * @param dollyDelta Amount to dolly
     * @return True if optionalTargetWorldPos was given, and we've dollied past that position.
     */
    dollyToCanvasPos(optionalTargetWorldPos, targetCanvasPos, dollyDelta) {

        let dolliedThroughSurface = false;

        const camera = this._scene.camera;

        if (optionalTargetWorldPos) {
            const eyeToWorldPosVec = math.subVec3(optionalTargetWorldPos, camera.eye, tempVec3a$9);
            const eyeWorldPosDist = math.lenVec3(eyeToWorldPosVec);
            dolliedThroughSurface = (eyeWorldPosDist < dollyDelta);
        }

        if (camera.projection === "perspective") {

            camera.ortho.scale = camera.ortho.scale - dollyDelta;

            const unprojectedWorldPos = this._unproject(targetCanvasPos, tempVec4a$6);
            const offset = math.subVec3(unprojectedWorldPos, camera.eye, tempVec4c$1);
            const moveVec = math.mulVec3Scalar(math.normalizeVec3(offset), -dollyDelta, []);

            camera.eye = [camera.eye[0] - moveVec[0], camera.eye[1] - moveVec[1], camera.eye[2] - moveVec[2]];
            camera.look = [camera.look[0] - moveVec[0], camera.look[1] - moveVec[1], camera.look[2] - moveVec[2]];

            if (optionalTargetWorldPos) {

                // Subtle UX tweak - if we have a target World position, then set camera eye->look distance to
                // the same distance as from eye->target. This just gives us a better position for look,
                // if we subsequently orbit eye about look, so that we don't orbit a position that's
                // suddenly a lot closer than the point we pivoted about on the surface of the last object
                // that we click-drag-pivoted on.

                const eyeTargetVec = math.subVec3(optionalTargetWorldPos, camera.eye, tempVec3a$9);
                const lenEyeTargetVec = math.lenVec3(eyeTargetVec);
                const eyeLookVec = math.mulVec3Scalar(math.normalizeVec3(math.subVec3(camera.look, camera.eye, tempVec3b$6)), lenEyeTargetVec);
                camera.look = [camera.eye[0] + eyeLookVec[0], camera.eye[1] + eyeLookVec[1], camera.eye[2] + eyeLookVec[2]];
            }

        } else if (camera.projection === "ortho") {

            // - set ortho scale, getting the unprojected targetCanvasPos before and after, get that difference in a vector;
            // - get the vector in which we're dollying;
            // - add both vectors to camera eye and look.

            const worldPos1 = this._unproject(targetCanvasPos, tempVec4a$6);

            camera.ortho.scale = camera.ortho.scale - dollyDelta;
            camera.ortho._update(); // HACK

            const worldPos2 = this._unproject(targetCanvasPos, tempVec4b$6);
            const offset = math.subVec3(worldPos2, worldPos1, tempVec4c$1);
            const eyeLookMoveVec = math.mulVec3Scalar(math.normalizeVec3(math.subVec3(camera.look, camera.eye, tempVec3a$9)), -dollyDelta, tempVec3b$6);
            const moveVec = math.addVec3(offset, eyeLookMoveVec, tempVec3c$5);

            camera.eye = [camera.eye[0] - moveVec[0], camera.eye[1] - moveVec[1], camera.eye[2] - moveVec[2]];
            camera.look = [camera.look[0] - moveVec[0], camera.look[1] - moveVec[1], camera.look[2] - moveVec[2]];
        }

        return dolliedThroughSurface;
    }

    _unproject(canvasPos, worldPos) {

        const camera = this._scene.camera;
        const transposedProjectMat = camera.project.transposedMatrix;
        const Pt3 = transposedProjectMat.subarray(8, 12);
        const Pt4 = transposedProjectMat.subarray(12);
        const D = [0, 0, -1, 1];
        const screenZ = math.dotVec4(D, Pt3) / math.dotVec4(D, Pt4);

        camera.project.unproject(canvasPos, screenZ, screenPos, viewPos, worldPos);

        return worldPos;
    }

    destroy() {
    }
}

const tempVec3a$8 = math.vec3();
const tempVec3b$5 = math.vec3();
const tempVec3c$4 = math.vec3();

const tempVec4a$5 = math.vec4();
const tempVec4b$5 = math.vec4();
const tempVec4c = math.vec4();

const TOP_LIMIT = 0.001;
const BOTTOM_LIMIT = Math.PI - 0.001;


/** @private */
class PivotController {

    /**
     * @private
     */
    constructor(scene, configs) {

        // Pivot math by: http://www.derschmale.com/

        this._scene = scene;
        this._configs = configs;
        this._pivotWorldPos = math.vec3();
        this._cameraOffset = math.vec3();
        this._azimuth = 0;
        this._polar = 0;
        this._radius = 0;
        this._pivotPosSet = false; // Initially false, true as soon as _pivotWorldPos has been set to some value
        this._pivoting = false; // True while pivoting
        this._shown = false;

        this._pivotSphereEnabled = false;
        this._pivotSphere = null;
        this._pivotSphereSize = 1;
        this._pivotSphereGeometry = null;
        this._pivotSphereMaterial = null;
        this._rtcCenter = math.vec3();
        this._rtcPos = math.vec3();

        this._pivotViewPos = math.vec4();
        this._pivotProjPos = math.vec4();
        this._pivotCanvasPos = math.vec2();
        this._cameraDirty = true;

        this._onViewMatrix = this._scene.camera.on("viewMatrix", () => {
            this._cameraDirty = true;
        });

        this._onProjMatrix = this._scene.camera.on("projMatrix", () => {
            this._cameraDirty = true;
        });

        this._onTick = this._scene.on("tick", () => {
            this.updatePivotElement();
            this.updatePivotSphere();
        });
    }

    createPivotSphere() {
        const currentPos = this.getPivotPos();
        const cameraPos = math.vec3();
        math.decomposeMat4(math.inverseMat4(this._scene.viewer.camera.viewMatrix, math.mat4()), cameraPos, math.vec4(), math.vec3());
        const length = math.distVec3(cameraPos, currentPos);
        let radius = (Math.tan(Math.PI / 500) * length) * this._pivotSphereSize;

        if (this._scene.camera.projection == "ortho") {
            radius /= (this._scene.camera.ortho.scale / 2);
        }

        worldToRTCPos(currentPos, this._rtcCenter, this._rtcPos);
        this._pivotSphereGeometry = new VBOGeometry(
            this._scene,
            buildSphereGeometry({ radius })
        );
        this._pivotSphere = new Mesh(this._scene, {
            geometry: this._pivotSphereGeometry,
            material: this._pivotSphereMaterial,
            pickable: false,
            position: this._rtcPos,
            rtcCenter: this._rtcCenter
        });
    };

    destroyPivotSphere() {
        if (this._pivotSphere) {
            this._pivotSphere.destroy();
            this._pivotSphere = null;
        }
        if (this._pivotSphereGeometry) {
            this._pivotSphereGeometry.destroy();
            this._pivotSphereGeometry = null;
        }
    }

    updatePivotElement() {

        const camera = this._scene.camera;
        const canvas = this._scene.canvas;

        if (this._pivoting && this._cameraDirty) {

            math.transformPoint3(camera.viewMatrix, this.getPivotPos(), this._pivotViewPos);
            this._pivotViewPos[3] = 1;
            math.transformPoint4(camera.projMatrix, this._pivotViewPos, this._pivotProjPos);

            const canvasAABB = canvas.boundary;
            const canvasWidth = canvasAABB[2];
            const canvasHeight = canvasAABB[3];

            this._pivotCanvasPos[0] = Math.floor((1 + this._pivotProjPos[0] / this._pivotProjPos[3]) * canvasWidth / 2);
            this._pivotCanvasPos[1] = Math.floor((1 - this._pivotProjPos[1] / this._pivotProjPos[3]) * canvasHeight / 2);

            // data-textures: avoid to do continuous DOM layout calculations            
            let canvasBoundingRect = canvas._lastBoundingClientRect;

            if (!canvasBoundingRect || canvas._canvasSizeChanged)
            {
                const canvasElem = canvas.canvas;

                canvasBoundingRect = canvas._lastBoundingClientRect = canvasElem.getBoundingClientRect ();
            }

            if (this._pivotElement) {
                this._pivotElement.style.left = (Math.floor(canvasBoundingRect.left + this._pivotCanvasPos[0]) - (this._pivotElement.clientWidth / 2) + window.scrollX) + "px";
                this._pivotElement.style.top = (Math.floor(canvasBoundingRect.top + this._pivotCanvasPos[1]) - (this._pivotElement.clientHeight / 2) + window.scrollY) + "px";
            }
            this._cameraDirty = false;
        }
    }

    updatePivotSphere() {
        if (this._pivoting && this._pivotSphere) {
            worldToRTCPos(this.getPivotPos(), this._rtcCenter, this._rtcPos);
            if(!math.compareVec3(this._rtcPos, this._pivotSphere.position)) {
                this.destroyPivotSphere();
                this.createPivotSphere();
            }
        }
    }
    /**
     * Sets the HTML DOM element that will represent the pivot position.
     *
     * @param pivotElement
     */
    setPivotElement(pivotElement) {
        this._pivotElement = pivotElement;
    }

    /**
     * Sets a sphere as the representation of the pivot position.
     *
     * @param {Object} [cfg] Sphere configuration.
     * @param {String} [cfg.size=1] Optional size factor of the sphere. Defaults to 1.
     * @param {String} [cfg.color=Array] Optional maretial color. Defaults to a red.
     */
    enablePivotSphere(cfg = {}) {
        this.destroyPivotSphere();
        this._pivotSphereEnabled = true;
        if (cfg.size) {
            this._pivotSphereSize = cfg.size;
        }
        const color = cfg.color || [1, 0, 0];
        this._pivotSphereMaterial = new PhongMaterial(this._scene, {
            emissive: color,
            ambient: color,
            specular: [0,0,0],
            diffuse: [0,0,0],
        });
    }

    /**
     * Remove the sphere as the representation of the pivot position.
     *
     */
    disablePivotSphere() {
        this.destroyPivotSphere();
        this._pivotSphereEnabled = false;
    }

    /**
     * Begins pivoting.
     */
    startPivot() {

        const camera = this._scene.camera;

        let lookat = math.lookAtMat4v(camera.eye, camera.look, camera.up);
        math.transformPoint3(lookat, this.getPivotPos(), this._cameraOffset);

        const pivotPos = this.getPivotPos();
        this._cameraOffset[2] += math.distVec3(camera.eye, pivotPos);

        lookat = math.inverseMat4(lookat);

        const offset = math.transformVec3(lookat, this._cameraOffset);
        const diff = math.vec3();

        math.subVec3(camera.eye, pivotPos, diff);
        math.addVec3(diff, offset);

        if (camera.zUp) {
            const t = diff[1];
            diff[1] = diff[2];
            diff[2] = t;
        }

        this._radius = math.lenVec3(diff);
        this._polar = Math.acos(diff[1] / this._radius);
        this._azimuth = Math.atan2(diff[0], diff[2]);
        this._pivoting = true;
    }

    _cameraLookingDownwards() { // Returns true if angle between camera viewing direction and World-space "up" axis is too small
        const camera = this._scene.camera;
        const forwardAxis = math.normalizeVec3(math.subVec3(camera.look, camera.eye, tempVec3a$8));
        const rightAxis = math.cross3Vec3(forwardAxis, camera.worldUp, tempVec3b$5);
        let rightAxisLen = math.sqLenVec3(rightAxis);
        return (rightAxisLen <= 0.0001);
    }

    /**
     * Returns true if we are currently pivoting.
     *
     * @returns {Boolean}
     */
    getPivoting() {
        return this._pivoting;
    }

    /**
     * Sets a 3D World-space position to pivot about.
     *
     * @param {Number[]} worldPos The new World-space pivot position.
     */
    setPivotPos(worldPos) {
        this._pivotWorldPos.set(worldPos);
        this._pivotPosSet = true;
    }

    /**
     * Sets the pivot position to the 3D projection of the given 2D canvas coordinates on a sphere centered
     * at the viewpoint. The radius of the sphere is configured via {@link CameraControl#smartPivot}.
     *
     *
     * @param canvasPos
     */
    setCanvasPivotPos(canvasPos) {
        const camera = this._scene.camera;
        const pivotShereRadius = Math.abs(math.distVec3(this._scene.center, camera.eye));
        const transposedProjectMat = camera.project.transposedMatrix;
        const Pt3 = transposedProjectMat.subarray(8, 12);
        const Pt4 = transposedProjectMat.subarray(12);
        const D = [0, 0, -1, 1];
        const screenZ = math.dotVec4(D, Pt3) / math.dotVec4(D, Pt4);
        const worldPos = tempVec4a$5;
        camera.project.unproject(canvasPos, screenZ, tempVec4b$5, tempVec4c, worldPos);
        const eyeWorldPosVec = math.normalizeVec3(math.subVec3(worldPos, camera.eye, tempVec3a$8));
        const posOnSphere = math.addVec3(camera.eye, math.mulVec3Scalar(eyeWorldPosVec, pivotShereRadius, tempVec3b$5), tempVec3c$4);
        this.setPivotPos(posOnSphere);
    }

    /**
     * Gets the current position we're pivoting about.
     * @returns {Number[]} The current World-space pivot position.
     */
    getPivotPos() {
        return (this._pivotPosSet) ? this._pivotWorldPos : this._scene.camera.look; // Avoid pivoting about [0,0,0] by default
    }

    /**
     * Continues to pivot.
     *
     * @param {Number} yawInc Yaw rotation increment.
     * @param {Number} pitchInc Pitch rotation increment.
     */
    continuePivot(yawInc, pitchInc) {
        if (!this._pivoting) {
            return;
        }
        if (yawInc === 0 && pitchInc === 0) {
            return;
        }
        const camera = this._scene.camera;
        var dx = -yawInc;
        const dy = -pitchInc;
        if (camera.worldUp[2] === 1) {
            dx = -dx;
        }
        this._azimuth += -dx * .01;

        const isMovingUp = dy < 0;
        const isMovingDown = dy > 0;
        
        // Track if we're at limits - only check if we're very close to the limit
        const atTopLimit = Math.abs(this._polar - TOP_LIMIT) < 0.005;
        const atBottomLimit = Math.abs(this._polar - BOTTOM_LIMIT) < 0.005;
        
        let newPolar = this._polar + dy * .01;
        
        // Case 1: At top limit and trying to go beyond
        if (atTopLimit && isMovingUp) {
            newPolar = TOP_LIMIT;
        }
        // Case 2: At bottom limit and trying to go beyond
        else if (atBottomLimit && isMovingDown) {
            newPolar = BOTTOM_LIMIT;
        }
        // Case 3: Normal rotation or moving away from a limit
        else {
            newPolar = math.clamp(newPolar, TOP_LIMIT, BOTTOM_LIMIT);
        }

        this._polar = newPolar;

        const pos = [
            this._radius * Math.sin(this._polar) * Math.sin(this._azimuth),
            this._radius * Math.cos(this._polar),
            this._radius * Math.sin(this._polar) * Math.cos(this._azimuth)
        ];
        if (camera.worldUp[2] === 1) {
            const t = pos[1];
            pos[1] = pos[2];
            pos[2] = t;
        }
        // Preserve the eye->look distance, since in xeokit "look" is the point-of-interest, not the direction vector.
        const eyeLookLen = math.lenVec3(math.subVec3(camera.look, camera.eye, math.vec3()));
        const pivotPos = this.getPivotPos();
        math.addVec3(pos, pivotPos);
        let lookat = math.lookAtMat4v(pos, pivotPos, camera.worldUp);
        lookat = math.inverseMat4(lookat);
        const offset = math.transformVec3(lookat, this._cameraOffset);
        lookat[12] -= offset[0];
        lookat[13] -= offset[1];
        lookat[14] -= offset[2];
        const zAxis = [lookat[8], lookat[9], lookat[10]];
        camera.eye = [lookat[12], lookat[13], lookat[14]];
        math.subVec3(camera.eye, math.mulVec3Scalar(zAxis, eyeLookLen), camera.look);
        camera.up = [lookat[4], lookat[5], lookat[6]];
        this.showPivot();
    }

    /**
     * Shows the pivot position.
     *
     * Only works if we set an  HTML DOM element to represent the pivot position.
     */
    showPivot() {
        if (this._shown) {
            return;
        }
        if (this._pivotElement) {
            this.updatePivotElement();
            this._pivotElement.style.visibility = "visible";
        }
        if (this._pivotSphereEnabled) {
            this.destroyPivotSphere();
            this.createPivotSphere();
        }
        this._shown = true;
    }

    /**
     * Hides the pivot position.
     *
     * Only works if we set an  HTML DOM element to represent the pivot position.
     */
    hidePivot() {
        if (!this._shown) {
            return;
        }
        if (this._pivotElement) {
            this._pivotElement.style.visibility = "hidden";
        }
        if (this._pivotSphereEnabled) {
            this.destroyPivotSphere();
        }
        this._shown = false;
    }

    /**
     * Finishes pivoting.
     */
    endPivot() {
        this._pivoting = false;
    }

    destroy() {
        this.destroyPivotSphere();
        this._scene.camera.off(this._onViewMatrix);
        this._scene.camera.off(this._onProjMatrix);
        this._scene.off(this._onTick);
    }
}

/**
 *
 * @private
 */
class PickController {

    constructor(cameraControl, configs) {
        /**
         * @type {Scene}
         */
        this._scene = cameraControl.scene;

        this._cameraControl = cameraControl;

        this._scene.canvas.canvas.oncontextmenu = function (e) {
            e.preventDefault();
        };

        this._configs = configs;

        /**
         * Set true to schedule picking of an Entity.
         * @type {boolean}
         */
        this.schedulePickEntity = false;

        /**
         * Set true to schedule picking of a position on teh surface of an Entity.
         * @type {boolean}
         */
        this.schedulePickSurface = false;

        /**
         * Set true to schedule snap-picking with surface picking as a fallback - used for measurement.
         * @type {boolean}
         */
        this.scheduleSnapOrPick = false;

        /**
         * The canvas position at which to do the next scheduled pick.
         * @type {Number[]}
         */
        this.pickCursorPos = math.vec2();

        /**
         * Will be true after picking to indicate that something was picked.
         * @type {boolean}
         */
        this.picked = false;

        /**
         * Will be true after picking to indicate that a position on the surface of an Entity was picked.
         * @type {boolean}
         */
        this.pickedSurface = false;

        /**
         * Will hold the PickResult after after picking.
         * @type {PickResult}
         */
        this.pickResult = null;

        this._lastPickedEntityId = null;

        this._lastHash = null;

        this._needFireEvents = 0;
    }

    /**
     * Immediately attempts a pick, if scheduled.
     */
    update() {

        if (!this._configs.pointerEnabled) {
            return;
        }

        if (!this.schedulePickEntity && !this.schedulePickSurface) {
            return;
        }

        const hash = `${~~this.pickCursorPos[0]}-${~~this.pickCursorPos[1]}-${this.scheduleSnapOrPick}-${this.schedulePickSurface}-${this.schedulePickEntity}`;
        if (this._lastHash === hash) {
            return;
        }

        this.picked = false;
        this.pickedSurface = false;
        this.snappedOrPicked = false;
        this.hoveredSnappedOrSurfaceOff = false;

        const hasHoverSurfaceSubs = this._cameraControl.hasSubs("hoverSurface");

        if (this.scheduleSnapOrPick) {
            const snapPickResult = this._scene.pick({
                canvasPos: this.pickCursorPos,
                snapRadius: this._configs.snapRadius,
                snapToVertex: this._configs.snapToVertex,
                snapToEdge: this._configs.snapToEdge,
            });
            if (snapPickResult && (snapPickResult.snappedToEdge || snapPickResult.snappedToVertex)) {
                this.snapPickResult = snapPickResult;
                this.snappedOrPicked = true;
                this._needFireEvents++;
            } else {
                this.schedulePickSurface = true; // Fallback
                this.snapPickResult = null;
            }
        }

        if (this.schedulePickSurface) {
            if (this.pickResult && this.pickResult.worldPos) {
                const pickResultCanvasPos = this.pickResult.canvasPos;
                if (pickResultCanvasPos[0] === this.pickCursorPos[0] && pickResultCanvasPos[1] === this.pickCursorPos[1]) {
                    this.picked = true;
                    this.pickedSurface = true;
                    this._needFireEvents += hasHoverSurfaceSubs ? 1 : 0;
                    this.schedulePickEntity = false;
                    this.schedulePickSurface = false;
                    if (this.scheduleSnapOrPick) {
                        this.snappedOrPicked = true;
                    } else {
                        this.hoveredSnappedOrSurfaceOff = true;
                    }
                    this.scheduleSnapOrPick = false;
                    return;
                }
            }
        }

        if (this.schedulePickEntity) {
            if (this.pickResult && (this.pickResult.canvasPos || this.pickResult.snappedCanvasPos)) {
                const pickResultCanvasPos = this.pickResult.canvasPos || this.pickResult.snappedCanvasPos;
                if (pickResultCanvasPos[0] === this.pickCursorPos[0] && pickResultCanvasPos[1] === this.pickCursorPos[1]) {
                    this.picked = true;
                    this.pickedSurface = false;
                    this.schedulePickEntity = false;
                    this.schedulePickSurface = false;
                    return;
                }
            }
        }

        if (this.schedulePickSurface || (this.scheduleSnapOrPick && !this.snapPickResult)) {
            this.pickResult = this._scene.pick({
                pickSurface: true,
                pickSurfaceNormal: false,
                canvasPos: this.pickCursorPos
            });
            if (this.pickResult) {
                this.picked = true;
                if (this.scheduleSnapOrPick) {
                    this.snappedOrPicked = true;
                } else {
                    this.pickedSurface = true;
                }
                this._needFireEvents++;
            } else if (this.scheduleSnapOrPick) {
                this.hoveredSnappedOrSurfaceOff = true;
                this._needFireEvents++;
            }

        } else { // schedulePickEntity == true

            this.pickResult = this._scene.pick({
                canvasPos: this.pickCursorPos
            });

            if (this.pickResult) {
                this.picked = true;
                this.pickedSurface = false;
                this._needFireEvents++;
            }
        }

        this.scheduleSnapOrPick = false;
        this.schedulePickEntity = false;
        this.schedulePickSurface = false;
    }

    fireEvents() {

        if (this._needFireEvents === 0) {
            return;
        }

        if (this.hoveredSnappedOrSurfaceOff) {
            this._cameraControl.fire("hoverSnapOrSurfaceOff", {
                canvasPos: this.pickCursorPos,
                pointerPos : this.pickCursorPos
            }, true);
        }

        if (this.snappedOrPicked) {
            if (this.snapPickResult) {
                const pickResult = new PickResult();
                pickResult.entity = this.snapPickResult.entity;
                pickResult.snappedToVertex = this.snapPickResult.snappedToVertex;
                pickResult.snappedToEdge = this.snapPickResult.snappedToEdge;
                pickResult.worldPos = this.snapPickResult.worldPos;
                pickResult.canvasPos = this.pickCursorPos;
                pickResult.snappedCanvasPos = this.snapPickResult.snappedCanvasPos;
                this._cameraControl.fire("hoverSnapOrSurface", pickResult, true);
                this.snapPickResult = null;
            } else {
                this._cameraControl.fire("hoverSnapOrSurface", this.pickResult, true);
            }
        }

        if (this.picked && this.pickResult && (this.pickResult.entity || this.pickResult.worldPos)) {

            if (this.pickResult.entity) {

                const pickedEntityId = this.pickResult.entity.id;

                if (this._lastPickedEntityId !== pickedEntityId) {

                    if (this._lastPickedEntityId !== undefined) {
                        this._cameraControl.fire("hoverOut", {
                            entity: this._scene.objects[this._lastPickedEntityId]
                        }, true);
                    }

                    this._cameraControl.fire("hoverEnter", this.pickResult, true);
                    this._lastPickedEntityId = pickedEntityId;
                }
            }

            this._cameraControl.fire("hover", this.pickResult, true);

            if (this.pickResult.worldPos) {
                this.pickedSurface = true;
                this._cameraControl.fire("hoverSurface", this.pickResult, true);
            }

        } else {

            if (this._lastPickedEntityId !== undefined) {
                this._cameraControl.fire("hoverOut", {
                    entity: this._scene.objects[this._lastPickedEntityId]
                }, true);
                this._lastPickedEntityId = undefined;
            }

            this._cameraControl.fire("hoverOff", {
                canvasPos: this.pickCursorPos
            }, true);
        }

        this.pickResult = null;

        this._needFireEvents = 0;
    }
}

/**
 * @private
 */

const canvasPos = math.vec2();

const getCanvasPosFromEvent$3 = function (event, canvasPos) {
    if (!event) {
        event = window.event;
        canvasPos[0] = event.x;
        canvasPos[1] = event.y;
    } else {
        let element = event.target;
        let totalOffsetLeft = 0;
        let totalOffsetTop = 0;
        let totalScrollX = 0;
        let totalScrollY = 0;
        while (element.offsetParent) {
          totalOffsetLeft += element.offsetLeft;
          totalOffsetTop += element.offsetTop;
          totalScrollX += element.scrollLeft;
          totalScrollY += element.scrollTop;
          element = element.offsetParent;
        }
        canvasPos[0] = event.pageX + totalScrollX - totalOffsetLeft;
        canvasPos[1] = event.pageY + totalScrollY - totalOffsetTop;
    }
    return canvasPos;
};

/**
 * @private
 */
class MousePanRotateDollyHandler {

    constructor(scene, controllers, configs, states, updates) {

        this._scene = scene;

        const pickController = controllers.pickController;
        const cameraControl = controllers.cameraControl;

        let lastX = 0;
        let lastY = 0;
        let lastXDown = 0;
        let lastYDown = 0;

        let mouseDownLeft;
        let mouseDownMiddle;
        let mouseDownRight;

        let mouseDownPicked = false;
        const pickedWorldPos = math.vec3();

        let mouseMovedOnCanvasSinceLastWheel = true;

        const canvas = this._scene.canvas.canvas;

        const keyDown = [];

        document.addEventListener("keydown", this._documentKeyDownHandler = (e) => {
            if (!(configs.active && configs.pointerEnabled) || (!scene.input.keyboardEnabled)) {
                return;
            }
            const keyCode = e.keyCode;
            keyDown[keyCode] = true;
        });

        document.addEventListener("keyup", this._documentKeyUpHandler = (e) => {
            if (!(configs.active && configs.pointerEnabled) || (!scene.input.keyboardEnabled)) {
                return;
            }
            const keyCode = e.keyCode;
            keyDown[keyCode] = false;
        });

        document.addEventListener("visibilitychange", this._onVisibilityChange = () => {
            keyDown.splice(0);
        });

        window.addEventListener("blur", this._onBlur = () => {
            keyDown.splice(0);
        });

        function setMousedownState(pick = true) {
            setMousedownPositions();
            if (pick) {
                setMousedownPick();
            }
        }

        function setMousedownPositions() {

            lastX = states.pointerCanvasPos[0];
            lastY = states.pointerCanvasPos[1];
            lastXDown = states.pointerCanvasPos[0];
            lastYDown = states.pointerCanvasPos[1];
        }

        function setMousedownPick() {
            pickController.pickCursorPos = states.pointerCanvasPos;
            pickController.schedulePickSurface = true;
            pickController.update();

            if (pickController.picked && pickController.pickedSurface && pickController.pickResult && pickController.pickResult.worldPos) {
                mouseDownPicked = true;
                pickedWorldPos.set(pickController.pickResult.worldPos);
            } else {
                mouseDownPicked = false;
            }
        }

        function isPanning() {
            return configs.planView || cameraControl._isKeyDownForAction(cameraControl.MOUSE_PAN, keyDown);
        }

        function isRotating() {
            return cameraControl._isKeyDownForAction(cameraControl.MOUSE_ROTATE, keyDown);
        }


        canvas.addEventListener("mousedown", this._mouseDownHandler = (e) => {

            if (!(configs.active && configs.pointerEnabled)) {
                return;
            }

            switch (e.which) {

                case 1: // Left button

                    if (keyDown[scene.input.KEY_SHIFT] || configs.planView) {

                        mouseDownLeft = true;
                        keyDown[scene.input.MOUSE_LEFT_BUTTON] = true;

                        setMousedownState();

                    } else {

                        mouseDownLeft = true;
                        keyDown[scene.input.MOUSE_LEFT_BUTTON] = true;

                        setMousedownState(false);
                    }

                    break;

                case 2: // Middle/both buttons

                    mouseDownMiddle = true;
                    keyDown[scene.input.MOUSE_MIDDLE_BUTTON] = true;

                    setMousedownState();

                    break;

                case 3: // Right button

                    mouseDownRight = true;
                    keyDown[scene.input.MOUSE_RIGHT_BUTTON] = true;

                    if (configs.panRightClick) {

                        setMousedownState();
                    }

                    break;
            }
        });

        document.addEventListener("mousemove", this._documentMouseMoveHandler = (e) => {

            if (!(configs.active && configs.pointerEnabled)) {
                return;
            }

            if (!mouseDownLeft && !mouseDownMiddle && !mouseDownRight) {
                return;
            }

            // Scaling drag-rotate to canvas boundary

            const canvasBoundary = scene.canvas.boundary;

            const canvasWidth = canvasBoundary[2];
            const canvasHeight = canvasBoundary[3];
            const x = states.pointerCanvasPos[0];
            const y = states.pointerCanvasPos[1];

            const panning = isPanning();
            const rotating = isRotating();

            const xDelta = document.pointerLockElement ? e.movementX : (x - lastX);
            const yDelta = document.pointerLockElement ? e.movementY : (y - lastY);

            if (panning) {

                const camera = scene.camera;

                // We use only canvasHeight here so that aspect ratio does not distort speed

                if (camera.projection === "perspective") {

                    const depth = Math.abs(mouseDownPicked ? math.lenVec3(math.subVec3(pickedWorldPos, scene.camera.eye, [])) : scene.camera.eyeLookDist);
                    const targetDistance = depth * Math.tan((camera.perspective.fov / 2) * Math.PI / 180.0);

                    updates.panDeltaX += (1.5 * xDelta * targetDistance / canvasHeight);
                    updates.panDeltaY += (1.5 * yDelta * targetDistance / canvasHeight);

                } else {

                    updates.panDeltaX += 0.5 * camera.ortho.scale * (xDelta / canvasHeight);
                    updates.panDeltaY += 0.5 * camera.ortho.scale * (yDelta / canvasHeight);
                }

            } else if (rotating) {

                if (!configs.planView) { // No rotating in plan-view mode

                    if (configs.firstPerson) {
                        updates.rotateDeltaY -= (xDelta / canvasWidth) * configs.dragRotationRate / 2;
                        updates.rotateDeltaX += (yDelta / canvasHeight) * (configs.dragRotationRate / 4);

                    } else {
                        updates.rotateDeltaY -= (xDelta / canvasWidth) * (configs.dragRotationRate * 1.5);
                        updates.rotateDeltaX += (yDelta / canvasHeight) * (configs.dragRotationRate * 1.5);
                    }
                }
            }

            lastX = x;
            lastY = y;
        });

        canvas.addEventListener("mousemove", this._canvasMouseMoveHandler = (e) => {

            if (!(configs.active && configs.pointerEnabled)) {
                return;
            }

            if (!states.mouseover) {
                return;
            }

            mouseMovedOnCanvasSinceLastWheel = true;
        });

        document.addEventListener("mouseup", this._documentMouseUpHandler = (e) => {
            if (!(configs.active && configs.pointerEnabled)) {
                return;
            }
            switch (e.which) {
                case 1: // Left button
                    mouseDownLeft = false;
                    mouseDownMiddle = false;
                    mouseDownRight = false;
                    keyDown[scene.input.MOUSE_LEFT_BUTTON] = false;
                    keyDown[scene.input.MOUSE_MIDDLE_BUTTON] = false;
                    keyDown[scene.input.MOUSE_RIGHT_BUTTON] = false;
                    break;
                case 2: // Middle/both buttons
                    mouseDownLeft = false;
                    mouseDownMiddle = false;
                    mouseDownRight = false;
                    keyDown[scene.input.MOUSE_LEFT_BUTTON] = false;
                    keyDown[scene.input.MOUSE_MIDDLE_BUTTON] = false;
                    keyDown[scene.input.MOUSE_RIGHT_BUTTON] = false;
                    break;
                case 3: // Right button
                    mouseDownLeft = false;
                    mouseDownMiddle = false;
                    mouseDownRight = false;
                    keyDown[scene.input.MOUSE_LEFT_BUTTON] = false;
                    keyDown[scene.input.MOUSE_MIDDLE_BUTTON] = false;
                    keyDown[scene.input.MOUSE_RIGHT_BUTTON] = false;
                    break;
            }
        });

        canvas.addEventListener("mouseup", this._mouseUpHandler = (e) => {
            if (!(configs.active && configs.pointerEnabled)) {
                return;
            }
            switch (e.which) {
                case 3: // Right button
                    getCanvasPosFromEvent$3(e, canvasPos);
                    const x = canvasPos[0];
                    const y = canvasPos[1];
                    if (Math.abs(x - lastXDown) < 3 && Math.abs(y - lastYDown) < 3) {
                        controllers.cameraControl.fire("rightClick", { // For context menus
                            pagePos: [Math.round(e.pageX), Math.round(e.pageY)],
                            canvasPos: canvasPos,
                            event: e
                        }, true);
                    }
                    break;
            }
            canvas.style.removeProperty("cursor");
        });

        canvas.addEventListener("mouseenter", this._mouseEnterHandler = () => {
            if (!(configs.active && configs.pointerEnabled)) {
                return;
            }
        });

        const maxElapsed = 1 / 20;
        const minElapsed = 1 / 60;

        let secsNowLast = null;

        canvas.addEventListener("wheel", this._mouseWheelHandler = (e) => {
            if (!(configs.active && configs.pointerEnabled && configs.zoomOnMouseWheel && cameraControl._isKeyDownForAction(cameraControl.MOUSE_DOLLY, keyDown))) {
                return;
            }
            const secsNow = performance.now() / 1000.0;
            var secsElapsed = (secsNowLast !== null) ? (secsNow - secsNowLast) : 0;
            secsNowLast = secsNow;
            if (secsElapsed > maxElapsed) {
                secsElapsed = maxElapsed;
            }
            if (secsElapsed < minElapsed) {
                secsElapsed = minElapsed;
            }
            const delta = Math.max(-1, Math.min(1, -e.deltaY * 40));
            if (delta === 0) {
                return;
            }
            const normalizedDelta = delta / Math.abs(delta);
            updates.dollyDelta += -normalizedDelta * secsElapsed * configs.mouseWheelDollyRate;

            if (mouseMovedOnCanvasSinceLastWheel) {
                states.followPointerDirty = true;
                mouseMovedOnCanvasSinceLastWheel = false;
            }

        }, {passive: true});
    }

    reset() {
    }

    destroy() {

        const canvas = this._scene.canvas.canvas;

        document.removeEventListener("keydown", this._documentKeyDownHandler);
        document.removeEventListener("keyup", this._documentKeyUpHandler);
        document.removeEventListener("visibilitychange", this._onVisibilityChange);
        window.removeEventListener("blur", this._onBlur);
        canvas.removeEventListener("mousedown", this._mouseDownHandler);
        document.removeEventListener("mousemove", this._documentMouseMoveHandler);
        canvas.removeEventListener("mousemove", this._canvasMouseMoveHandler);
        document.removeEventListener("mouseup", this._documentMouseUpHandler);
        canvas.removeEventListener("mouseup", this._mouseUpHandler);
        canvas.removeEventListener("mouseenter", this._mouseEnterHandler);
        canvas.removeEventListener("wheel", this._mouseWheelHandler);
    }
}

const center = math.vec3();
const tempVec3a$7 = math.vec3();
const tempVec3b$4 = math.vec3();
const tempVec3c$3 = math.vec3();
const tempVec3d = math.vec3();

const tempCameraTarget = {
    eye: math.vec3(),
    look: math.vec3(),
    up: math.vec3()
};

/**
 * @private
 */
class KeyboardAxisViewHandler {

    constructor(scene, controllers, configs, states) {

        this._scene = scene;
        const cameraControl = controllers.cameraControl;
        const camera = scene.camera;

        this._onSceneKeyDown = scene.input.on("keydown", () => {

            if (!(configs.active && configs.pointerEnabled) || (!scene.input.keyboardEnabled)) {
                return;
            }

            if (configs.keyboardEnabledOnlyIfMouseover && !states.mouseover) {
                return;
            }

            const axisViewRight = cameraControl._isKeyDownForAction(cameraControl.AXIS_VIEW_RIGHT);
            const axisViewBack = cameraControl._isKeyDownForAction(cameraControl.AXIS_VIEW_BACK);
            const axisViewLeft = cameraControl._isKeyDownForAction(cameraControl.AXIS_VIEW_LEFT);
            const axisViewFront = cameraControl._isKeyDownForAction(cameraControl.AXIS_VIEW_FRONT);
            const axisViewTop = cameraControl._isKeyDownForAction(cameraControl.AXIS_VIEW_TOP);
            const axisViewBottom = cameraControl._isKeyDownForAction(cameraControl.AXIS_VIEW_BOTTOM);

            if ((!axisViewRight) && (!axisViewBack) && (!axisViewLeft) && (!axisViewFront) && (!axisViewTop) && (!axisViewBottom)) {
                return;
            }

            const aabb = scene.aabb;
            const diag = math.getAABB3Diag(aabb);

            math.getAABB3Center(aabb, center);

            const perspectiveDist = Math.abs(diag / Math.tan(controllers.cameraFlight.fitFOV * math.DEGTORAD));
            const orthoScale = diag * 1.1;

            tempCameraTarget.orthoScale = orthoScale;

            if (axisViewRight) {

                tempCameraTarget.eye.set(math.addVec3(center, math.mulVec3Scalar(camera.worldRight, perspectiveDist, tempVec3a$7), tempVec3d));
                tempCameraTarget.look.set(center);
                tempCameraTarget.up.set(camera.worldUp);

            } else if (axisViewBack) {

                tempCameraTarget.eye.set(math.addVec3(center, math.mulVec3Scalar(camera.worldForward, perspectiveDist, tempVec3a$7), tempVec3d));
                tempCameraTarget.look.set(center);
                tempCameraTarget.up.set(camera.worldUp);

            } else if (axisViewLeft) {

                tempCameraTarget.eye.set(math.addVec3(center, math.mulVec3Scalar(camera.worldRight, -perspectiveDist, tempVec3a$7), tempVec3d));
                tempCameraTarget.look.set(center);
                tempCameraTarget.up.set(camera.worldUp);

            } else if (axisViewFront) {

                tempCameraTarget.eye.set(math.addVec3(center, math.mulVec3Scalar(camera.worldForward, -perspectiveDist, tempVec3a$7), tempVec3d));
                tempCameraTarget.look.set(center);
                tempCameraTarget.up.set(camera.worldUp);

            } else if (axisViewTop) {

                tempCameraTarget.eye.set(math.addVec3(center, math.mulVec3Scalar(camera.worldUp, perspectiveDist, tempVec3a$7), tempVec3d));
                tempCameraTarget.look.set(center);
                tempCameraTarget.up.set(math.normalizeVec3(math.mulVec3Scalar(camera.worldForward, 1, tempVec3b$4), tempVec3c$3));

            } else if (axisViewBottom) {

                tempCameraTarget.eye.set(math.addVec3(center, math.mulVec3Scalar(camera.worldUp, -perspectiveDist, tempVec3a$7), tempVec3d));
                tempCameraTarget.look.set(center);
                tempCameraTarget.up.set(math.normalizeVec3(math.mulVec3Scalar(camera.worldForward, -1, tempVec3b$4)));
            }

            if ((!configs.firstPerson) && configs.followPointer) {
                controllers.pivotController.setPivotPos(center);
            }

            if (controllers.cameraFlight.duration > 0) {
                controllers.cameraFlight.flyTo(tempCameraTarget, () => {
                    if (controllers.pivotController.getPivoting() && configs.followPointer) {
                        controllers.pivotController.showPivot();
                    }
                });

            } else {
                controllers.cameraFlight.jumpTo(tempCameraTarget);
                if (controllers.pivotController.getPivoting() && configs.followPointer) {
                    controllers.pivotController.showPivot();
                }
            }
        });
    }

    reset() {
    }

    destroy() {
        this._scene.input.off(this._onSceneKeyDown);
    }
}

/**
 * @private
 */
class MousePickHandler {

    constructor(scene, controllers, configs, states, updates) {

        this._scene = scene;

        const pickController = controllers.pickController;
        const pivotController = controllers.pivotController;
        const cameraControl = controllers.cameraControl;

        this._clicks = 0;
        this._timeout = null;
        this._lastPickedEntityId = null;
        this._lastClickedWorldPos = null;

        let leftDown = false;
        let rightDown = false;

        const canvas = this._scene.canvas.canvas;

        const flyCameraTo = (pickResult) => {
            let pos;
            if (pickResult && pickResult.worldPos) {
                pos = pickResult.worldPos;
            }
            const aabb = pickResult && pickResult.entity ? pickResult.entity.aabb : scene.aabb;
            if (pos) { // Fly to look at point, don't change eye->look dist
                const camera = scene.camera;
                math.subVec3(camera.eye, camera.look, []);
                controllers.cameraFlight.flyTo({
                    // look: pos,
                    // eye: xeokit.math.addVec3(pos, diff, []),
                    // up: camera.up,
                    aabb: aabb
                });
                // TODO: Option to back off to fit AABB in view
            } else {// Fly to fit target boundary in view
                controllers.cameraFlight.flyTo({
                    aabb: aabb
                });
            }
        };

        const tickifiedMouseMoveFn = scene.tickify (
            this._canvasMouseMoveHandler = (e) => {
                if (!(configs.active && configs.pointerEnabled)) {
                    return;
                }

                if (leftDown || rightDown) {
                    return;
                }

                if (cameraControl.hasSubs("rayMove"))
                {
                    const origin = math.vec3();
                    const direction = math.vec3();
                    math.canvasPosToWorldRay(scene.canvas.canvas, scene.camera.viewMatrix, scene.camera.projMatrix, scene.camera.projection, states.pointerCanvasPos, origin, direction);
                    cameraControl.fire("rayMove", { canvasPos: states.pointerCanvasPos, ray: { origin: origin, direction: direction, canvasPos: states.pointerCanvasPos } }, true);
                }

                const hoverSubs = cameraControl.hasSubs("hover");
                const hoverEnterSubs = cameraControl.hasSubs("hoverEnter");
                const hoverOutSubs = cameraControl.hasSubs("hoverOut");
                const hoverOffSubs = cameraControl.hasSubs("hoverOff");
                const hoverSurfaceSubs = cameraControl.hasSubs("hoverSurface");
                const hoverSnapOrSurfaceSubs = cameraControl.hasSubs("hoverSnapOrSurface");

                if (hoverSubs || hoverEnterSubs || hoverOutSubs || hoverOffSubs || hoverSurfaceSubs || hoverSnapOrSurfaceSubs) {

                    pickController.pickCursorPos = states.pointerCanvasPos;
                    pickController.schedulePickEntity = true;
                    pickController.schedulePickSurface = hoverSurfaceSubs;
                    pickController.scheduleSnapOrPick = hoverSnapOrSurfaceSubs;

                    pickController.update();

                    if (pickController.pickResult) {

                        if (pickController.pickResult.entity) {
                            const pickedEntityId = pickController.pickResult.entity.id;

                            if (this._lastPickedEntityId !== pickedEntityId) {

                                if (this._lastPickedEntityId !== undefined) {

                                    cameraControl.fire("hoverOut", { // Hovered off an entity
                                        entity: scene.objects[this._lastPickedEntityId]
                                    }, true);
                                }

                                cameraControl.fire("hoverEnter", pickController.pickResult, true); // Hovering over a new entity

                                this._lastPickedEntityId = pickedEntityId;
                            }
                        }

                        cameraControl.fire("hover", pickController.pickResult, true);

                        if (pickController.pickResult.worldPos || pickController.pickResult.snappedWorldPos) { // Hovering the surface of an entity
                            cameraControl.fire("hoverSurface", pickController.pickResult, true);
                        }

                    } else {

                        if (this._lastPickedEntityId !== undefined) {

                            cameraControl.fire("hoverOut", { // Hovered off an entity
                                entity: scene.objects[this._lastPickedEntityId]
                            }, true);

                            this._lastPickedEntityId = undefined;
                        }

                        cameraControl.fire("hoverOff", { // Not hovering on any entity
                            canvasPos: pickController.pickCursorPos
                        }, true);
                    }
                }
            }
        );

        canvas.addEventListener("mousemove", tickifiedMouseMoveFn);

        canvas.addEventListener('mousedown', this._canvasMouseDownHandler = (e) => {

            if (e.which === 1) {
                leftDown = true;
            }

            if (e.which === 3) {
                rightDown = true;
            }

            const leftButtonDown = (e.which === 1);

            if (!leftButtonDown) {
                return;
            }

            if (!(configs.active && configs.pointerEnabled)) {
                return;
            }

            // Left mouse button down to start pivoting

            states.mouseDownClientX = e.clientX;
            states.mouseDownClientY = e.clientY;
            states.mouseDownCursorX = states.pointerCanvasPos[0];
            states.mouseDownCursorY = states.pointerCanvasPos[1];

            if ((!configs.firstPerson) && configs.followPointer) {

                pickController.pickCursorPos = states.pointerCanvasPos;
                pickController.schedulePickSurface = true;

                pickController.update();

                if (e.which === 1) {// Left button
                    const pickResult = pickController.pickResult;
                    if (pickResult && pickResult.worldPos) {
                        pivotController.setPivotPos(pickResult.worldPos);
                        pivotController.startPivot();
                        this._lastClickedWorldPos = pickResult.worldPos.slice();
                    } else {
                        if (configs.smartPivot) {
                            pivotController.setCanvasPivotPos(states.pointerCanvasPos);
                        } else {
                            if (this._lastClickedWorldPos) {
                                pivotController.setPivotPos(this._lastClickedWorldPos);
                            } else {
                                pivotController.setPivotPos(scene.camera.look);
                            }
                        }
                        pivotController.startPivot();
                    }
                }
            }
        });

        document.addEventListener('mouseup', this._documentMouseUpHandler = (e) => {

            if (e.which === 1) {
                leftDown = false;
            }

            if (e.which === 3) {
                rightDown = false;
            }

            if (pivotController.getPivoting()) {
                pivotController.endPivot();
            }
        });

        canvas.addEventListener('mouseup', this._canvasMouseUpHandler = (e) => {

            if (!(configs.active && configs.pointerEnabled)) {
                return;
            }

            const leftButtonUp = (e.which === 1);

            if (!leftButtonUp) {
                return;
            }

            // Left mouse button up to possibly pick or double-pick

            pivotController.hidePivot();

            if (Math.abs(e.clientX - states.mouseDownClientX) > 3 || Math.abs(e.clientY - states.mouseDownClientY) > 3) {
                return;
            }

            const pickedSubs = cameraControl.hasSubs("picked");
            const pickedNothingSubs = cameraControl.hasSubs("pickedNothing");
            const pickedSurfaceSubs = cameraControl.hasSubs("pickedSurface");
            const doublePickedSubs = cameraControl.hasSubs("doublePicked");
            const doublePickedSurfaceSubs = cameraControl.hasSubs("doublePickedSurface");
            const doublePickedNothingSubs = cameraControl.hasSubs("doublePickedNothing");

            if ((!configs.doublePickFlyTo) &&
                (!doublePickedSubs) &&
                (!doublePickedSurfaceSubs) &&
                (!doublePickedNothingSubs)) {

                //  Avoid the single/double click differentiation timeout

                if (pickedSubs || pickedNothingSubs || pickedSurfaceSubs) {

                    pickController.pickCursorPos = states.pointerCanvasPos;
                    pickController.schedulePickEntity = true;
                    pickController.schedulePickSurface = pickedSurfaceSubs;
                    pickController.update();

                    if (pickController.pickResult) {

                        cameraControl.fire("picked", pickController.pickResult, true);

                        if (pickController.pickedSurface) {
                            cameraControl.fire("pickedSurface", pickController.pickResult, true);
                        }
                    } else {
                        cameraControl.fire("pickedNothing", {
                            canvasPos: states.pointerCanvasPos
                        }, true);
                    }
                }

                this._clicks = 0;

                return;
            }

            this._clicks++;

            if (this._clicks === 1) { // First click

                pickController.pickCursorPos = states.pointerCanvasPos;
                pickController.schedulePickEntity = configs.doublePickFlyTo;
                pickController.schedulePickSurface = pickedSurfaceSubs;
                pickController.update();

                const firstClickPickResult = pickController.pickResult;
                const firstClickPickSurface = pickController.pickedSurface;

                this._timeout = setTimeout(() => {

                    if (firstClickPickResult) {

                        cameraControl.fire("picked", firstClickPickResult, true);

                        if (firstClickPickSurface) {

                            cameraControl.fire("pickedSurface", firstClickPickResult, true);

                            if ((!configs.firstPerson) && configs.followPointer) {
                                controllers.pivotController.setPivotPos(firstClickPickResult.worldPos);
                                if (controllers.pivotController.startPivot()) {
                                    controllers.pivotController.showPivot();
                                }
                            }
                        }
                    } else {
                        cameraControl.fire("pickedNothing", {
                            canvasPos: states.pointerCanvasPos
                        }, true);
                    }

                    this._clicks = 0;

                }, configs.doubleClickTimeFrame);

            } else { // Second click

                if (this._timeout !== null) {
                    window.clearTimeout(this._timeout);
                    this._timeout = null;
                }

                pickController.pickCursorPos = states.pointerCanvasPos;
                pickController.schedulePickEntity = configs.doublePickFlyTo || doublePickedSubs || doublePickedSurfaceSubs;
                pickController.schedulePickSurface = pickController.schedulePickEntity && doublePickedSurfaceSubs;
                pickController.update();

                if (pickController.pickResult) {

                    cameraControl.fire("doublePicked", pickController.pickResult, true);

                    if (pickController.pickedSurface) {
                        cameraControl.fire("doublePickedSurface", pickController.pickResult, true);
                    }

                    if (configs.doublePickFlyTo) {

                        flyCameraTo(pickController.pickResult);

                        if ((!configs.firstPerson) && configs.followPointer) {

                            const pickedEntityAABB = pickController.pickResult.entity.aabb;
                            const pickedEntityCenterPos = math.getAABB3Center(pickedEntityAABB);

                            controllers.pivotController.setPivotPos(pickedEntityCenterPos);

                            if (controllers.pivotController.startPivot()) {
                                controllers.pivotController.showPivot();
                            }
                        }
                    }

                } else {

                    cameraControl.fire("doublePickedNothing", {
                        canvasPos: states.pointerCanvasPos
                    }, true);

                    if (configs.doublePickFlyTo) {

                        flyCameraTo();

                        if ((!configs.firstPerson) && configs.followPointer) {

                            const sceneAABB = scene.aabb;
                            const sceneCenterPos = math.getAABB3Center(sceneAABB);

                            controllers.pivotController.setPivotPos(sceneCenterPos);

                            if (controllers.pivotController.startPivot()) {
                                controllers.pivotController.showPivot();
                            }
                        }
                    }
                }

                this._clicks = 0;
            }
        }, false);
    }

    reset() {
        this._clicks = 0;
        this._lastPickedEntityId = null;
        if (this._timeout) {
            window.clearTimeout(this._timeout);
            this._timeout = null;
        }
    }

    destroy() {
        const canvas = this._scene.canvas.canvas;
        canvas.removeEventListener("mousemove", this._canvasMouseMoveHandler);
        canvas.removeEventListener("mousedown", this._canvasMouseDownHandler);
        document.removeEventListener("mouseup", this._documentMouseUpHandler);
        canvas.removeEventListener("mouseup", this._canvasMouseUpHandler);
        if (this._timeout) {
            window.clearTimeout(this._timeout);
            this._timeout = null;
        }
    }
}

/**
 * @private
 */
class KeyboardPanRotateDollyHandler {

    constructor(scene, controllers, configs, states, updates) {

        this._scene = scene;
        const input = scene.input;

        const keyDownMap = [];

        scene.canvas.canvas;

        let mouseMovedSinceLastKeyboardDolly = true;

        this._onSceneMouseMove = input.on("mousemove", () => {
            mouseMovedSinceLastKeyboardDolly = true;
        });

        this._onSceneKeyDown = input.on("keydown", (keyCode) => {
            if (!(configs.active && configs.pointerEnabled) || (!scene.input.keyboardEnabled)) {
                return;
            }
            if (configs.keyboardEnabledOnlyIfMouseover && !states.mouseover) {
                return;
            }
            keyDownMap[keyCode] = true;
        });

        this._onSceneKeyUp = input.on("keyup", (keyCode) => {
            if (!(configs.active && configs.pointerEnabled) || (!scene.input.keyboardEnabled)) {
                return;
            }
            keyDownMap[keyCode] = false;

            if (controllers.pivotController.getPivoting()) {
                controllers.pivotController.endPivot();
            }
        });

        document.addEventListener("visibilitychange", this._onVisibilityChange = () => {
            keyDownMap.splice(0);
        });

        window.addEventListener("blur", this._onBlur = () => {
            keyDownMap.splice(0);
        });

        this._onTick = scene.on("tick", (e) => {

            if (!(configs.active && configs.pointerEnabled) || (!scene.input.keyboardEnabled)) {
                return;
            }

            if (configs.keyboardEnabledOnlyIfMouseover &&  !states.mouseover) {
                return;
            }

            const cameraControl = controllers.cameraControl;
            const elapsedSecs = (e.deltaTime / 1000.0);

            //-------------------------------------------------------------------------------------------------
            // Keyboard rotation
            //-------------------------------------------------------------------------------------------------

            if (!configs.planView) {

                const rotateYPos = cameraControl._isKeyDownForAction(cameraControl.ROTATE_Y_POS, keyDownMap);
                const rotateYNeg = cameraControl._isKeyDownForAction(cameraControl.ROTATE_Y_NEG, keyDownMap);
                const rotateXPos = cameraControl._isKeyDownForAction(cameraControl.ROTATE_X_POS, keyDownMap);
                const rotateXNeg = cameraControl._isKeyDownForAction(cameraControl.ROTATE_X_NEG, keyDownMap);

                const orbitDelta = elapsedSecs * configs.keyboardRotationRate;

                if (rotateYPos || rotateYNeg || rotateXPos || rotateXNeg) {

                    if ((!configs.firstPerson) && configs.followPointer) {
                        controllers.pivotController.startPivot();
                    }

                    if (rotateYPos) {
                        updates.rotateDeltaY += orbitDelta;

                    } else if (rotateYNeg) {
                        updates.rotateDeltaY -= orbitDelta;
                    }

                    if (rotateXPos) {
                        updates.rotateDeltaX += orbitDelta;

                    } else if (rotateXNeg) {
                        updates.rotateDeltaX -= orbitDelta;
                    }

                    if ((!configs.firstPerson) && configs.followPointer) {
                        controllers.pivotController.startPivot();
                    }
                }
            }

            //-------------------------------------------------------------------------------------------------
            // Keyboard panning
            //-------------------------------------------------------------------------------------------------

            if (!keyDownMap[input.KEY_CTRL] && !keyDownMap[input.KEY_ALT]) {

                const dollyBackwards = cameraControl._isKeyDownForAction(cameraControl.DOLLY_BACKWARDS, keyDownMap);
                const dollyForwards = cameraControl._isKeyDownForAction(cameraControl.DOLLY_FORWARDS, keyDownMap);

                if (dollyBackwards || dollyForwards) {

                    const dollyDelta = elapsedSecs * configs.keyboardDollyRate;

                    if ((!configs.firstPerson) && configs.followPointer) {
                        controllers.pivotController.startPivot();
                    }
                    if (dollyForwards) {
                        updates.dollyDelta -= dollyDelta;
                    } else if (dollyBackwards) {
                        updates.dollyDelta += dollyDelta;
                    }

                    if (mouseMovedSinceLastKeyboardDolly) {
                        states.followPointerDirty = true;
                        mouseMovedSinceLastKeyboardDolly = false;
                    }
                }
            }

            const panForwards = cameraControl._isKeyDownForAction(cameraControl.PAN_FORWARDS, keyDownMap);
            const panBackwards = cameraControl._isKeyDownForAction(cameraControl.PAN_BACKWARDS, keyDownMap);
            const panLeft = cameraControl._isKeyDownForAction(cameraControl.PAN_LEFT, keyDownMap);
            const panRight = cameraControl._isKeyDownForAction(cameraControl.PAN_RIGHT, keyDownMap);
            const panUp = cameraControl._isKeyDownForAction(cameraControl.PAN_UP, keyDownMap);
            const panDown = cameraControl._isKeyDownForAction(cameraControl.PAN_DOWN, keyDownMap);

            const panDelta = (keyDownMap[input.KEY_ALT] ? 0.3 : 1.0) * elapsedSecs * configs.keyboardPanRate; // ALT for slower pan rate

            if (panForwards || panBackwards || panLeft || panRight || panUp || panDown) {

                if ((!configs.firstPerson) && configs.followPointer) {
                    controllers.pivotController.startPivot();
                }

                if (panDown) {
                    updates.panDeltaY += panDelta;

                } else if (panUp) {
                    updates.panDeltaY += -panDelta;
                }

                if (panRight) {
                    updates.panDeltaX += -panDelta;

                } else if (panLeft) {
                    updates.panDeltaX += panDelta;
                }

                if (panBackwards) {
                    updates.panDeltaZ += panDelta;

                } else if (panForwards) {
                    updates.panDeltaZ += -panDelta;
                }
            }
        });
    }

    reset() {
    }

    destroy() {

        this._scene.off(this._onTick);

        this._scene.input.off(this._onSceneMouseMove);
        this._scene.input.off(this._onSceneKeyDown);
        document.removeEventListener("visibilitychange", this._onVisibilityChange);
        window.removeEventListener("blur", this._onBlur);
        this._scene.input.off(this._onSceneKeyUp);
    }
}

const SCALE_DOLLY_EACH_FRAME = 1; // Recalculate dolly speed for eye->target distance on each Nth frame
const EPSILON = 0.001;
const tempVec3$3$1 = math.vec3();

/**
 * Handles camera updates on each "tick" that were scheduled by the various controllers.
 *
 * @private
 */
class CameraUpdater {

    constructor(scene, controllers, configs, states, updates) {

        this._scene = scene;
        const camera = scene.camera;
        const pickController = controllers.pickController;
        const pivotController = controllers.pivotController;
        const panController = controllers.panController;
        const cameraControl = controllers.cameraControl;

        let countDown = SCALE_DOLLY_EACH_FRAME; // Decrements on each tick
        let dollyDistFactor = 1.0; // Calculated when countDown is zero
        let followPointerWorldPos = null; // Holds the pointer's World position when configs.followPointer is true
        
        this._onTick = scene.on("tick", () => {

            if (!(configs.active && configs.pointerEnabled)) {
                return;
            }

            let cursorType = "default";

            //----------------------------------------------------------------------------------------------------------
            // Dolly decay
            //------------------------------------------------------------------------------------  ----------------------

            if (Math.abs(updates.dollyDelta) < EPSILON) {
                updates.dollyDelta = 0;
            }

            //----------------------------------------------------------------------------------------------------------
            // Rotation decay
            //----------------------------------------------------------------------------------------------------------

            if (Math.abs(updates.rotateDeltaX) < EPSILON) {
                updates.rotateDeltaX = 0;
            }

            if (Math.abs(updates.rotateDeltaY) < EPSILON) {
                updates.rotateDeltaY = 0;
            }

            if (updates.rotateDeltaX !== 0 || updates.rotateDeltaY !== 0) {
                updates.dollyDelta = 0;
            }

            //----------------------------------------------------------------------------------------------------------
            // Dolly speed eye->look scaling
            //
            // If pointer is over an object, then dolly speed is proportional to the distance to that object.
            //
            // If pointer is not over an object, then dolly speed is proportional to the distance to the last
            // object the pointer was over. This is so that we can dolly to structures that may have gaps through
            // which empty background shows, that the pointer may inadvertently be over. In these cases, we don't
            // want dolly speed wildly varying depending on how accurately the user avoids the gaps with the pointer.
            //----------------------------------------------------------------------------------------------------------

            if (configs.followPointer) {

                if (--countDown <= 0) {

                    countDown = SCALE_DOLLY_EACH_FRAME;

                    if (updates.dollyDelta !== 0) {
                        if (updates.rotateDeltaY === 0 && updates.rotateDeltaX === 0) {

                            if (configs.followPointer && states.followPointerDirty) {

                                pickController.pickCursorPos = states.pointerCanvasPos;
                                pickController.schedulePickSurface = true;
                                pickController.update();

                                if (pickController.pickResult && pickController.pickResult.worldPos) {
                                    followPointerWorldPos = pickController.pickResult.worldPos;
                                    
                                } else {
                                    dollyDistFactor = 1.0;
                                    followPointerWorldPos = null;
                                }

                                states.followPointerDirty = false;
                            }
                        }

                        if (followPointerWorldPos) {
                            const dist = Math.abs(math.lenVec3(math.subVec3(followPointerWorldPos, scene.camera.eye, tempVec3$3$1)));
                            dollyDistFactor = dist / configs.dollyProximityThreshold;
                        }

                        if (dollyDistFactor < configs.dollyMinSpeed) {
                            dollyDistFactor = configs.dollyMinSpeed;
                        }
                    }
                }
            } else {
              dollyDistFactor = 1;
              followPointerWorldPos = null;
            }

            const dollyDeltaForDist = (updates.dollyDelta * dollyDistFactor);

            //----------------------------------------------------------------------------------------------------------
            // Rotation
            //----------------------------------------------------------------------------------------------------------

            if (updates.rotateDeltaY !== 0 || updates.rotateDeltaX !== 0) {

                if ((!configs.firstPerson) && configs.followPointer && pivotController.getPivoting()) {
                    pivotController.continuePivot(updates.rotateDeltaY, updates.rotateDeltaX);
                    pivotController.showPivot();

                } else {

                    if (updates.rotateDeltaX !== 0) {
                        if (configs.firstPerson) {
                            camera.pitch(-updates.rotateDeltaX);
                        } else {
                            camera.orbitPitch(updates.rotateDeltaX);
                        }
                    }

                    if (updates.rotateDeltaY !== 0) {
                        if (configs.firstPerson) {
                            camera.yaw(updates.rotateDeltaY);
                        } else {
                            camera.orbitYaw(updates.rotateDeltaY);
                        }
                    }
                }

                updates.rotateDeltaX *= configs.rotationInertia;
                updates.rotateDeltaY *= configs.rotationInertia;

                cursorType = cameraControl._cursors.rotate;
            }

            //----------------------------------------------------------------------------------------------------------
            // Panning
            //----------------------------------------------------------------------------------------------------------

            if (Math.abs(updates.panDeltaX) < EPSILON) {
                updates.panDeltaX = 0;
            }

            if (Math.abs(updates.panDeltaY) < EPSILON) {
                updates.panDeltaY = 0;
            }

            if (Math.abs(updates.panDeltaZ) < EPSILON) {
                updates.panDeltaZ = 0;
            }

            if (updates.panDeltaX !== 0 || updates.panDeltaY !== 0 || updates.panDeltaZ !== 0) {

                const vec = math.vec3();

                vec[0] = updates.panDeltaX;
                vec[1] = updates.panDeltaY;
                vec[2] = updates.panDeltaZ;

                let verticalEye;
                let verticalLook;

                if (configs.constrainVertical) {

                    if (camera.xUp) {
                        verticalEye = camera.eye[0];
                        verticalLook = camera.look[0];
                    } else if (camera.yUp) {
                        verticalEye = camera.eye[1];
                        verticalLook = camera.look[1];
                    } else if (camera.zUp) {
                        verticalEye = camera.eye[2];
                        verticalLook = camera.look[2];
                    }

                    camera.pan(vec);

                    const eye = camera.eye;
                    const look = camera.look;

                    if (camera.xUp) {
                        eye[0] = verticalEye;
                        look[0] = verticalLook;
                    } else if (camera.yUp) {
                        eye[1] = verticalEye;
                        look[1] = verticalLook;
                    } else if (camera.zUp) {
                        eye[2] = verticalEye;
                        look[2] = verticalLook;
                    }

                    camera.eye = eye;
                    camera.look = look;

                } else {
                    camera.pan(vec);
                }

                cursorType = cameraControl._cursors.pan;
            }

            updates.panDeltaX *= configs.panInertia;
            updates.panDeltaY *= configs.panInertia;
            updates.panDeltaZ *= configs.panInertia;

            //----------------------------------------------------------------------------------------------------------
            // Dollying
            //----------------------------------------------------------------------------------------------------------

            if (dollyDeltaForDist !== 0) {

                if (dollyDeltaForDist < 0) {
                    cursorType = cameraControl._cursors.dollyForward;
                } else {
                    cursorType = cameraControl._cursors.dollyBackward;
                }

                if (configs.firstPerson) {

                    let verticalEye;
                    let verticalLook;

                    if (configs.constrainVertical) {
                        if (camera.xUp) {
                            verticalEye = camera.eye[0];
                            verticalLook = camera.look[0];
                        } else if (camera.yUp) {
                            verticalEye = camera.eye[1];
                            verticalLook = camera.look[1];
                        } else if (camera.zUp) {
                            verticalEye = camera.eye[2];
                            verticalLook = camera.look[2];
                        }
                    }

                    if (configs.followPointer) {
                        const dolliedThroughSurface = panController.dollyToCanvasPos(followPointerWorldPos, states.pointerCanvasPos, -dollyDeltaForDist);
                        if (dolliedThroughSurface) {
                            states.followPointerDirty = true;
                        }
                    } else {
                        camera.pan([0, 0, dollyDeltaForDist]);
                        camera.ortho.scale = camera.ortho.scale - dollyDeltaForDist;
                    }

                    if (configs.constrainVertical) {
                        const eye = camera.eye;
                        const look = camera.look;
                        if (camera.xUp) {
                            eye[0] = verticalEye;
                            look[0] = verticalLook;
                        } else if (camera.yUp) {
                            eye[1] = verticalEye;
                            look[1] = verticalLook;
                        } else if (camera.zUp) {
                            eye[2] = verticalEye;
                            look[2] = verticalLook;
                        }
                        camera.eye = eye;
                        camera.look = look;
                    }

                } else if (configs.planView) {

                    if (configs.followPointer) {
                        const dolliedThroughSurface = panController.dollyToCanvasPos(followPointerWorldPos, states.pointerCanvasPos, -dollyDeltaForDist);
                        if (dolliedThroughSurface) {
                            states.followPointerDirty = true;
                        }
                    } else {
                        camera.ortho.scale = camera.ortho.scale + dollyDeltaForDist;
                        camera.zoom(dollyDeltaForDist);
                    }

                } else { // Orbiting

                    if (configs.followPointer) {
                        const dolliedThroughSurface = panController.dollyToCanvasPos(followPointerWorldPos, states.pointerCanvasPos, -dollyDeltaForDist);
                        if (dolliedThroughSurface) {
                            states.followPointerDirty = true;
                        }
                    } else {
                        camera.ortho.scale = camera.ortho.scale + dollyDeltaForDist;
                        camera.zoom(dollyDeltaForDist);
                    }
                }

                updates.dollyDelta *= configs.dollyInertia;
            }

            pickController.fireEvents();

            document.body.style.cursor = cursorType;
        });
    }


    destroy() {
        this._scene.off(this._onTick);
    }
}

/**
 * @private
 */
class MouseMiscHandler {

    constructor(scene, controllers, configs, states, updates) {

        this._scene = scene;

        const canvas = this._scene.canvas.canvas;

        canvas.addEventListener("mouseenter", this._mouseEnterHandler = () => {
            states.mouseover = true;
        });

        canvas.addEventListener("mouseleave", this._mouseLeaveHandler = () => {
            states.mouseover = false;
            canvas.style.cursor = null;
        });

        document.addEventListener("mousemove", this._mouseMoveHandler = (e) => {
            getCanvasPosFromEvent$2(e, canvas, states.pointerCanvasPos);
        });

        canvas.addEventListener("mousedown", this._mouseDownHandler = (e) => {
            if (!(configs.active && configs.pointerEnabled)) {
                return;
            }
            getCanvasPosFromEvent$2(e, canvas, states.pointerCanvasPos);
            states.mouseover = true;
        });

        canvas.addEventListener("mouseup", this._mouseUpHandler = (e) => {
            if (!(configs.active && configs.pointerEnabled)) {
                return;
            }
        });
    }

    reset() {
    }

    destroy() {

        const canvas = this._scene.canvas.canvas;

        document.removeEventListener("mousemove", this._mouseMoveHandler);
        canvas.removeEventListener("mouseenter", this._mouseEnterHandler);
        canvas.removeEventListener("mouseleave", this._mouseLeaveHandler);
        canvas.removeEventListener("mousedown", this._mouseDownHandler);
        canvas.removeEventListener("mouseup", this._mouseUpHandler);
    }
}

function getCanvasPosFromEvent$2(event, canvas, canvasPos) {
    if (!event) {
        event = window.event;
        canvasPos[0] = event.x;
        canvasPos[1] = event.y;
    } else {
        const { left, top } = canvas.getBoundingClientRect();
        canvasPos[0] = event.clientX - left;
        canvasPos[1] = event.clientY - top;
    }
    return canvasPos;
}

const getCanvasPosFromEvent$1 = function (event, canvasPos) {
    if (!event) {
        event = window.event;
        canvasPos[0] = event.x;
        canvasPos[1] = event.y;
    } else {
        let element = event.target;
        let totalOffsetLeft = 0;
        let totalOffsetTop = 0;
        while (element.offsetParent) {
            totalOffsetLeft += element.offsetLeft;
            totalOffsetTop += element.offsetTop;
            element = element.offsetParent;
        }
        canvasPos[0] = event.pageX - totalOffsetLeft;
        canvasPos[1] = event.pageY - totalOffsetTop;
    }
    return canvasPos;
};

/**
 * @private
 */
class TouchPanRotateAndDollyHandler {

    constructor(scene, controllers, configs, states, updates) {

        this._scene = scene;

        const pickController = controllers.pickController;
        const pivotController = controllers.pivotController;

        const tapStartCanvasPos = math.vec2();
        const tapCanvasPos0 = math.vec2();
        const tapCanvasPos1 = math.vec2();
        const touch0Vec = math.vec2();

        const lastCanvasTouchPosList = [];
        const canvas = this._scene.canvas.canvas;

        let numTouches = 0;
        let waitForTick = false;

        this._onTick = scene.on("tick", () => {
            waitForTick = false;
        });

        canvas.addEventListener("touchstart", this._canvasTouchStartHandler = (event) => {

            if (!(configs.active && configs.pointerEnabled)) {
                return;
            }

            event.preventDefault();

            const touches = event.touches;
            const changedTouches = event.changedTouches;

            states.touchStartTime = Date.now();

            if (touches.length === 1 && changedTouches.length === 1) {

                getCanvasPosFromEvent$1(touches[0], tapStartCanvasPos);

                if (configs.followPointer) {

                    pickController.pickCursorPos = tapStartCanvasPos;
                    pickController.schedulePickSurface = true;
                    pickController.update();

                    if (!configs.planView) {

                        if (pickController.picked && pickController.pickedSurface && pickController.pickResult && pickController.pickResult.worldPos) {

                            pivotController.setPivotPos(pickController.pickResult.worldPos);

                            if (!configs.firstPerson && pivotController.startPivot()) {
                                pivotController.showPivot();
                            }

                        } else {

                            if (configs.smartPivot) {
                                pivotController.setCanvasPivotPos(states.pointerCanvasPos);
                            } else {
                                pivotController.setPivotPos(scene.camera.look);
                            }

                            if (!configs.firstPerson && pivotController.startPivot()) {
                                pivotController.showPivot();
                            }
                        }
                    }
                }

            }

            while (lastCanvasTouchPosList.length < touches.length) {
                lastCanvasTouchPosList.push(math.vec2());
            }

            for (let i = 0, len = touches.length; i < len; ++i) {
                getCanvasPosFromEvent$1(touches[i], lastCanvasTouchPosList[i]);
            }

            numTouches = touches.length;
        });

        canvas.addEventListener("touchend", this._canvasTouchEndHandler = () => {
            if (pivotController.getPivoting()) {
                pivotController.endPivot();
                pivotController.hidePivot();
            }
        });

        canvas.addEventListener("touchmove", this._canvasTouchMoveHandler = (event) => {

            if (!(configs.active && configs.pointerEnabled)) {
                return;
            }

            event.stopPropagation();
            event.preventDefault();

            if (waitForTick) {
                // Limit changes detection to one per frame
                return;
            }

            waitForTick = true;

            // Scaling drag-rotate to canvas boundary

            const canvasBoundary = scene.canvas.boundary;
            const canvasWidth = canvasBoundary[2];
            const canvasHeight = canvasBoundary[3];

            const touches = event.touches;

            if (event.touches.length !== numTouches) {
                // Two fingers were pressed, then one of them is removed
                // We don't want to rotate in this case (weird behavior)
                return;
            }

            if (numTouches === 1) {

                getCanvasPosFromEvent$1(touches[0], tapCanvasPos0);

                //-----------------------------------------------------------------------------------------------
                // Drag rotation
                //-----------------------------------------------------------------------------------------------

                math.subVec2(tapCanvasPos0, lastCanvasTouchPosList[0], touch0Vec);

                const xPanDelta = touch0Vec[0];
                const yPanDelta = touch0Vec[1];

                if (states.longTouchTimeout !== null && (Math.abs(xPanDelta) > configs.longTapRadius || Math.abs(yPanDelta) > configs.longTapRadius)) {
                    clearTimeout(states.longTouchTimeout);
                    states.longTouchTimeout = null;
                }

                if (configs.planView) { // No rotating in plan-view mode

                    const camera = scene.camera;

                    // We use only canvasHeight here so that aspect ratio does not distort speed

                    if (camera.projection === "perspective") {

                        const depth = Math.abs(scene.camera.eyeLookDist);
                        const targetDistance = depth * Math.tan((camera.perspective.fov / 2) * Math.PI / 180.0);

                        updates.panDeltaX += (xPanDelta * targetDistance / canvasHeight) * configs.touchPanRate;
                        updates.panDeltaY += (yPanDelta * targetDistance / canvasHeight) * configs.touchPanRate;

                    } else {

                        updates.panDeltaX += 0.5 * camera.ortho.scale * (xPanDelta / canvasHeight) * configs.touchPanRate;
                        updates.panDeltaY += 0.5 * camera.ortho.scale * (yPanDelta / canvasHeight) * configs.touchPanRate;
                    }

                } else {
                  //  if (!absorbTinyFirstDrag) {
                        updates.rotateDeltaY -= (xPanDelta / canvasWidth) * (configs.dragRotationRate * 1.0); // Full horizontal rotation
                        updates.rotateDeltaX += (yPanDelta / canvasHeight) * (configs.dragRotationRate * 1.5); // Half vertical rotation
                    // } else {
                    //     firstDragDeltaY -= (xPanDelta / canvasWidth) * (configs.dragRotationRate * 1.0); // Full horizontal rotation
                    //     firstDragDeltaX += (yPanDelta / canvasHeight) * (configs.dragRotationRate * 1.5); // Half vertical rotation
                    //     if (Math.abs(firstDragDeltaX) > 5 || Math.abs(firstDragDeltaY) > 5) {
                    //         updates.rotateDeltaX += firstDragDeltaX;
                    //         updates.rotateDeltaY += firstDragDeltaY;
                    //         firstDragDeltaX = 0;
                    //         firstDragDeltaY = 0;
                    //         absorbTinyFirstDrag = false;
                    //     }
                    // }
                }

            } else if (numTouches === 2) {

                const touch0 = touches[0];
                const touch1 = touches[1];

                getCanvasPosFromEvent$1(touch0, tapCanvasPos0);
                getCanvasPosFromEvent$1(touch1, tapCanvasPos1);

                const lastMiddleTouch = math.geometricMeanVec2(lastCanvasTouchPosList[0], lastCanvasTouchPosList[1]);
                const currentMiddleTouch = math.geometricMeanVec2(tapCanvasPos0, tapCanvasPos1);

                const touchDelta = math.vec2();

                math.subVec2(lastMiddleTouch, currentMiddleTouch, touchDelta);

                const xPanDelta = touchDelta[0];
                const yPanDelta = touchDelta[1];

                const camera = scene.camera;

                // Dollying

                const d1 = math.distVec2([touch0.pageX, touch0.pageY], [touch1.pageX, touch1.pageY]);
                const d2 = math.distVec2(lastCanvasTouchPosList[0], lastCanvasTouchPosList[1]);

                const dollyDelta = (d2 - d1) * configs.touchDollyRate;

                updates.dollyDelta = dollyDelta;

                if (Math.abs(dollyDelta) < 1.0) {

                    // We use only canvasHeight here so that aspect ratio does not distort speed

                    if (camera.projection === "perspective") {
                        const pickedWorldPos = pickController.pickResult ? pickController.pickResult.worldPos : scene.center;

                        const depth = Math.abs(math.lenVec3(math.subVec3(pickedWorldPos, scene.camera.eye, [])));
                        const targetDistance = depth * Math.tan((camera.perspective.fov / 2) * Math.PI / 180.0);

                        updates.panDeltaX -= (xPanDelta * targetDistance / canvasHeight) * configs.touchPanRate;
                        updates.panDeltaY -= (yPanDelta * targetDistance / canvasHeight) * configs.touchPanRate;

                    } else {

                        updates.panDeltaX -= 0.5 * camera.ortho.scale * (xPanDelta / canvasHeight) * configs.touchPanRate;
                        updates.panDeltaY -= 0.5 * camera.ortho.scale * (yPanDelta / canvasHeight) * configs.touchPanRate;
                    }
                }


                states.pointerCanvasPos = currentMiddleTouch;
            }

            for (let i = 0; i < numTouches; ++i) {
                getCanvasPosFromEvent$1(touches[i], lastCanvasTouchPosList[i]);
            }
        });
    }

    reset() {
    }

    destroy() {
        const canvas = this._scene.canvas.canvas;
        canvas.removeEventListener("touchstart", this._canvasTouchStartHandler);
        canvas.removeEventListener("touchend", this._canvasTouchEndHandler);
        canvas.removeEventListener("touchmove", this._canvasTouchMoveHandler);
        this._scene.off(this._onTick);
    }
}

const TAP_INTERVAL = 150;
const DBL_TAP_INTERVAL = 325;
const TAP_DISTANCE_THRESHOLD = 4;

const getCanvasPosFromEvent = function (event, canvasPos) {
    if (!event) {
        event = window.event;
        canvasPos[0] = event.x;
        canvasPos[1] = event.y;
    } else {
        let element = event.target;
        let totalOffsetLeft = 0;
        let totalOffsetTop = 0;
        while (element.offsetParent) {
            totalOffsetLeft += element.offsetLeft;
            totalOffsetTop += element.offsetTop;
            element = element.offsetParent;
        }
        canvasPos[0] = event.pageX - totalOffsetLeft;
        canvasPos[1] = event.pageY - totalOffsetTop;
    }
    return canvasPos;
};

/**
 * @private
 */
class TouchPickHandler {

    constructor(scene, controllers, configs, states, updates) {

        this._scene = scene;

        const pickController = controllers.pickController;
        const cameraControl = controllers.cameraControl;

        let touchStartTime;
        const activeTouches = [];
        const tapStartPos = new Float32Array(2);
        let tapStartTime = -1;
        let lastTapTime = -1;

        const canvas = this._scene.canvas.canvas;

        const flyCameraTo = (pickResult) => {
            let pos;
            if (pickResult && pickResult.worldPos) {
                pos = pickResult.worldPos;
            }
            const aabb = pickResult ? pickResult.entity.aabb : scene.aabb;
            if (pos) { // Fly to look at point, don't change eye->look dist
                const camera = scene.camera;
                math.subVec3(camera.eye, camera.look, []);
                controllers.cameraFlight.flyTo({
                    aabb: aabb
                });
                // TODO: Option to back off to fit AABB in view
            } else {// Fly to fit target boundary in view
                controllers.cameraFlight.flyTo({
                    aabb: aabb
                });
            }
        };

        canvas.addEventListener("touchstart", this._canvasTouchStartHandler = (e) => {

            if (!(configs.active && configs.pointerEnabled)) {
                return;
            }

            if (states.longTouchTimeout !== null) {
                clearTimeout(states.longTouchTimeout);
                states.longTouchTimeout = null;
            }

            const touches = e.touches;
            const changedTouches = e.changedTouches;

            touchStartTime = Date.now();

            if (touches.length === 1 && changedTouches.length === 1) {
                tapStartTime = touchStartTime;

                getCanvasPosFromEvent(touches[0], tapStartPos);

                const rightClickClientX = tapStartPos[0];
                const rightClickClientY = tapStartPos[1];

                const rightClickPageX = touches[0].pageX;
                const rightClickPageY = touches[0].pageY;

                states.longTouchTimeout = setTimeout(() => {
                    controllers.cameraControl.fire("rightClick", { // For context menus
                        pagePos: [Math.round(rightClickPageX), Math.round(rightClickPageY)],
                        canvasPos: [Math.round(rightClickClientX), Math.round(rightClickClientY)],
                        event: e
                    }, true);

                    states.longTouchTimeout = null;
                }, configs.longTapTimeout);

            } else {
                tapStartTime = -1;
            }

            while (activeTouches.length < touches.length) {
                activeTouches.push(new Float32Array(2));
            }

            for (let i = 0, len = touches.length; i < len; ++i) {
                getCanvasPosFromEvent(touches[i], activeTouches[i]);
            }

            activeTouches.length = touches.length;

        }, {passive: true});


        canvas.addEventListener("touchend", this._canvasTouchEndHandler = (e) => {

            if (!(configs.active && configs.pointerEnabled)) {
                return;
            }

            const currentTime = Date.now();
            const touches = e.touches;
            const changedTouches = e.changedTouches;

            const pickedSurfaceSubs = cameraControl.hasSubs("pickedSurface");

            if (states.longTouchTimeout !== null) {
                clearTimeout(states.longTouchTimeout);
                states.longTouchTimeout = null;
            }

            // process tap

            if (touches.length === 0 && changedTouches.length === 1) {

                if (tapStartTime > -1 && currentTime - tapStartTime < TAP_INTERVAL) {

                    if (lastTapTime > -1 && tapStartTime - lastTapTime < DBL_TAP_INTERVAL) {

                        // Double-tap

                        getCanvasPosFromEvent(changedTouches[0], pickController.pickCursorPos);
                        pickController.schedulePickEntity = true;
                        pickController.schedulePickSurface = pickedSurfaceSubs;

                        pickController.update();

                        if (pickController.pickResult) {

                            pickController.pickResult.touchInput = true;

                            cameraControl.fire("doublePicked", pickController.pickResult);

                            if (pickController.pickedSurface) {
                                cameraControl.fire("doublePickedSurface", pickController.pickResult);
                            }

                            if (configs.doublePickFlyTo) {
                                flyCameraTo(pickController.pickResult);
                            }
                        } else {
                            cameraControl.fire("doublePickedNothing");
                            if (configs.doublePickFlyTo) {
                                flyCameraTo();
                            }
                        }

                        lastTapTime = -1;

                    } else if (math.distVec2(activeTouches[0], tapStartPos) < TAP_DISTANCE_THRESHOLD) {

                        // Single-tap

                        getCanvasPosFromEvent(changedTouches[0], pickController.pickCursorPos);
                        pickController.schedulePickEntity = true;
                        pickController.schedulePickSurface = pickedSurfaceSubs;

                        pickController.update();

                        if (pickController.pickResult) {

                            pickController.pickResult.touchInput = true;

                            cameraControl.fire("picked", pickController.pickResult);

                            if (pickController.pickedSurface) {
                                cameraControl.fire("pickedSurface", pickController.pickResult);
                            }

                        } else {
                            cameraControl.fire("pickedNothing");
                        }

                        lastTapTime = currentTime;
                    }

                    tapStartTime = -1;
                }
            }

            activeTouches.length = touches.length;

            for (let i = 0, len = touches.length; i < len; ++i) {
                activeTouches[i][0] = touches[i].pageX;
                activeTouches[i][1] = touches[i].pageY;
            }

          //  e.stopPropagation();

        }, {passive: true});

    }

    reset() {
        // TODO
        // tapStartTime = -1;
        // lastTapTime = -1;

    }

    destroy() {
        const canvas = this._scene.canvas.canvas;
        canvas.removeEventListener("touchstart", this._canvasTouchStartHandler);
        canvas.removeEventListener("touchend", this._canvasTouchEndHandler);
    }
}

const DEFAULT_SNAP_PICK_RADIUS = 30;
const DEFAULT_SNAP_VERTEX = true;
const DEFAULT_SNAP_EDGE = true;

/**
 * @desc Controls the {@link Camera} with user input, and fires events when the user interacts with pickable {@link Entity}s.
 *
 * # Contents
 *
 * * [Overview](#overview)
 * * [Examples](#examples)
 * * [Orbit Mode](#orbit-mode)
 *      + [Following the Pointer in Orbit Mode](#--following-the-pointer-in-orbit-mode--)
 *      + [Showing the Pivot Position](#--showing-the-pivot-position--)
 *      + [Axis-Aligned Views in Orbit Mode](#--axis-aligned-views-in-orbit-mode--)
 *      + [View-Fitting Entitys in Orbit Mode](#--view-fitting-entitys-in-orbit-mode--)
 * * [First-Person Mode](#first-person-mode)
 *      + [Following the Pointer in First-Person Mode](#--following-the-pointer-in-first-person-mode--)
 *      + [Constraining Vertical Position in First-Person Mode](#--constraining-vertical-position-in-first-person-mode--)
 *      + [Axis-Aligned Views in First-Person Mode](#--axis-aligned-views-in-first-person-mode--)
 *      + [View-Fitting Entitys in First-Person Mode](#--view-fitting-entitys-in-first-person-mode--)
 * * [Plan-View Mode](#plan-view-mode)
 *      + [Following the Pointer in Plan-View Mode](#--following-the-pointer-in-plan-view-mode--)
 *      + [Axis-Aligned Views in Plan-View Mode](#--axis-aligned-views-in-plan-view-mode--)
 * * [CameraControl Events](#cameracontrol-events)
 *      + ["hover"](#---hover---)
 *      + ["hoverOff"](#---hoveroff---)
 *      + ["hoverEnter"](#---hoverenter---)
 *      + ["hoverOut"](#---hoverout---)
 *      + ["picked"](#---picked---)
 *      + ["pickedSurface"](#---pickedsurface---)
 *      + ["pickedNothing"](#---pickednothing---)
 *      + ["doublePicked"](#---doublepicked---)
 *      + ["doublePickedSurface"](#---doublepickedsurface---)
 *      + ["doublePickedNothing"](#---doublepickednothing---)
 *      + ["rightClick"](#---rightclick---)
 * * [Custom Keyboard Mappings](#custom-keyboard-mappings)
 *
 * <br><br>
 *
 * # Overview
 *
 * * Each {@link Viewer} has a ````CameraControl````, located at {@link Viewer#cameraControl}.
 * * {@link CameraControl#navMode} selects the navigation mode:
 *      * ````"orbit"```` rotates the {@link Camera} position about the target.
 *      * ````"firstPerson"```` rotates the World about the Camera position.
 *      * ````"planView"```` never rotates, but still allows to pan and dolly, typically for an axis-aligned view.
 * * {@link CameraControl#followPointer} makes the Camera follow the mouse or touch pointer.
 * * {@link CameraControl#constrainVertical} locks the Camera to its current height when in first-person mode.
 * * ````CameraControl```` fires pick events when we hover, click or tap on an {@link Entity}.
 * <br><br>
 *
 * # Examples
 *
 * * [Orbit Navigation - Duplex Model](https://xeokit.github.io/xeokit-sdk/examples/index.html#CameraControl_orbit_Duplex)
 * * [Orbit Navigation - Holter Tower Model](https://xeokit.github.io/xeokit-sdk/examples/index.html#CameraControl_orbit_HolterTower)
 * * [First-Person Navigation - Duplex Model](https://xeokit.github.io/xeokit-sdk/examples/index.html#CameraControl_firstPerson_Duplex)
 * * [First-Person Navigation - Holter Tower Model](https://xeokit.github.io/xeokit-sdk/examples/index.html#CameraControl_firstPerson_HolterTower)
 * * [Plan-view Navigation - Schependomlaan Model](https://xeokit.github.io/xeokit-sdk/examples/index.html#CameraControl_planView_Schependomlaan)
 * * [Custom Keyboard Mapping](https://xeokit.github.io/xeokit-sdk/examples/index.html#CameraControl_keyMap)
 * <br><br>
 *
 * # Orbit Mode
 *
 * In orbit mode, ````CameraControl```` orbits the {@link Camera} about the target.
 *
 * To enable orbit mode:
 *
 * ````javascript
 * const cameraControl = myViewer.cameraControl;
 * cameraControl.navMode = "orbit";
 * ````
 *
 * Then orbit by:
 *
 * * left-dragging the mouse,
 * * tap-dragging the touch pad, and
 * * pressing arrow keys, or ````Q```` and ````E```` on a QWERTY keyboard, or ````A```` and ````E```` on an AZERTY keyboard.
 * <br><br>
 *
 * Dolly forwards and backwards by:
 *
 * * spinning the mouse wheel,
 * * pinching on the touch pad, and
 * * pressing the ````+```` and ````-```` keys, or ````W```` and ````S```` on a QWERTY keyboard, or ````Z```` and ````S```` for AZERTY.
 * <br><br>
 *
 * Pan horizontally and vertically by:
 *
 * * right-dragging the mouse,
 * * left-dragging the mouse with the SHIFT key down,
 * * tap-dragging the touch pad with SHIFT down,
 * * pressing the ````A````, ````D````, ````Z```` and ````X```` keys on a QWERTY keyboard, and
 * * pressing the ````Q````, ````D````, ````W```` and ````X```` keys on an AZERTY keyboard,
 * <br><br>
 *
 * ## Following the Pointer in Orbit Mode
 *
 * When {@link CameraControl#followPointer} is ````true````in orbiting mode, the mouse or touch pointer will dynamically
 * indicate the target that the {@link Camera} will orbit, as well as dolly to and from.
 *
 * Lets ensure that we're in orbit mode, then enable the {@link Camera} to follow the pointer:
 *
 * ````javascript
 * cameraControl.navMode = "orbit";
 * cameraControl.followPointer = true;
 * ````
 *
 * ## Smart Pivoting
 *
 * TODO
 *
 * ## Showing the Pivot Position
 *
 * We can configure {@link CameraControl#pivotElement} with an HTML element to indicate the current
 * pivot position. The indicator will appear momentarily each time we move the {@link Camera} while in orbit mode with
 * {@link CameraControl#followPointer} set ````true````.
 *
 * First we'll define some CSS to style our pivot indicator as a black dot with a white border:
 *
 * ````css
 * .camera-pivot-marker {
 *      color: #ffffff;
 *      position: absolute;
 *      width: 25px;
 *      height: 25px;
 *      border-radius: 15px;
 *      border: 2px solid #ebebeb;
 *      background: black;
 *      visibility: hidden;
 *      box-shadow: 5px 5px 15px 1px #000000;
 *      z-index: 10000;
 *      pointer-events: none;
 * }
 * ````
 *
 * Then we'll attach our pivot indicator's HTML element to the ````CameraControl````:
 *
 * ````javascript
 * const pivotElement = document.createRange().createContextualFragment("<div class='camera-pivot-marker'></div>").firstChild;
 *
 * document.body.appendChild(pivotElement);
 *
 * cameraControl.pivotElement = pivotElement;
 * ````
 *
 * ## Axis-Aligned Views in Orbit Mode
 *
 * In orbit mode, we can use keys 1-6 to position the {@link Camera} to look at the center of the {@link Scene} from along each of the
 * six World-space axis. Pressing one of these keys will fly the {@link Camera} to the corresponding axis-aligned view.
 *
 * ## View-Fitting Entitys in Orbit Mode
 *
 * When {@link CameraControl#doublePickFlyTo} is ````true````, we can left-double-click or
 * double-tap (ie. "double-pick") an {@link Entity} to fit it to view. This will cause the {@link Camera}
 * to fly to that Entity. Our target then becomes the center of that Entity. If we are currently pivoting,
 * then our pivot position is then also set to the Entity center.
 *
 * Disable that behaviour by setting {@link CameraControl#doublePickFlyTo} ````false````.
 *
 * # First-Person Mode
 *
 * In first-person mode, ````CameraControl```` rotates the World about the {@link Camera} position.
 *
 * To enable first-person mode:
 *
 * ````javascript
 * cameraControl.navMode = "firstPerson";
 * ````
 *
 * Then rotate by:
 *
 * * left-dragging the mouse,
 * * tap-dragging the touch pad,
 * * pressing arrow keys, or ````Q```` and ````E```` on a QWERTY keyboard, or ````A```` and ````E```` on an AZERTY keyboard.
 * <br><br>
 *
 * Dolly forwards and backwards by:
 *
 * * spinning the mouse wheel,
 * * pinching on the touch pad, and
 * * pressing the ````+```` and ````-```` keys, or ````W```` and ````S```` on a QWERTY keyboard, or ````Z```` and ````S```` for AZERTY.
 * <br><br>
 *
 * Pan left, right, up and down by:
 *
 * * left-dragging or right-dragging the mouse, and
 * * tap-dragging the touch pad with SHIFT down.
 *
 * Pan forwards, backwards, left, right, up and down by pressing the ````WSADZX```` keys on a QWERTY keyboard,
 * or ````WSQDWX```` keys on an AZERTY keyboard.
 * <br><br>
 *
 * ## Following the Pointer in First-Person Mode
 *
 * When {@link CameraControl#followPointer} is ````true```` in first-person mode, the mouse or touch pointer will dynamically
 * indicate the target to which the {@link Camera} will dolly to and from. In first-person mode, however, the World will always rotate
 * about the {@link Camera} position.
 *
 * Lets ensure that we're in first-person mode, then enable the {@link Camera} to follow the pointer:
 *
 * ````javascript
 * cameraControl.navMode = "firstPerson";
 * cameraControl.followPointer = true;
 * ````
 *
 * When the pointer is over empty space, the target will remain the last object that the pointer was over.
 *
 * ## Constraining Vertical Position in First-Person Mode
 *
 * In first-person mode, we can lock the {@link Camera} to its current position on the vertical World axis, which is useful for walk-through navigation:
 *
 * ````javascript
 * cameraControl.constrainVertical = true;
 * ````
 *
 * ## Axis-Aligned Views in First-Person Mode
 *
 * In first-person mode we can use keys 1-6 to position the {@link Camera} to look at the center of
 * the {@link Scene} from along each of the six World-space axis. Pressing one of these keys will fly the {@link Camera} to the
 * corresponding axis-aligned view.
 *
 * ## View-Fitting Entitys in First-Person Mode
 *
 * As in orbit mode, when in first-person mode and {@link CameraControl#doublePickFlyTo} is ````true````, we can double-click
 * or double-tap an {@link Entity} (ie. "double-picking") to fit it in view. This will cause the {@link Camera} to fly to
 * that Entity. Our target then becomes the center of that Entity.
 *
 * Disable that behaviour by setting {@link CameraControl#doublePickFlyTo} ````false````.
 *
 * # Plan-View Mode
 *
 * In plan-view mode, ````CameraControl```` pans and rotates the {@link Camera}, without rotating it.
 *
 * To enable plan-view mode:
 *
 * ````javascript
 * cameraControl.navMode = "planView";
 * ````
 *
 * Dolly forwards and backwards by:
 *
 * * spinning the mouse wheel,
 * * pinching on the touch pad, and
 * * pressing the ````+```` and ````-```` keys.
 *
 * <br>
 * Pan left, right, up and down by:
 *
 * * left-dragging or right-dragging the mouse, and
 * * tap-dragging the touch pad with SHIFT down.
 *
 * Pan forwards, backwards, left, right, up and down by pressing the ````WSADZX```` keys on a QWERTY keyboard,
 * or ````WSQDWX```` keys on an AZERTY keyboard.
 * <br><br>
 *
 * ## Following the Pointer in Plan-View Mode
 *
 * When {@link CameraControl#followPointer} is ````true```` in plan-view mode, the mouse or touch pointer will dynamically
 * indicate the target to which the {@link Camera} will dolly to and from.  In plan-view mode, however, the {@link Camera} cannot rotate.
 *
 * Lets ensure that we're in plan-view mode, then enable the {@link Camera} to follow the pointer:
 *
 * ````javascript
 * cameraControl.navMode = "planView";
 * cameraControl.followPointer = true; // Default
 * ````
 *
 * When the pointer is over empty space, the target will remain the last object that the pointer was over.
 *
 * ## Axis-Aligned Views in Plan-View Mode
 *
 * As in orbit and first-person modes, in plan-view mode we can use keys 1-6 to position the {@link Camera} to look at the center of
 * the {@link Scene} from along each of the six World-space axis. Pressing one of these keys will fly the {@link Camera} to the
 * corresponding axis-aligned view.
 *
 * # CameraControl Events
 *
 * ````CameraControl```` fires events as we interact with {@link Entity}s using mouse or touch input.
 *
 * The following examples demonstrate how to subscribe to those events.
 *
 * The first example shows how to save a handle to a subscription, which we can later use to unsubscribe.
 *
 * ## "hover"
 *
 * Event fired when the pointer moves while hovering over an Entity.
 *
 * ````javascript
 * const onHover = cameraControl.on("hover", (e) => {
 *      const entity = e.entity; // Entity
 *      const canvasPos = e.canvasPos; // 2D canvas position
 * });
 * ````
 *
 * To unsubscribe from the event:
 *
 * ````javascript
 * cameraControl.off(onHover);
 * ````
 *
 * ## "hoverOff"
 *
 * Event fired when the pointer moves while hovering over empty space.
 *
 * ````javascript
 * cameraControl.on("hoverOff", (e) => {
 *      const canvasPos = e.canvasPos;
 * });
 * ````
 *
 * ## "hoverEnter"
 *
 * Event fired when the pointer moves onto an Entity.
 *
 * ````javascript
 * cameraControl.on("hoverEnter", (e) => {
 *      const entity = e.entity;
 *      const canvasPos = e.canvasPos;
 * });
 * ````
 *
 * ## "hoverOut"
 *
 * Event fired when the pointer moves off an Entity.
 *
 * ````javascript
 * cameraControl.on("hoverOut", (e) => {
 *      const entity = e.entity;
 *      const canvasPos = e.canvasPos;
 * });
 * ````
 *
 * ## "picked"
 *
 * Event fired when we left-click or tap on an Entity.
 *
 * ````javascript
 * cameraControl.on("picked", (e) => {
 *      const entity = e.entity;
 *      const canvasPos = e.canvasPos;
 * });
 * ````
 *
 * ## "pickedSurface"
 *
 * Event fired when we left-click or tap on the surface of an Entity.
 *
 * ````javascript
 * cameraControl.on("picked", (e) => {
 *      const entity = e.entity;
 *      const canvasPos = e.canvasPos;
 *      const worldPos = e.worldPos; // 3D World-space position
 *      const viewPos = e.viewPos; // 3D View-space position
 *      const worldNormal = e.worldNormal; // 3D World-space normal vector
 * });
 * ````
 *
 * ## "pickedNothing"
 *
 * Event fired when we left-click or tap on empty space.
 *
 * ````javascript
 * cameraControl.on("pickedNothing", (e) => {
 *      const canvasPos = e.canvasPos;
 * });
 * ````
 *
 * ## "doublePicked"
 *
 * Event fired wwhen we left-double-click or double-tap on an Entity.
 *
 * ````javascript
 * cameraControl.on("doublePicked", (e) => {
 *      const entity = e.entity;
 *      const canvasPos = e.canvasPos;
 * });
 * ````
 *
 * ## "doublePickedSurface"
 *
 * Event fired when we left-double-click or double-tap on the surface of an Entity.
 *
 * ````javascript
 * cameraControl.on("doublePickedSurface", (e) => {
 *      const entity = e.entity;
 *      const canvasPos = e.canvasPos;
 *      const worldPos = e.worldPos;
 *      const viewPos = e.viewPos;
 *      const worldNormal = e.worldNormal;
 * });
 * ````
 *
 * ## "doublePickedNothing"
 *
 * Event fired when we left-double-click or double-tap on empty space.
 *
 * ````javascript
 * cameraControl.on("doublePickedNothing", (e) => {
 *      const canvasPos = e.canvasPos;
 * });
 * ````
 *
 * ## "rightClick"
 *
 * Event fired when we right-click on the canvas.
 *
 * ````javascript
 * cameraControl.on("rightClick", (e) => {
 *      const event = e.event; // Mouse event
 *      const canvasPos = e.canvasPos;
 * });
 * ````
 *
 * ## Custom Keyboard Mappings
 *
 * We can customize````CameraControl```` key bindings as shown below.
 *
 * In this example, we'll just set the default bindings for a QWERTY keyboard.
 *
 * ````javascript
 * const input = myViewer.scene.input;
 *
 * cameraControl.navMode = "orbit";
 * cameraControl.followPointer = true;
 *
 * const keyMap = {};
 *
 * keyMap[cameraControl.PAN_LEFT] = [input.KEY_A];
 * keyMap[cameraControl.PAN_RIGHT] = [input.KEY_D];
 * keyMap[cameraControl.PAN_UP] = [input.KEY_Z];
 * keyMap[cameraControl.PAN_DOWN] = [input.KEY_X];
 * keyMap[cameraControl.DOLLY_FORWARDS] = [input.KEY_W, input.KEY_ADD];
 * keyMap[cameraControl.DOLLY_BACKWARDS] = [input.KEY_S, input.KEY_SUBTRACT];
 * keyMap[cameraControl.ROTATE_X_POS] = [input.KEY_DOWN_ARROW];
 * keyMap[cameraControl.ROTATE_X_NEG] = [input.KEY_UP_ARROW];
 * keyMap[cameraControl.ROTATE_Y_POS] = [input.KEY_LEFT_ARROW];
 * keyMap[cameraControl.ROTATE_Y_NEG] = [input.KEY_RIGHT_ARROW];
 * keyMap[cameraControl.AXIS_VIEW_RIGHT] = [input.KEY_NUM_1];
 * keyMap[cameraControl.AXIS_VIEW_BACK] = [input.KEY_NUM_2];
 * keyMap[cameraControl.AXIS_VIEW_LEFT] = [input.KEY_NUM_3];
 * keyMap[cameraControl.AXIS_VIEW_FRONT] = [input.KEY_NUM_4];
 * keyMap[cameraControl.AXIS_VIEW_TOP] = [input.KEY_NUM_5];
 * keyMap[cameraControl.AXIS_VIEW_BOTTOM] = [input.KEY_NUM_6];
 * keyMap[cameraControl.MOUSE_PAN] = [[input.KEY_SHIFT, input.MOUSE_LEFT_BUTTON]];
 * keyMap[cameraControl.MOUSE_ROTATE] = [
 *     [input.KEY_SHIFT, input.MOUSE_MIDDLE_BUTTON],
 *     [input.KEY_SHIFT, input.MOUSE_RIGHT_BUTTON]
 * ]
 * keyMap[cameraControl.MOUSE_DOLLY] = [[input.KEY_CTRL, input.MOUSE_RIGHT_BUTTON]];
 *
 * cameraControl.keyMap = keyMap;
 * ````
 *
 * We can also just configure default bindings for a specified keyboard layout, like this:
 *
 * ````javascript
 * cameraControl.keyMap = "qwerty";
 * ````
 *
 * Then, ````CameraControl```` will internally set {@link CameraControl#keyMap} to the default key map for the QWERTY
 * layout (which is the same set of mappings we set in the previous example). In other words, if we subsequently
 * read {@link CameraControl#keyMap}, it will now be a key map, instead of the "qwerty" string value we set it to.
 *
 * Supported layouts are, so far:
 *
 * * ````"qwerty"````
 * * ````"azerty"````
 * 
 * ## Basic Keyboard Mapping
 * * ````"OR" Relation````
 * Set multiple keys to trigger an action if any one is pressed:
 * 
 * ````javascript
 * keyMap[cameraControl.DOLLY_BACKWARDS] = [input.KEY_S, input.KEY_SUBTRACT];
 * ````
 * 
 * If either ````KEY_S```` or ````KEY_SUBTRACT```` is pressed, the camera will dolly backward.
 * 
 * * ````"AND" Relation````
 * To require all keys in a combination to be pressed:
 * 
 * ````javascript
 * keyMap[cameraControl.DOLLY_BACKWARDS] = [[input.KEY_S, input.KEY_SUBTRACT]];
 * ````
 * 
 * The camera will dolly backward if ````both KEY_S```` and ````KEY_SUBTRACT```` are pressed.
 * 
 * * ````Mix "AND" and "OR" Relation````
 * Use a combination of keys and groups for flexibility:
 * 
 * ````javascript
 * keyMap[cameraControl.DOLLY_BACKWARDS] = [
 *     [input.KEY_S, input.KEY_SUBTRACT],  // 'And' group
 *     input.KEY_SHIFT                      // 'Or' with previous
 * ];
 * ````
 * 
 * The camera will dolly backward if ````KEY_S```` + ````KEY_SUBTRACT```` are pressed together, or if ````KEY_SHIFT```` is pressed.
 * 
 * ## Special Mouse Actions
 * Certain actions support combinations with specific mouse buttons or events:
 * 
 * * ````MOUSE_PAN````
 * For panning with a key and mouse movement:
 * 
 * ````javascript
 * keyMap[cameraControl.MOUSE_PAN] = [[input.KEY_SHIFT, input.MOUSE_LEFT_BUTTON]];
 * ````
 * 
 * Panning is triggered by pressing ````KEY_SHIFT```` + ````MOUSE_LEFT_BUTTON```` while moving the mouse.
 * 
 * * ````MOUSE_ROTATE````
 * Similar to panning, rotation can be configured with key and mouse button combinations:
 * 
 * ````javascript
 * keyMap[cameraControl.MOUSE_ROTATE] = [[input.KEY_CTRL, input.MOUSE_LEFT_BUTTON]];
 * ````
 * 
 * Rotation is triggered by pressing ````KEY_CTRL```` + ````MOUSE_LEFT_BUTTON```` during mouse movement.
 * 
 * * ````MOUSE_DOLLY````
 * Dolly with mouse wheel scrolling and optional key combinations:
 * 
 * ````javascript
 * keyMap[cameraControl.MOUSE_DOLLY] = [[input.KEY_ALT]];
 * ````
 * 
 * Dolly action occurs when scrolling the mouse wheel with ````KEY_ALT```` held (no need to specify MOUSE_WHEEL).
 * 
 * ## Special Mouse Actions
 * Use ````input.MOUSE_LEFT_BUTTON````, ````input.MOUSE_MIDDLE_BUTTON````, and ````input.MOUSE_RIGHT_BUTTON```` in combinations only for camera movements involving the mouse.
 */
class CameraControl extends Component {

    /**
     * @private
     * @constructor
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.PAN_LEFT = 0;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.PAN_RIGHT = 1;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.PAN_UP = 2;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.PAN_DOWN = 3;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.PAN_FORWARDS = 4;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.PAN_BACKWARDS = 5;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.ROTATE_X_POS = 6;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.ROTATE_X_NEG = 7;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.ROTATE_Y_POS = 8;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.ROTATE_Y_NEG = 9;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.DOLLY_FORWARDS = 10;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.DOLLY_BACKWARDS = 11;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.AXIS_VIEW_RIGHT = 12;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.AXIS_VIEW_BACK = 13;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.AXIS_VIEW_LEFT = 14;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.AXIS_VIEW_FRONT = 15;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.AXIS_VIEW_TOP = 16;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.AXIS_VIEW_BOTTOM = 17;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.MOUSE_PAN = 18;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.MOUSE_ROTATE = 19;

        /**
         * Identifies the XX action.
         * @final
         * @type {Number}
         */
        this.MOUSE_DOLLY = 20;

        this._keyMap = {}; // Maps key codes to the above actions

        this.scene.canvas.canvas.oncontextmenu = (e) => {
            e.preventDefault();
        };

        // User-settable CameraControl configurations

        this._configs = {

            // Private

            longTapTimeout: 600, // Millisecs
            longTapRadius: 5, // Pixels

            // General

            active: true,
            keyboardLayout: "qwerty",
            navMode: "orbit",
            planView: false,
            firstPerson: false,
            followPointer: true,
            doublePickFlyTo: true,
            panRightClick: true,
            showPivot: false,
            pointerEnabled: true,
            constrainVertical: false,
            smartPivot: false,
            doubleClickTimeFrame: 250,
            zoomOnMouseWheel: true,

            snapToVertex: DEFAULT_SNAP_VERTEX,
            snapToEdge: DEFAULT_SNAP_EDGE,
            snapRadius: DEFAULT_SNAP_PICK_RADIUS,

            keyboardEnabledOnlyIfMouseover: true,

            // Rotation

            dragRotationRate: 360.0,
            keyboardRotationRate: 90.0,
            rotationInertia: 0.0,

            // Panning

            keyboardPanRate: 1.0,
            touchPanRate: 1.0,
            panInertia: 0.5,

            // Dollying

            keyboardDollyRate: 10,
            mouseWheelDollyRate: 100,
            touchDollyRate: 0.2,
            dollyInertia: 0,
            dollyProximityThreshold: 30.0,
            dollyMinSpeed: 0.04
        };

        // Current runtime state of the CameraControl

        this._states = {
            pointerCanvasPos: math.vec2(),
            mouseover: false,
            followPointerDirty: true,
            mouseDownClientX: 0,
            mouseDownClientY: 0,
            mouseDownCursorX: 0,
            mouseDownCursorY: 0,
            touchStartTime: null,
            activeTouches: [],
            tapStartPos: math.vec2(),
            tapStartTime: -1,
            lastTapTime: -1,
            longTouchTimeout: null
        };

        // Updates for CameraUpdater to process on next Scene "tick" event

        this._updates = {
            rotateDeltaX: 0,
            rotateDeltaY: 0,
            panDeltaX: 0,
            panDeltaY: 0,
            panDeltaZ: 0,
            dollyDelta: 0
        };

        // Controllers to assist input event handlers with controlling the Camera

        const scene = this.scene;

        this._controllers = {
            cameraControl: this,
            pickController: new PickController(this, this._configs),
            pivotController: new PivotController(scene, this._configs),
            panController: new PanController(scene),
            cameraFlight: new CameraFlightAnimation(this, {
                duration: 0.5
            })
        };

        // Input event handlers

        this._handlers = [
            new MouseMiscHandler(this.scene, this._controllers, this._configs, this._states, this._updates),
            new TouchPanRotateAndDollyHandler(this.scene, this._controllers, this._configs, this._states, this._updates),
            new MousePanRotateDollyHandler(this.scene, this._controllers, this._configs, this._states, this._updates),
            new KeyboardAxisViewHandler(this.scene, this._controllers, this._configs, this._states, this._updates),
            new MousePickHandler(this.scene, this._controllers, this._configs, this._states, this._updates),
            new TouchPickHandler(this.scene, this._controllers, this._configs, this._states, this._updates),
            new KeyboardPanRotateDollyHandler(this.scene, this._controllers, this._configs, this._states, this._updates)
        ];

        this._cursors = {
            dollyForward: "zoom-in",
            dollyBackward: "zoom-out",
            rotate: 'grabbing',
            pan: 'move',
        };

        // Applies scheduled updates to the Camera on each Scene "tick" event

        this._cameraUpdater = new CameraUpdater(this.scene, this._controllers, this._configs, this._states, this._updates);

        // Set initial user configurations

        this.navMode = cfg.navMode;
        if (cfg.planView) {
            this.planView = cfg.planView;
        }
        this.constrainVertical = cfg.constrainVertical;
        if (cfg.keyboardLayout) {
            this.keyboardLayout = cfg.keyboardLayout; // Deprecated
        } else {
            this.keyMap = cfg.keyMap;
        }
        this.doublePickFlyTo = cfg.doublePickFlyTo;
        this.panRightClick = cfg.panRightClick;
        this.active = cfg.active;
        this.followPointer = cfg.followPointer;
        this.rotationInertia = cfg.rotationInertia;
        this.keyboardPanRate = cfg.keyboardPanRate;
        this.touchPanRate = cfg.touchPanRate;
        this.keyboardRotationRate = cfg.keyboardRotationRate;
        this.dragRotationRate = cfg.dragRotationRate;
        this.touchDollyRate = cfg.touchDollyRate;
        this.dollyInertia = cfg.dollyInertia;
        this.dollyProximityThreshold = cfg.dollyProximityThreshold;
        this.dollyMinSpeed = cfg.dollyMinSpeed;
        this.panInertia = cfg.panInertia;
        this.pointerEnabled = true;
        this.keyboardDollyRate = cfg.keyboardDollyRate;
        this.mouseWheelDollyRate = cfg.mouseWheelDollyRate;
    }

    /**
     * Sets custom mappings of keys to ````CameraControl```` actions.
     *
     * See class docs for usage.
     *
     * @param {{Number:Number}|String} value Either a set of new key mappings, or a string to select a keyboard layout,
     * which causes ````CameraControl```` to use the default key mappings for that layout.
     */
    set keyMap(value) {
        value = value || "qwerty";
        if (utils.isString(value)) {
            const input = this.scene.input;
            const keyMap = {};

            switch (value) {

                default:
                    this.error("Unsupported value for 'keyMap': " + value + " defaulting to 'qwerty'");
                // Intentional fall-through to "qwerty"
                case "qwerty":
                    keyMap[this.PAN_LEFT] = [input.KEY_A];
                    keyMap[this.PAN_RIGHT] = [input.KEY_D];
                    keyMap[this.PAN_UP] = [input.KEY_Z];
                    keyMap[this.PAN_DOWN] = [input.KEY_X];
                    keyMap[this.PAN_BACKWARDS] = [];
                    keyMap[this.PAN_FORWARDS] = [];
                    keyMap[this.DOLLY_FORWARDS] = [input.KEY_W, input.KEY_ADD];
                    keyMap[this.DOLLY_BACKWARDS] = [input.KEY_S, input.KEY_SUBTRACT];
                    keyMap[this.ROTATE_X_POS] = [input.KEY_DOWN_ARROW];
                    keyMap[this.ROTATE_X_NEG] = [input.KEY_UP_ARROW];
                    keyMap[this.ROTATE_Y_POS] = [input.KEY_Q, input.KEY_LEFT_ARROW];
                    keyMap[this.ROTATE_Y_NEG] = [input.KEY_E, input.KEY_RIGHT_ARROW];
                    keyMap[this.AXIS_VIEW_RIGHT] = [input.KEY_NUM_1];
                    keyMap[this.AXIS_VIEW_BACK] = [input.KEY_NUM_2];
                    keyMap[this.AXIS_VIEW_LEFT] = [input.KEY_NUM_3];
                    keyMap[this.AXIS_VIEW_FRONT] = [input.KEY_NUM_4];
                    keyMap[this.AXIS_VIEW_TOP] = [input.KEY_NUM_5];
                    keyMap[this.AXIS_VIEW_BOTTOM] = [input.KEY_NUM_6];
                    keyMap[this.MOUSE_PAN] = [
                        [input.MOUSE_LEFT_BUTTON, input.KEY_SHIFT],
                        this._configs.panRightClick ? input.MOUSE_RIGHT_BUTTON : input.MOUSE_MIDDLE_BUTTON
                    ];
                    keyMap[this.MOUSE_ROTATE] = [input.MOUSE_LEFT_BUTTON];
                    keyMap[this.MOUSE_DOLLY] = [];
                    break;

                case "azerty":
                    keyMap[this.PAN_LEFT] = [input.KEY_Q];
                    keyMap[this.PAN_RIGHT] = [input.KEY_D];
                    keyMap[this.PAN_UP] = [input.KEY_W];
                    keyMap[this.PAN_DOWN] = [input.KEY_X];
                    keyMap[this.PAN_BACKWARDS] = [];
                    keyMap[this.PAN_FORWARDS] = [];
                    keyMap[this.DOLLY_FORWARDS] = [input.KEY_Z, input.KEY_ADD];
                    keyMap[this.DOLLY_BACKWARDS] = [input.KEY_S, input.KEY_SUBTRACT];
                    keyMap[this.ROTATE_X_POS] = [input.KEY_DOWN_ARROW];
                    keyMap[this.ROTATE_X_NEG] = [input.KEY_UP_ARROW];
                    keyMap[this.ROTATE_Y_POS] = [input.KEY_A, input.KEY_LEFT_ARROW];
                    keyMap[this.ROTATE_Y_NEG] = [input.KEY_E, input.KEY_RIGHT_ARROW];
                    keyMap[this.AXIS_VIEW_RIGHT] = [input.KEY_NUM_1];
                    keyMap[this.AXIS_VIEW_BACK] = [input.KEY_NUM_2];
                    keyMap[this.AXIS_VIEW_LEFT] = [input.KEY_NUM_3];
                    keyMap[this.AXIS_VIEW_FRONT] = [input.KEY_NUM_4];
                    keyMap[this.AXIS_VIEW_TOP] = [input.KEY_NUM_5];
                    keyMap[this.AXIS_VIEW_BOTTOM] = [input.KEY_NUM_6];
                    keyMap[this.MOUSE_PAN] = [
                        [input.MOUSE_LEFT_BUTTON, input.KEY_SHIFT],
                        this._configs.panRightClick ? input.MOUSE_RIGHT_BUTTON : input.MOUSE_MIDDLE_BUTTON
                    ];
                    keyMap[this.MOUSE_ROTATE] = [input.MOUSE_LEFT_BUTTON];
                    keyMap[this.MOUSE_DOLLY] = [];
                    break;
            }

            this._keyMap = keyMap;
        } else {
            const keyMap = value;
            this._keyMap = keyMap;
        }
    }

    /**
     * Gets custom mappings of keys to {@link CameraControl} actions.
     *
     * @returns {{Number:Number}} Current key mappings.
     */
    get keyMap() {
        return this._keyMap;
    }

    _areAllKeysDown(keyDownMap, keys) {
        if (!keys || keys.length <= 0) {
            return true;
        }
        if (!keyDownMap) {
            return false;
        }
        for (let i = 0, len = keys.length; i < len; i++) {
            const key = keys[i];
            if (!keyDownMap[key])
                return false;
        }
        return true;
    }

    _isAnyOtherKeyDown(keyDownMap, keyMap) {
        for (let i = 0, len = keyDownMap.length; i < len; i++) {
            if (keyDownMap[i]) {
                if (Array.isArray(keyMap)) {
                    if (keyMap.indexOf(i) < 0) return true;
                }
                else if (i !== keyMap) return true;
            }
        }
        return false;
    }

    _isMouseAction(action) {
        switch (action) {
            case this.MOUSE_ROTATE:
            case this.MOUSE_DOLLY:
            case this.MOUSE_PAN:
                return true;
            default:
                return false;
        }
    }

    /**
     * Returns true if any keys configured for the given action are down.
     * @param action
     * @param keyDownMap
     * @private
     */
    _isKeyDownForAction(action, keyDownMap) {
        const keys = this._keyMap[action];
        if (!keys) {
            return false;
        }
        if (keys.length === 0 && this._isMouseAction(action)) return true;
        if (!keyDownMap) {
            keyDownMap = this.scene.input.keyDown;
        }
        for (let i = 0, len = keys.length; i < len; i++) {
            const key = keys[i];
            if (!Array.isArray(key)) {
                if (keyDownMap[key] && !this._isAnyOtherKeyDown(keyDownMap, key))
                    return true;
            }
            else {
                if (this._areAllKeysDown(keyDownMap, key) && !this._isAnyOtherKeyDown(keyDownMap, key))
                    return true;
            }

        }
        return false;
    }

    /**
     * Sets the HTMl element to represent the pivot point when {@link CameraControl#followPointer} is true.
     *
     * See class comments for an example.
     *
     * @param {HTMLElement} element HTML element representing the pivot point.
     */
    set pivotElement(element) {
        this._controllers.pivotController.setPivotElement(element);
    }

    /**
     *  Sets if this ````CameraControl```` is active or not.
     *
     * When inactive, the ````CameraControl```` will not react to input.
     *
     * Default is ````true````.
     *
     * @param {Boolean} value Set ````true```` to activate this ````CameraControl````.
     */
    set active(value) {
        value = value !== false;
        this._configs.active = value;
        this._handlers[1]._active = value;
        this._handlers[5]._active = value;
    }

    /**
     * Gets if this ````CameraControl```` is active or not.
     *
     * When inactive, the ````CameraControl```` will not react to input.
     *
     * Default is ````true````.
     *
     * @returns {Boolean} Returns ````true```` if this ````CameraControl```` is active.
     */
    get active() {
        return this._configs.active;
    }

    /**
     * Sets whether the pointer snap to vertex.
     *
     * @param {boolean} snapToVertex
     */
    set snapToVertex(snapToVertex) {
        this._configs.snapToVertex = !!snapToVertex;
    }

    /**
     * Gets whether the pointer snap to vertex.
     *
     * @returns {boolean}
     */
    get snapToVertex() {
        return this._configs.snapToVertex;
    }

    /**
     * Sets whether the pointer snap to edge.
     *
     * @param {boolean} snapToEdge
     */
    set snapToEdge(snapToEdge) {
        this._configs.snapToEdge = !!snapToEdge;
    }

    /**
     * Gets whether the pointer snap to edge.
     *
     * @returns {boolean}
     */
    get snapToEdge() {
        return this._configs.snapToEdge;
    }

    /**
     * Sets the current snap radius for "hoverSnapOrSurface" events, to specify whether the radius
     * within which the pointer snaps to the nearest vertex or the nearest edge.
     *
     * Default value is 30 pixels.
     *
     * @param {Number} snapRadius The snap radius.
     */
    set snapRadius(snapRadius) {
        snapRadius = snapRadius || DEFAULT_SNAP_PICK_RADIUS;
        this._configs.snapRadius = snapRadius;
    }

    /**
     * Gets the current snap radius.
     *
     * @returns {Number} The snap radius.
     */
    get snapRadius() {
        return this._configs.snapRadius;
    }

    /**
     * If `true`, the keyboard shortcuts are enabled ONLY if the mouse is over the canvas.
     * 
     * @param {boolean} value
     */
    set keyboardEnabledOnlyIfMouseover(value) {
        this._configs.keyboardEnabledOnlyIfMouseover = !!value;
    }

    /**
     * Gets whether the keyboard shortcuts are enabled ONLY if the mouse is over the canvas or ALWAYS.
     * 
     * @returns {boolean}
     */
    get keyboardEnabledOnlyIfMouseover() {
        return this._configs.keyboardEnabledOnlyIfMouseover;
    }
    
    /**
     * Sets the current navigation mode.
     *
     * Accepted values are:
     *
     * * "orbit" - rotation orbits about the current target or pivot point,
     * * "firstPerson" - rotation is about the current eye position,
     * * "planView" - rotation is disabled.
     *
     * See class comments for more info.
     *
     * @param {String} navMode The navigation mode: "orbit", "firstPerson" or "planView".
     */
    set navMode(navMode) {
        navMode = navMode || "orbit";
        if (navMode !== "firstPerson" && navMode !== "orbit" && navMode !== "planView") {
            this.error("Unsupported value for navMode: " + navMode + " - supported values are 'orbit', 'firstPerson' and 'planView' - defaulting to 'orbit'");
            navMode = "orbit";
        }
        this._configs.firstPerson = (navMode === "firstPerson");
        this._configs.planView = (navMode === "planView");
        if (this._configs.firstPerson || this._configs.planView) {
            this._controllers.pivotController.hidePivot();
            this._controllers.pivotController.endPivot();
        }
        this._configs.navMode = navMode;
    }

    /**
     * Gets the current navigation mode.
     *
     * @returns {String} The navigation mode: "orbit", "firstPerson" or "planView".
     */
    get navMode() {
        return this._configs.navMode;
    }

    /**
     * Sets whether mouse and touch input is enabled.
     *
     * Default is ````true````.
     *
     * Disabling mouse and touch input on ````CameraControl```` is useful when we want to temporarily use mouse or
     * touch input to interact with some other 3D control, without disturbing the {@link Camera}.
     *
     * @param {Boolean} value Set ````true```` to enable mouse and touch input.
     */
    set pointerEnabled(value) {
        this._reset();
        this._configs.pointerEnabled = !!value;
    }

    /**
     * Sets the cursor to be used when a particular action is being performed.
     *
     * Accepted actions are:
     * 
     * * "dollyForward" - when the camera is dollying in the forward direction
     * * "dollyBackward" - when the camera is dollying in the backward direction
     * * "pan" - when the camera is being panned
     * * "rotate" - when the camera is being rotated
     *
     * @param {String} action
     * @param {String} style
     */
    setCursorStyle(action, style) {
        if (Object.prototype.hasOwnProperty.call(this._cursors, action)) {
            this._cursors = { ...this._cursors, [action]: style };
        }
        else
            console.warn(`Action '${action}' is not valid for cursor styles.`);
    }

    /**
     * Gets the current style for a particular action.
     *
     * @param {String} action To get the style for
     * @returns {String} style set on the cursor for action
     */
    getCursorStyle(action) {
        return this._cursors[action] || null;
    }

    _reset() {
        for (let i = 0, len = this._handlers.length; i < len; i++) {
            const handler = this._handlers[i];
            if (handler.reset) {
                handler.reset();
            }
        }

        this._updates.panDeltaX = 0;
        this._updates.panDeltaY = 0;
        this._updates.rotateDeltaX = 0;
        this._updates.rotateDeltaY = 0;
        this._updates.dolyDelta = 0;
    }

    /**
     * Gets whether mouse and touch input is enabled.
     *
     * Default is ````true````.
     *
     * Disabling mouse and touch input on ````CameraControl```` is desirable when we want to temporarily use mouse or
     * touch input to interact with some other 3D control, without interfering with the {@link Camera}.
     *
     * @returns {Boolean} Returns ````true```` if mouse and touch input is enabled.
     */
    get pointerEnabled() {
        return this._configs.pointerEnabled;
    }

    /**
     * Sets whether the {@link Camera} follows the mouse/touch pointer.
     *
     * In orbiting mode, the Camera will orbit about the pointer, and will dolly to and from the pointer.
     *
     * In fly-to mode, the Camera will dolly to and from the pointer, however the World will always rotate about the Camera position.
     *
     * In plan-view mode, the Camera will dolly to and from the pointer, however the Camera will not rotate.
     *
     * Default is ````true````.
     *
     * See class comments for more info.
     *
     * @param {Boolean} value Set ````true```` to enable the Camera to follow the pointer.
     */
    set followPointer(value) {
        this._configs.followPointer = (value !== false);
    }

    /**
     * Sets whether the {@link Camera} follows the mouse/touch pointer.
     *
     * In orbiting mode, the Camera will orbit about the pointer, and will dolly to and from the pointer.
     *
     * In fly-to mode, the Camera will dolly to and from the pointer, however the World will always rotate about the Camera position.
     *
     * In plan-view mode, the Camera will dolly to and from the pointer, however the Camera will not rotate.
     *
     * Default is ````true````.
     *
     * See class comments for more info.
     *
     * @returns {Boolean} Returns ````true```` if the Camera follows the pointer.
     */
    get followPointer() {
        return this._configs.followPointer;
    }

    /**
     * Sets the current World-space 3D target position.
     *
     * Only applies when {@link CameraControl#followPointer} is ````true````.
     *
     * @param {Number[]} worldPos The new World-space 3D target position.
     */
    set pivotPos(worldPos) {
        this._controllers.pivotController.setPivotPos(worldPos);
    }

    /**
     * Gets the current World-space 3D pivot position.
     *
     * Only applies when {@link CameraControl#followPointer} is ````true````.
     *
     * @return {Number[]} worldPos The current World-space 3D pivot position.
     */
    get pivotPos() {
        return this._controllers.pivotController.getPivotPos();
    }

    /**
     * @deprecated
     * @param {Boolean} value Set ````true```` to enable dolly-to-pointer behaviour.
     */
    set dollyToPointer(value) {
        this.warn("dollyToPointer property is deprecated - replaced with followPointer");
        this.followPointer = value;
    }

    /**
     * @deprecated
     * @returns {Boolean} Returns ````true```` if dolly-to-pointer behaviour is enabled.
     */
    get dollyToPointer() {
        this.warn("dollyToPointer property is deprecated - replaced with followPointer");
        return this.followPointer;
    }

    /**
     * @deprecated
     * @param {Boolean} value Set ````true```` to enable dolly-to-pointer behaviour.
     */
    set panToPointer(value) {
        this.warn("panToPointer property is deprecated - replaced with followPointer");
    }

    /**
     * @deprecated
     * @returns {Boolean} Returns ````true```` if dolly-to-pointer behaviour is enabled.
     */
    get panToPointer() {
        this.warn("panToPointer property is deprecated - replaced with followPointer");
        return false;
    }

    /**
     * Sets whether this ````CameraControl```` is in plan-view mode.
     *
     * When in plan-view mode, rotation is disabled.
     *
     * Default is ````false````.
     *
     * Deprecated - use {@link CameraControl#navMode} instead.
     *
     * @param {Boolean} value Set ````true```` to enable plan-view mode.
     * @deprecated
     */
    set planView(value) {
        this._configs.planView = !!value;
        this._configs.firstPerson = false;
        if (this._configs.planView) {
            this._controllers.pivotController.hidePivot();
            this._controllers.pivotController.endPivot();
        }
        this.warn("planView property is deprecated - replaced with navMode");
    }

    /**
     * Gets whether this ````CameraControl```` is in plan-view mode.
     *
     * When in plan-view mode, rotation is disabled.
     *
     * Default is ````false````.
     *
     * Deprecated - use {@link CameraControl#navMode} instead.
     *
     * @returns {Boolean} Returns ````true```` if plan-view mode is enabled.
     * @deprecated
     */
    get planView() {
        this.warn("planView property is deprecated - replaced with navMode");
        return this._configs.planView;
    }

    /**
     * Sets whether this ````CameraControl```` is in first-person mode.
     *
     * In "first person" mode (disabled by default) the look position rotates about the eye position. Otherwise,  {@link Camera#eye} rotates about {@link Camera#look}.
     *
     * Default is ````false````.
     *
     * Deprecated - use {@link CameraControl#navMode} instead.
     *
     * @param {Boolean} value Set ````true```` to enable first-person mode.
     * @deprecated
     */
    set firstPerson(value) {
        this.warn("firstPerson property is deprecated - replaced with navMode");
        this._configs.firstPerson = !!value;
        this._configs.planView = false;
        if (this._configs.firstPerson) {
            this._controllers.pivotController.hidePivot();
            this._controllers.pivotController.endPivot();
        }
    }

    /**
     * Gets whether this ````CameraControl```` is in first-person mode.
     *
     * In "first person" mode (disabled by default) the look position rotates about the eye position. Otherwise,  {@link Camera#eye} rotates about {@link Camera#look}.
     *
     * Default is ````false````.
     *
     * Deprecated - use {@link CameraControl#navMode} instead.
     *
     * @returns {Boolean} Returns ````true```` if first-person mode is enabled.
     * @deprecated
     */
    get firstPerson() {
        this.warn("firstPerson property is deprecated - replaced with navMode");
        return this._configs.firstPerson;
    }

    /**
     * Sets whether to vertically constrain the {@link Camera} position for first-person navigation.
     *
     * When set ````true````, this constrains {@link Camera#eye} to its current vertical position.
     *
     * Only applies when {@link CameraControl#navMode} is ````"firstPerson"````.
     *
     * Default is ````false````.
     *
     * @param {Boolean} value Set ````true```` to vertically constrain the Camera.
     */
    set constrainVertical(value) {
        this._configs.constrainVertical = !!value;
    }

    /**
     * Gets whether to vertically constrain the {@link Camera} position for first-person navigation.
     *
     * When set ````true````, this constrains {@link Camera#eye} to its current vertical position.
     *
     * Only applies when {@link CameraControl#navMode} is ````"firstPerson"````.
     *
     * Default is ````false````.
     *
     * @returns {Boolean} ````true```` when Camera is vertically constrained.
     */
    get constrainVertical() {
        return this._configs.constrainVertical;
    }

    /**
     * Sets whether double-picking an {@link Entity} causes the {@link Camera} to fly to its boundary.
     *
     * Default is ````false````.
     *
     * @param {Boolean} value Set ````true```` to enable double-pick-fly-to mode.
     */
    set doublePickFlyTo(value) {
        this._configs.doublePickFlyTo = value !== false;
    }

    /**
     * Gets whether double-picking an {@link Entity} causes the {@link Camera} to fly to its boundary.
     *
     * Default is ````false````.
     *
     * @returns {Boolean} Returns ````true```` when double-pick-fly-to mode is enabled.
     */
    get doublePickFlyTo() {
        return this._configs.doublePickFlyTo;
    }

    /**
     * Sets whether either right-clicking (true) or middle-clicking (false) pans the {@link Camera}.
     *
     * Default is ````true````.
     *
     * @param {Boolean} value Set ````false```` to disable pan on right-click.
     */
    set panRightClick(value) {
        this._configs.panRightClick = value !== false;
        const panKeyMap = this._keyMap[this.MOUSE_PAN];
        if (panKeyMap && panKeyMap.length > 0) {
            const input = this.scene.input;
            for (let i = 0, len = panKeyMap.length; i < len; i++) {
                if (this._configs.panRightClick && panKeyMap[i] === input.MOUSE_MIDDLE_BUTTON) 
                    this._keyMap[this.MOUSE_PAN][i] = input.MOUSE_RIGHT_BUTTON;
                else if (!this._configs.panRightClick && panKeyMap[i] === input.MOUSE_RIGHT_BUTTON)
                    this._keyMap[this.MOUSE_PAN][i] = input.MOUSE_MIDDLE_BUTTON;
            }
        }
    }

    /**
     * Gets whether right-clicking pans the {@link Camera}.
     *
     * Default is ````true````.
     *
     * @returns {Boolean} Returns ````false```` when pan on right-click is disabled.
     */
    get panRightClick() {
        return this._configs.panRightClick;
    }

    /**
     * Sets a factor in range ````[0..1]```` indicating how much the {@link Camera} keeps moving after you finish rotating it.
     *
     * A value of ````0.0```` causes it to immediately stop, ````0.5```` causes its movement to decay 50% on each tick,
     * while ````1.0```` causes no decay, allowing it continue moving, by the current rate of rotation.
     *
     * You may choose an inertia of zero when you want be able to precisely rotate the Camera,
     * without interference from inertia. Zero inertia can also mean that less frames are rendered while
     * you are rotating the Camera.
     *
     * Default is ````0.0````.
     *
     * Does not apply when {@link CameraControl#navMode} is ````"planView"````, which disallows rotation.
     *
     * @param {Number} rotationInertia New inertial factor.
     */
    set rotationInertia(rotationInertia) {
        this._configs.rotationInertia = (rotationInertia !== undefined && rotationInertia !== null) ? rotationInertia : 0.0;
    }

    /**
     * Gets the rotation inertia factor.
     *
     * Default is ````0.0````.
     *
     * Does not apply when {@link CameraControl#navMode} is ````"planView"````, which disallows rotation.
     *
     * @returns {Number} The inertia factor.
     */
    get rotationInertia() {
        return this._configs.rotationInertia;
    }

    /**
     * Sets how much the {@link Camera} pans each second with keyboard input.
     *
     * Default is ````5.0````, to pan the Camera ````5.0```` World-space units every second that
     * a panning key is depressed. See the ````CameraControl```` class documentation for which keys control
     * panning.
     *
     * Panning direction is aligned to our Camera's orientation. When we pan horizontally, we pan
     * to our left and right, when we pan vertically, we pan upwards and downwards, and when we pan forwards
     * and backwards, we pan along the direction the Camera is pointing.
     *
     * Unlike dollying when {@link followPointer} is ````true````, panning does not follow the pointer.
     *
     * @param {Number} keyboardPanRate The new keyboard pan rate.
     */
    set keyboardPanRate(keyboardPanRate) {
        this._configs.keyboardPanRate = (keyboardPanRate !== null && keyboardPanRate !== undefined) ? keyboardPanRate : 5.0;
    }


    /**
     * Sets how fast the camera pans on touch panning
     *
     * @param {Number} touchPanRate The new touch pan rate.
     */
    set touchPanRate(touchPanRate) {
        this._configs.touchPanRate = (touchPanRate !== null && touchPanRate !== undefined) ? touchPanRate : 1.0;
    }

    /**
     * Gets how fast the {@link Camera} pans on touch panning
     *
     * Default is ````1.0````.
     *
     * @returns {Number} The current touch pan rate.
     */
    get touchPanRate() {
        return this._configs.touchPanRate;
    }

    /**
     * Gets how much the {@link Camera} pans each second with keyboard input.
     *
     * Default is ````5.0````.
     *
     * @returns {Number} The current keyboard pan rate.
     */
    get keyboardPanRate() {
        return this._configs.keyboardPanRate;
    }

    /**
     * Sets how many degrees per second the {@link Camera} rotates/orbits with keyboard input.
     *
     * Default is ````90.0````, to rotate/orbit the Camera ````90.0```` degrees every second that
     * a rotation key is depressed. See the ````CameraControl```` class documentation for which keys control
     * rotation/orbit.
     *
     * @param {Number} keyboardRotationRate The new keyboard rotation rate.
     */
    set keyboardRotationRate(keyboardRotationRate) {
        this._configs.keyboardRotationRate = (keyboardRotationRate !== null && keyboardRotationRate !== undefined) ? keyboardRotationRate : 90.0;
    }

    /**
     * Sets how many degrees per second the {@link Camera} rotates/orbits with keyboard input.
     *
     * Default is ````90.0````.
     *
     * @returns {Number} The current keyboard rotation rate.
     */
    get keyboardRotationRate() {
        return this._configs.keyboardRotationRate;
    }

    /**
     * Sets the current drag rotation rate.
     *
     * This configures how many degrees the {@link Camera} rotates/orbits for a full sweep of the canvas by mouse or touch dragging.
     *
     * For example, a value of ````360.0```` indicates that the ````Camera```` rotates/orbits ````360.0```` degrees horizontally
     * when we sweep the entire width of the canvas.
     *
     * ````CameraControl```` makes vertical rotation half as sensitive as horizontal rotation, so that we don't tend to
     * flip upside-down. Therefore, a value of ````360.0```` rotates/orbits the ````Camera```` through ````180.0```` degrees
     * vertically when we sweep the entire height of the canvas.
     *
     * Default is ````360.0````.
     *
     * @param {Number} dragRotationRate The new drag rotation rate.
     */
    set dragRotationRate(dragRotationRate) {
        this._configs.dragRotationRate = (dragRotationRate !== null && dragRotationRate !== undefined) ? dragRotationRate : 360.0;
    }

    /**
     * Gets the current drag rotation rate.
     *
     * Default is ````360.0````.
     *
     * @returns {Number} The current drag rotation rate.
     */
    get dragRotationRate() {
        return this._configs.dragRotationRate;
    }

    /**
     * Sets how much the {@link Camera} dollys each second with keyboard input.
     *
     * Default is ````15.0````, to dolly the {@link Camera} ````15.0```` World-space units per second while we hold down
     * the ````+```` and ````-```` keys.
     *
     * @param {Number} keyboardDollyRate The new keyboard dolly rate.
     */
    set keyboardDollyRate(keyboardDollyRate) {
        this._configs.keyboardDollyRate = (keyboardDollyRate !== null && keyboardDollyRate !== undefined) ? keyboardDollyRate : 15.0;
    }

    /**
     * Gets how much the {@link Camera} dollys each second with keyboard input.
     *
     * Default is ````15.0````.
     *
     * @returns {Number} The current keyboard dolly rate.
     */
    get keyboardDollyRate() {
        return this._configs.keyboardDollyRate;
    }

    /**
     * Sets how much the {@link Camera} dollys with touch input.
     *
     * Default is ````0.2````
     *
     * @param {Number} touchDollyRate The new touch dolly rate.
     */
    set touchDollyRate(touchDollyRate) {
        this._configs.touchDollyRate = (touchDollyRate !== null && touchDollyRate !== undefined) ? touchDollyRate : 0.2;
    }

    /**
     * Gets how much the {@link Camera} dollys each second with touch input.
     *
     * Default is ````0.2````.
     *
     * @returns {Number} The current touch dolly rate.
     */
    get touchDollyRate() {
        return this._configs.touchDollyRate;
    }

    /**
     * Sets how much the {@link Camera} dollys each second while the mouse wheel is spinning.
     *
     * Default is ````100.0````, to dolly the {@link Camera} ````10.0```` World-space units per second as we spin
     * the mouse wheel.
     *
     * @param {Number} mouseWheelDollyRate The new mouse wheel dolly rate.
     */
    set mouseWheelDollyRate(mouseWheelDollyRate) {
        this._configs.mouseWheelDollyRate = (mouseWheelDollyRate !== null && mouseWheelDollyRate !== undefined) ? mouseWheelDollyRate : 100.0;
    }

    /**
     * Gets how much the {@link Camera} dollys each second while the mouse wheel is spinning.
     *
     * Default is ````100.0````.
     *
     * @returns {Number} The current mouseWheel dolly rate.
     */
    get mouseWheelDollyRate() {
        return this._configs.mouseWheelDollyRate;
    }

    /**
     * Sets the dolly inertia factor.
     *
     * This factor configures how much the {@link Camera} keeps moving after you finish dollying it.
     *
     * This factor is a value in range ````[0..1]````. A value of ````0.0```` causes dollying to immediately stop,
     * ````0.5```` causes dollying to decay 50% on each animation frame, while ````1.0```` causes no decay, which allows dollying
     * to continue until further input stops it.
     *
     * You might set ````dollyInertia```` to zero when you want be able to precisely position or rotate the Camera,
     * without interference from inertia. This also means that xeokit renders less frames while dollying the Camera,
     * which can improve rendering performance.
     *
     * Default is ````0````.
     *
     * @param {Number} dollyInertia New dolly inertia factor.
     */
    set dollyInertia(dollyInertia) {
        this._configs.dollyInertia = (dollyInertia !== undefined && dollyInertia !== null) ? dollyInertia : 0;
    }

    /**
     * Gets the dolly inertia factor.
     *
     * Default is ````0````.
     *
     * @returns {Number} The current dolly inertia factor.
     */
    get dollyInertia() {
        return this._configs.dollyInertia;
    }

    /**
     * Sets the proximity to the closest object below which dolly speed decreases, and above which dolly speed increases.
     *
     * Default is ````35.0````.
     *
     * @param {Number} dollyProximityThreshold New dolly proximity threshold.
     */
    set dollyProximityThreshold(dollyProximityThreshold) {
        this._configs.dollyProximityThreshold = (dollyProximityThreshold !== undefined && dollyProximityThreshold !== null) ? dollyProximityThreshold : 35.0;
    }

    /**
     * Gets the proximity to the closest object below which dolly speed decreases, and above which dolly speed increases.
     *
     * Default is ````35.0````.
     *
     * @returns {Number} The current dolly proximity threshold.
     */
    get dollyProximityThreshold() {
        return this._configs.dollyProximityThreshold;
    }

    /**
     * Sets the minimum dolly speed.
     *
     * Default is ````0.04````.
     *
     * @param {Number} dollyMinSpeed New dolly minimum speed.
     */
    set dollyMinSpeed(dollyMinSpeed) {
        this._configs.dollyMinSpeed = (dollyMinSpeed !== undefined && dollyMinSpeed !== null) ? dollyMinSpeed : 0.04;
    }

    /**
     * Gets the minimum dolly speed.
     *
     * Default is ````0.04````.
     *
     * @returns {Number} The current minimum dolly speed.
     */
    get dollyMinSpeed() {
        return this._configs.dollyMinSpeed;
    }

    /**
     * Sets the pan inertia factor.
     *
     * This factor configures how much the {@link Camera} keeps moving after you finish panning it.
     *
     * This factor is a value in range ````[0..1]````. A value of ````0.0```` causes panning to immediately stop,
     * ````0.5```` causes panning to decay 50% on each animation frame, while ````1.0```` causes no decay, which allows panning
     * to continue until further input stops it.
     *
     * You might set ````panInertia```` to zero when you want be able to precisely position or rotate the Camera,
     * without interference from inertia. This also means that xeokit renders less frames while panning the Camera,
     * wich can improve rendering performance.
     *
     * Default is ````0.5````.
     *
     * @param {Number} panInertia New pan inertia factor.
     */
    set panInertia(panInertia) {
        this._configs.panInertia = (panInertia !== undefined && panInertia !== null) ? panInertia : 0.5;
    }

    /**
     * Gets the pan inertia factor.
     *
     * Default is ````0.5````.
     *
     * @returns {Number} The current pan inertia factor.
     */
    get panInertia() {
        return this._configs.panInertia;
    }

    /**
     * Sets the keyboard layout.
     *
     * Supported layouts are:
     *
     * * ````"qwerty"```` (default)
     * * ````"azerty"````
     *
     * @deprecated
     * @param {String} value Selects the keyboard layout.
     */
    set keyboardLayout(value) {
        // this.warn("keyboardLayout property is deprecated - use keyMap property instead");
        value = value || "qwerty";
        if (value !== "qwerty" && value !== "azerty") {
            this.error("Unsupported value for keyboardLayout - defaulting to 'qwerty'");
            value = "qwerty";
        }
        this._configs.keyboardLayout = value;
        this.keyMap = this._configs.keyboardLayout;
    }

    /**
     * Gets the keyboard layout.
     *
     * Supported layouts are:
     *
     * * ````"qwerty"```` (default)
     * * ````"azerty"````
     *
     * @deprecated
     * @returns {String} The current keyboard layout.
     */
    get keyboardLayout() {
        return this._configs.keyboardLayout;
    }

    /**
     * Sets a sphere as the representation of the pivot position.
     *
     * @param {Object} [cfg] Sphere configuration.
     * @param {String} [cfg.size=1] Optional size factor of the sphere. Defaults to 1.
     * @param {String} [cfg.material=PhongMaterial] Optional size factor of the sphere. Defaults to a red opaque material.
     */
    enablePivotSphere(cfg = {}) {
        this._controllers.pivotController.enablePivotSphere(cfg);
    }

    /**
     * Remove the sphere as the representation of the pivot position.
     *
     */
    disablePivotSphere() {
        this._controllers.pivotController.disablePivotSphere();
    }
    
    /**
     * Sets whether smart default pivoting is enabled.
     *
     * When ````true````, we'll pivot by default about the 3D position of the mouse/touch pointer on an
     * imaginary sphere that's centered at {@link Camera#eye} and sized to the {@link Scene} boundary.
     *
     * When ````false````, we'll pivot by default about {@link Camera#look}.
     *
     * Default is ````false````.
     *
     * @param {Boolean} enabled Set ````true```` to pivot by default about the selected point on the virtual sphere, or ````false```` to pivot by default about {@link Camera#look}.
     */
    set smartPivot(enabled) {
        this._configs.smartPivot = (enabled !== false);
    }

    /**
     * Gets whether smart default pivoting is enabled.
     *
     * When ````true````, we'll pivot by default about the 3D position of the mouse/touch pointer on an
     * imaginary sphere that's centered at {@link Camera#eye} and sized to the {@link Scene} boundary.
     *
     * When ````false````, we'll pivot by default about {@link Camera#look}.
     *
     * Default is ````false````.
     *
     * @returns {Boolean} Returns ````true```` when pivoting by default about the selected point on the virtual sphere, or ````false```` when pivoting by default about {@link Camera#look}.
     */
    get smartPivot() {
        return this._configs.smartPivot;
    }

    /**
     * Sets the double click time frame length in milliseconds.
     * 
     * If two mouse click events occur within this time frame, it is considered a double click. 
     * 
     * Default is ````250````
     * 
     * @param {Number} value New double click time frame.
     */
    set doubleClickTimeFrame(value) {
        this._configs.doubleClickTimeFrame = (value !== undefined && value !== null) ? value : 250;
    }

    /**
     * Gets the double click time frame length in milliseconds.
     *  
     * Default is ````250````
     * 
     * @param {Number} value Current double click time frame.
     */
    get doubleClickTimeFrame() {
        return this._configs.doubleClickTimeFrame;
    }

    /**
     * Sets whether to zoom the camera on mouse wheel
     *
     * Default is ````true````
     *
     * @param {Boolean} enabled
     */
    set zoomOnMouseWheel(enabled) {
        this._configs.zoomOnMouseWheel = !!enabled;
    }

    /**
     * Gets whether to zoom the camera on mouse wheel
     *
     * @returns {Boolean}
     */
    get zoomOnMouseWheel() {
        return this._configs.zoomOnMouseWheel;
    }

    /**
     * Destroys this ````CameraControl````.
     * @private
     */
    destroy() {
        this._destroyHandlers();
        this._destroyControllers();
        this._cameraUpdater.destroy();
        super.destroy();
    }

    _destroyHandlers() {
        for (let i = 0, len = this._handlers.length; i < len; i++) {
            const handler = this._handlers[i];
            if (handler.destroy) {
                handler.destroy();
            }
        }
    }

    _destroyControllers() {
        for (let i = 0, len = this._controllers.length; i < len; i++) {
            const controller = this._controllers[i];
            if (controller.destroy) {
                controller.destroy();
            }
        }
    }
}

/**
 * @desc A property within a {@link PropertySet}.
 *
 * @class Property
 */
class Property {

    /**
     * @private
     */
    constructor(name, value, type, valueType, description) {

        /**
         * The name of this property.
         *
         * @property name
         * @type {String}
         */
        this.name = name;

        /**
         * The type of this property.
         *
         * @property type
         * @type {Number|String}
         */
        this.type = type;

        /**
         * The value of this property.
         *
         * @property value
         * @type {*}
         */
        this.value = value;

        /**
         * The type of this property's value.
         *
         * @property valueType
         * @type {Number|String}
         */
        this.valueType = valueType;

        /**
         * Informative text to explain the property.
         *
         * @property name
         * @type {String}
         */
        this.description = description;
    }
}

/**
 * @desc A set of properties associated with one or more {@link MetaObject}s.
 *
 * A PropertySet is created within {@link MetaScene#createMetaModel} and belongs to a {@link MetaModel}.
 *
 * Each PropertySet is registered by {@link PropertySet#id} in {@link MetaScene#propertySets} and {@link MetaModel#propertySets}.
 *
 * @class PropertySet
 */
class PropertySet {

    /**
     * @private
     */
    constructor(params) {

        /**
         * Globally-unique ID for this PropertySet.
         *
         * PropertySet instances are registered by this ID in {@link MetaScene#propertySets} and {@link MetaModel#propertySets}.
         *
         * @property id
         * @type {String}
         */
        this.id = params.id;

        /**
         * ID of the corresponding object within the originating system, if any.
         *
         * @type {String}
         * @abstract
         */
        this.originalSystemId = params.originalSystemId;

        /**
         * The MetaModels that share this PropertySet.
         * @type {MetaModel[]}
         */
        this.metaModels = [];

        /**
         * Human-readable name of this PropertySet.
         *
         * @property name
         * @type {String}
         */
        this.name = params.name;

        /**
         * Type of this PropertySet.
         *
         * @property type
         * @type {String}
         */
        this.type = params.type;

        /**
         * Properties within this PropertySet.
         *
         * @property properties
         * @type {Property[]}
         */
        this.properties = [];

        if (params.properties) {
            const properties = params.properties;
            for (let i = 0, len = properties.length; i < len; i++) {
                const property = properties[i];
                if (Number.isInteger(property)) { // Will decompress in MetaModel.finalize();
                    this.properties.push(property);
                } else {
                    this.properties.push(new Property(property.name, property.value, property.type, property.valueType, property.description));
                }
            }
        }
    }
}

/**
 * @desc Metadata corresponding to an {@link Entity} that represents an object.
 *
 * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````
 *
 * A MetaObject corresponds to an {@link Entity} by having the same {@link MetaObject#id} as the {@link Entity#id}.
 *
 * A MetaObject is created within {@link MetaScene#createMetaModel} and belongs to a {@link MetaModel}.
 *
 * Each MetaObject is registered by {@link MetaObject#id} in {@link MetaScene#metaObjects}.
 *
 * A {@link MetaModel} represents its object structure with a tree of MetaObjects, with {@link MetaModel#rootMetaObject} referencing
 * the root MetaObject.
 *
 * @class MetaObject
 */
class MetaObject {

    /**
     * @private
     */
    constructor(params) {

        /**
         * The MetaModels that share this MetaObject.
         * @type {MetaModel[]}
         */
        this.metaModels = [];

        /**
         * Globally-unique ID.
         *
         * MetaObject instances are registered by this ID in {@link MetaScene#metaObjects}.
         *
         * @property id
         * @type {String|Number}
         */
        this.id = params.id;

        /**
         * ID of the parent MetaObject.
         * @type {String|Number}
         */
        this.parentId = params.parentId;

        /**
         * The parent MetaObject.
         * @type {MetaObject | null}
         */
        this.parent = null;

        /**
         * ID of the corresponding object within the originating system, if any.
         *
         * @type {String}
         * @abstract
         */
        this.originalSystemId = params.originalSystemId;

        /**
         * Human-readable name.
         *
         * @property name
         * @type {String}
         */
        this.name = params.name;

        /**
         * Type - often an IFC product type.
         *
         * @property type
         * @type {String}
         */
        this.type = params.type;

        /**
         * IDs of PropertySets associated with this MetaObject.
         * @type {[]|*}
         */
        this.propertySetIds = params.propertySetIds;

        /**
         * The {@link PropertySet}s associated with this MetaObject.
         *
         * @property propertySets
         * @type {PropertySet[]}
         */
        this.propertySets = [];

        /**
         * The attributes of this MetaObject.
         * @type {{}}
         */
        this.attributes = params.attributes || {};

        if (params.external !== undefined && params.external !== null) {
        
            /**
             * External application-specific metadata
             *
             * Undefined when there are is no external application-specific metadata.
             *
             * @property external
             * @type {*}
             */
            this.external = params.external;
        }
    }

    /**
     * Backwards compatibility with the object belonging to a single MetaModel.
     * 
     * @property metaModel
     * @type {MetaModel|null}
     **/
    get metaModel() {
        if (this.metaModels.length == 1) {
            return this.metaModels[0];
        }

        return null;
    }

    /**
     * Gets the {@link MetaObject#id}s of the {@link MetaObject}s within the subtree.
     *
     * @returns {String[]} Array of {@link MetaObject#id}s.
     */
    getObjectIDsInSubtree() {
        const objectIds = [];

        function visit(metaObject) {
            if (!metaObject) {
                return;
            }
            objectIds.push(metaObject.id);
            const children = metaObject.children;
            if (children) {
                for (var i = 0, len = children.length; i < len; i++) {
                    visit(children[i]);
                }
            }
        }

        visit(this);
        return objectIds;
    }


    /**
     * Iterates over the {@link MetaObject}s within the subtree.
     *
     * @param {Function} callback Callback fired at each {@link MetaObject}.
     */
    withMetaObjectsInSubtree(callback) {

        function visit(metaObject) {
            if (!metaObject) {
                return;
            }
            callback(metaObject);
            const children = metaObject.children;
            if (children) {
                for (var i = 0, len = children.length; i < len; i++) {
                    visit(children[i]);
                }
            }
        }

        visit(this);
    }

    /**
     * Gets the {@link MetaObject#id}s of the {@link MetaObject}s within the subtree that have the given {@link MetaObject#type}s.
     *
     * @param {String[]} types {@link MetaObject#type} values.
     * @returns {String[]} Array of {@link MetaObject#id}s.
     */
    getObjectIDsInSubtreeByType(types) {
        const mask = {};
        for (var i = 0, len = types.length; i < len; i++) {
            mask[types[i]] = types[i];
        }
        const objectIds = [];

        function visit(metaObject) {
            if (!metaObject) {
                return;
            }
            if (mask[metaObject.type]) {
                objectIds.push(metaObject.id);
            }
            const children = metaObject.children;
            if (children) {
                for (var i = 0, len = children.length; i < len; i++) {
                    visit(children[i]);
                }
            }
        }

        visit(this);
        return objectIds;
    }

    /**
     * Returns properties of this MeteObject as JSON.
     *
     * @returns {{id: (String|Number), type: String, name: String, parent: (String|Number|Undefined)}}
     */
    getJSON() {
        var json = {
            id: this.id,
            type: this.type,
            name: this.name
        };
        if (this.parent) {
            json.parent = this.parent.id;
        }
        return json;
    }
}

/**
 * @desc Metadata corresponding to an {@link Entity} that represents a model.
 *
 * An {@link Entity} represents a model when {@link Entity#isModel} is ````true````
 *
 * A MetaModel corresponds to an {@link Entity} by having the same {@link MetaModel#id} as the {@link Entity#id}.
 *
 * A MetaModel is created by {@link MetaScene#createMetaModel} and belongs to a {@link MetaScene}.
 *
 * Each MetaModel is registered by {@link MetaModel#id} in {@link MetaScene#metaModels}.
 *
 * A {@link MetaModel} represents its object structure with a tree of {@link MetaObject}s, with {@link MetaModel#rootMetaObject} referencing the root {@link MetaObject}.
 *
 * @class MetaModel
 */
class MetaModel {

    /**
     * Creates a new, unfinalized MetaModel.
     *
     * * The MetaModel is immediately registered by {@link MetaModel#id} in {@link MetaScene#metaModels}, even though it's not yet populated.
     * * The MetaModel then needs to be populated with one or more calls to {@link metaModel#loadData}.
     * * As we populate it, the MetaModel will create {@link MetaObject}s and {@link PropertySet}s in itself, and in the MetaScene.
     * * When populated, call {@link MetaModel#finalize} to finish it off, which causes MetaScene to fire a "metaModelCreated" event.
     */
    constructor(params) {

        /**
         * Globally-unique ID.
         *
         * MetaModels are registered by ID in {@link MetaScene#metaModels}.
         *
         * When this MetaModel corresponds to an {@link Entity} then this ID will match the {@link Entity#id}.
         *
         * @property id
         * @type {String|Number}
         */
        this.id = params.id;

        /**
         * The project ID
         * @property projectId
         * @type {String|Number}
         */
        this.projectId = params.projectId;

        /**
         * The revision ID, if available.
         *
         * Will be undefined if not available.
         *
         * @property revisionId
         * @type {String|Number}
         */
        this.revisionId = params.revisionId;

        /**
         * The model author, if available.
         *
         * Will be undefined if not available.
         *
         * @property author
         * @type {String}
         */
        this.author = params.author;

        /**
         * The date the model was created, if available.
         *
         * Will be undefined if not available.
         *
         * @property createdAt
         * @type {String}
         */
        this.createdAt = params.createdAt;

        /**
         * The application that created the model, if available.
         *
         * Will be undefined if not available.
         *
         * @property creatingApplication
         * @type {String}
         */
        this.creatingApplication = params.creatingApplication;

        /**
         * The model schema version, if available.
         *
         * Will be undefined if not available.
         *
         * @property schema
         * @type {String}
         */
        this.schema = params.schema;

        /**
         * Metadata on the {@link Scene}.
         *
         * @property metaScene
         * @type {MetaScene}
         */
        this.metaScene = params.metaScene;

        /**
         * The {@link PropertySet}s in this MetaModel.
         *
         * @property propertySets
         * @type  {PropertySet[]}
         */
        this.propertySets = [];

        /**
         * The root {@link MetaObject}s in this MetaModel's composition structure hierarchy.
         *
         * @property rootMetaObject
         * @type {MetaObject[]}
         */
        this.rootMetaObjects = [];

        /**
         * The {@link MetaObject}s in this MetaModel, each mapped to its ID.
         *
         * @property metaObjects
         * @type  {{String:MetaObject}}
         */
        this.metaObjects = {};

        /**
         * Connectivity graph.
         * @type {{}}
         */
        this.graph = params.graph || {};

        this.metaScene.metaModels[this.id] = this;

        this._propertyLookup = [];

        /**
         * True when this MetaModel has been finalized.
         * @type {boolean}
         */
        this.finalized = false;
    }

    /**
     * Backwards compatibility with the model having a single root MetaObject.
     *
     * @property rootMetaObject
     * @type {MetaObject|null}
     */
    get rootMetaObject() {
        if (this.rootMetaObjects.length === 1) {
            return this.rootMetaObjects[0];
        }
        return null;
    }

    /**
     * Load metamodel data into this MetaModel.
     * @param metaModelData
     */
    loadData(metaModelData, options = {}) {

        if (this.finalized) {
            throw "MetaScene already finalized - can't add more data";
        }

        this._globalizeIDs(metaModelData, options);

        const metaScene = this.metaScene;
        const propertyLookup = metaModelData.properties;

        if (propertyLookup) {
            for (let i = 0, len = propertyLookup.length; i < len; i++) {
                this._propertyLookup.push(propertyLookup[i]);
            }
        }

        // Create global Property Sets

        if (metaModelData.propertySets) {
            for (let i = 0, len = metaModelData.propertySets.length; i < len; i++) {
                const propertySetData = metaModelData.propertySets[i];
                if (!propertySetData.properties) { // HACK: https://github.com/Creoox/creoox-ifc2gltfcxconverter/issues/8
                    propertySetData.properties = [];
                }
                let propertySet = metaScene.propertySets[propertySetData.id];
                if (!propertySet) {
                    propertySet = new PropertySet({
                        id: propertySetData.id,
                        originalSystemId: propertySetData.originalSystemId || propertySetData.id,
                        type: propertySetData.type,
                        name: propertySetData.name,
                        properties: propertySetData.properties
                    });
                    metaScene.propertySets[propertySet.id] = propertySet;
                }
                propertySet.metaModels.push(this);
                this.propertySets.push(propertySet);
            }
        }

        if (metaModelData.metaObjects) {
            for (let i = 0, len = metaModelData.metaObjects.length; i < len; i++) {
                const metaObjectData = metaModelData.metaObjects[i];
                const id = metaObjectData.id;
                let metaObject = metaScene.metaObjects[id];
                if (!metaObject) {
                    const type = metaObjectData.type;
                    const originalSystemId = metaObjectData.originalSystemId;
                    const propertySetIds = metaObjectData.propertySets || metaObjectData.propertySetIds;
                    metaObject = new MetaObject({
                        id,
                        originalSystemId,
                        parentId: metaObjectData.parent,
                        type,
                        name: metaObjectData.name,
                        attributes: metaObjectData.attributes,
                        propertySetIds,
                        external: metaObjectData.external,
                    });
                    this.metaScene.metaObjects[id] = metaObject;
                    metaObject.metaModels = [];
                }
                this.metaObjects[id] = metaObject;
                if (!metaObjectData.parent) {
                    this.rootMetaObjects.push(metaObject);
                    metaScene.rootMetaObjects[id] = metaObject;
                }
                metaObject.metaModels.push(this);
            }
        }
    }

    _decompressProperties(propertyLookup, properties) {
        const propsNotFound = [];
        for (let i = 0, len = properties.length; i < len; i++) {
            const property = properties[i];
            if (Number.isInteger(property)) {
                const lookupProperty = propertyLookup[property];
                if (lookupProperty) {
                    properties[i] = lookupProperty;
                } else {
                    propsNotFound.push(property);
                }
            }
        }
        if (propsNotFound.length > 0) {
            console.error(`[MetaModel._decompressProperties] Properties not found: ${propsNotFound}`);
        }
    }

    finalize() {

        if (this.finalized) {
            throw "MetaScene already finalized - can't re-finalize";
        }

        // Re-link MetaScene's entire MetaObject parent/child hierarchy

        const metaScene = this.metaScene;

        for (let objectId in metaScene.metaObjects) {
            const metaObject = metaScene.metaObjects[objectId];
            if (metaObject.children) {
                metaObject.children = [];
            }

            // Re-link each MetaObject's property sets

            if (metaObject.propertySets) {
                metaObject.propertySets = [];
            }
            if (metaObject.propertySetIds) {
                for (let i = 0, len = metaObject.propertySetIds.length; i < len; i++) {
                    const propertySetId = metaObject.propertySetIds[i];
                    const propertySet = metaScene.propertySets[propertySetId];
                    metaObject.propertySets.push(propertySet);
                }
            }
        }

        for (let objectId in metaScene.metaObjects) {
            const metaObject = metaScene.metaObjects[objectId];
            if (metaObject.parentId) {
                const parentMetaObject = metaScene.metaObjects[metaObject.parentId];
                if (parentMetaObject) {
                    metaObject.parent = parentMetaObject;
                    (parentMetaObject.children || (parentMetaObject.children = [])).push(metaObject);
                }
            }
        }

        // Relink MetaObjects to their MetaModels

        for (let objectId in metaScene.metaObjects) {
            const metaObject = metaScene.metaObjects[objectId];
            metaObject.metaModels = [];
        }

        for (let modelId in metaScene.metaModels) {
            const metaModel = metaScene.metaModels[modelId];
            for (let objectId in metaModel.metaObjects) {
                const metaObject = metaModel.metaObjects[objectId];
                metaObject.metaModels.push(metaModel);
            }
        }

        // Rebuild MetaScene's MetaObjects-by-type lookup

        metaScene.metaObjectsByType = {};
        for (let objectId in metaScene.metaObjects) {
            const metaObject = metaScene.metaObjects[objectId];
            const type = metaObject.type;
            (metaScene.metaObjectsByType[type] || (metaScene.metaObjectsByType[type] = {}))[objectId] = metaObject;
        }

        // Decompress properties

        if (this.propertySets) {
            for (let i = 0, len = this.propertySets.length; i < len; i++) {
                const propertySet = this.propertySets[i];
                this._decompressProperties(this._propertyLookup, propertySet.properties);
            }
        }


        this._propertyLookup = [];

        this.finalized = true;

        this.metaScene.fire("metaModelCreated", this.id);
    }

    /**
     * Gets this MetaModel as JSON.
     * @returns {{schema: (String|string|*), createdAt: (String|string|*), metaObjects: *[], author: (String|string|*), id: (String|Number|string|number|*), creatingApplication: (String|string|*), projectId: (String|Number|string|number|*), propertySets: *[]}}
     */
    getJSON() {
        const json = {
            id: this.id,
            projectId: this.projectId,
            author: this.author,
            createdAt: this.createdAt,
            schema: this.schema,
            creatingApplication: this.creatingApplication,
            metaObjects: [],
            propertySets: []
        };
        const metaObjectsList = Object.values(this.metaObjects);
        for (let i = 0, len = metaObjectsList.length; i < len; i++) {
            const metaObject = metaObjectsList[i];
            const metaObjectCfg = {
                id: metaObject.id,
                originalSystemId: metaObject.originalSystemId,
                extId: metaObject.extId,
                type: metaObject.type,
                name: metaObject.name
            };
            if (metaObject.parent) {
                metaObjectCfg.parent = metaObject.parent.id;
            }
            if (metaObject.attributes) {
                metaObjectCfg.attributes = metaObject.attributes;
            }
            if (metaObject.propertySetIds) {
                metaObjectCfg.propertySetIds = metaObject.propertySetIds;
            }
            json.metaObjects.push(metaObjectCfg);
        }
        for (let i = 0, len = this.propertySets.length; i < len; i++) {
            const propertySet = this.propertySets[i];
            const propertySetCfg = {
                id: propertySet.id,
                originalSystemId: propertySet.originalSystemId,
                extId: propertySet.extId,
                type: propertySet.type,
                name: propertySet.name,
                propertyies: []
            };
            for (let j = 0, lenj = propertySet.properties.length; j < lenj; j++) {
                const property = propertySet.properties[j];
                const propertyCfg = {
                    id: property.id,
                    description: property.description,
                    type: property.type,
                    name: property.name,
                    value: property.value,
                    valueType: property.valueType
                };
                propertySetCfg.properties.push(propertyCfg);
            }
            json.propertySets.push(propertySetCfg);
        }
        return json;
    }

    _globalizeIDs(metaModelData, options) {

        const globalize = !!options.globalizeObjectIds;

        if (metaModelData.metaObjects) {
            for (let i = 0, len = metaModelData.metaObjects.length; i < len; i++) {
                const metaObjectData = metaModelData.metaObjects[i];

                // Globalize MetaObject IDs and parent IDs

                metaObjectData.originalSystemId = metaObjectData.id;
                if (metaObjectData.parent) {
                    metaObjectData.originalParentSystemId = metaObjectData.parent;
                }
                if (globalize) {
                    metaObjectData.id = math.globalizeObjectId(this.id, metaObjectData.id);
                    if (metaObjectData.parent) {
                        metaObjectData.parent = math.globalizeObjectId(this.id, metaObjectData.parent);
                    }
                }

                // Globalize MetaObject property set IDs

                if (globalize) {
                    const propertySetIds = metaObjectData.propertySetIds;
                    if (propertySetIds) {
                        const propertySetGlobalIds = [];
                        for (let j = 0, lenj = propertySetIds.length; j < lenj; j++) {
                            propertySetGlobalIds.push(math.globalizeObjectId(this.id, propertySetIds[j]));
                        }
                        metaObjectData.propertySetIds = propertySetGlobalIds;
                        metaObjectData.originalSystemPropertySetIds = propertySetIds;
                    }
                } else {
                    metaObjectData.originalSystemPropertySetIds = metaObjectData.propertySetIds;
                }
            }
        }

        // Globalize global PropertySet IDs

        if (metaModelData.propertySets) {
            for (let i = 0, len = metaModelData.propertySets.length; i < len; i++) {
                const propertySet = metaModelData.propertySets[i];
                propertySet.originalSystemId = propertySet.id;
                if (globalize) {
                    propertySet.id = math.globalizeObjectId(this.id, propertySet.id);
                }
            }
        }
    }
}

/**
 * @desc Metadata corresponding to a {@link Scene}.
 *
 * * Located in {@link Viewer#metaScene}.
 * * Contains {@link MetaModel}s and {@link MetaObject}s.
 * * [Scene graph example with metadata](http://xeokit.github.io/xeokit-sdk/examples/index.html#sceneRepresentation_SceneGraph_metadata)
 */
class MetaScene {

    /**
     * @private
     */
    constructor(viewer, scene) {

        /**
         * The {@link Viewer}.
         * @property viewer
         * @type {Viewer}
         */
        this.viewer = viewer;

        /**
         * The {@link Scene}.
         * @property scene
         * @type {Scene}
         */
        this.scene = scene;

        /**
         * The {@link MetaModel}s belonging to this MetaScene, each mapped to its {@link MetaModel#modelId}.
         *
         * @type {{String:MetaModel}}
         */
        this.metaModels = {};

        /**
         * The {@link PropertySet}s belonging to this MetaScene, each mapped to its {@link PropertySet#id}.
         *
         * @type {{String:PropertySet}}
         */
        this.propertySets = {};

        /**
         * The {@link MetaObject}s belonging to this MetaScene, each mapped to its {@link MetaObject#id}.
         *
         * @type {{String:MetaObject}}
         */
        this.metaObjects = {};

        /**
         * The {@link MetaObject}s belonging to this MetaScene, each mapped to its {@link MetaObject#type}.
         *
         * @type {{String:MetaObject}}
         */
        this.metaObjectsByType = {};

        /**
         * The root {@link MetaObject}s belonging to this MetaScene, each mapped to its {@link MetaObject#id}.
         *
         * @type {{String:MetaObject}}
         */
        this.rootMetaObjects = {};

        /**
         * Subscriptions to events sent with {@link fire}.
         * @private
         */
        this._eventSubs = {};
    }

    /**
     * Subscribes to an event fired at this Viewer.
     *
     * @param {String} event The event
     * @param {Function} callback Callback fired on the event
     */
    on(event, callback) {
        let subs = this._eventSubs[event];
        if (!subs) {
            subs = [];
            this._eventSubs[event] = subs;
        }
        subs.push(callback);
    }

    /**
     * Fires an event at this Viewer.
     *
     * @param {String} event Event name
     * @param {Object} value Event parameters
     */
    fire(event, value) {
        const subs = this._eventSubs[event];
        if (subs) {
            for (let i = 0, len = subs.length; i < len; i++) {
                subs[i](value);
            }
        }
    }

    /**
     * Unsubscribes from an event fired at this Viewer.
     * @param event
     */
    off(event) { // TODO

    }

    /**
     * Creates a {@link MetaModel} in this MetaScene.
     *
     * The MetaModel will contain a hierarchy of {@link MetaObject}s, created from the
     * meta objects in ````metaModelData````.
     *
     * The meta object hierarchy in ````metaModelData```` is expected to be non-cyclic, with a single root. If the meta
     * objects are cyclic, then this method will log an error and attempt to recover by creating a dummy root MetaObject
     * of type "Model" and connecting all other MetaObjects as its direct children. If the meta objects contain multiple
     * roots, then this method similarly attempts to recover by creating a dummy root MetaObject of type "Model" and
     * connecting all the root MetaObjects as its children.
     *
     * @param {String} modelId ID for the new {@link MetaModel}, which will have {@link MetaModel#id} set to this value.
     * @param {Object} metaModelData Data for the {@link MetaModel}.
     * @param {Object} [options] Options for creating the {@link MetaModel}.
     * @param {Boolean} [options.globalizeObjectIds=false] Whether to globalize each {@link MetaObject#id}. Set this ````true```` when you need to load multiple instances of the same meta model, to avoid ID clashes between the meta objects in the different instances.
     * @returns {MetaModel} The new MetaModel.
     */
    createMetaModel(modelId, metaModelData, options = {}) {

        const metaModel = new MetaModel({ // Registers MetaModel in #metaModels
            metaScene: this,
            id: modelId,
            projectId: metaModelData.projectId || "none",
            revisionId: metaModelData.revisionId || "none",
            author: metaModelData.author || "none",
            createdAt: metaModelData.createdAt || "none",
            creatingApplication: metaModelData.creatingApplication || "none",
            schema: metaModelData.schema || "none",
            propertySets: []
        });

        metaModel.loadData(metaModelData);

        metaModel.finalize();

        return metaModel;
    }

    /**
     * Removes a {@link MetaModel} from this MetaScene.
     *
     * Fires a "metaModelDestroyed" event with the value of the {@link MetaModel#id}.
     *
     * @param {String} metaModelId ID of the target {@link MetaModel}.
     */
    destroyMetaModel(metaModelId) {

        const metaModel = this.metaModels[metaModelId];
        if (!metaModel) {
            return;
        }

        // Remove global PropertySets

        if (metaModel.propertySets) {
            for (let i = 0, len = metaModel.propertySets.length; i < len; i++) {
                const propertySet = metaModel.propertySets[i];
                if (propertySet.metaModels.length === 1 && propertySet.metaModels[0].id === metaModelId) { // Property set owned only by this model, delete
                    delete this.propertySets[propertySet.id];
                } else {
                    const newMetaModels = [];
                    for (let j = 0, lenj = propertySet.metaModels.length; j < lenj; j++) {
                        if (propertySet.metaModels[j].id !== metaModelId) {
                            newMetaModels.push(propertySet.metaModels[j]);
                        }
                    }
                    propertySet.metaModels = newMetaModels;
                }
            }
        }

        // Remove MetaObjects

        if (metaModel.metaObjects) {
            for (let objectId in metaModel.metaObjects) {
                const metaObject = metaModel.metaObjects[objectId];
                if (metaObject.metaModels.length === 1 && metaObject.metaModels[0].id === metaModelId) { // MetaObject owned only by this model, delete
                    delete this.metaObjects[objectId];
                    if (!metaObject.parent) {
                        delete this.rootMetaObjects[objectId];
                    }
                } else {
                    metaObject.metaModels = metaObject.metaModels.filter(metaModel => metaModel.id !== metaModelId);
                }
            }
        }

        // Re-link entire MetaObject parent/child hierarchy

        for (let objectId in this.metaObjects) {
            const metaObject = this.metaObjects[objectId];
            if (metaObject.children) {
                metaObject.children = [];
            }

            // Re-link each MetaObject's property sets

            if (metaObject.propertySets) {
                metaObject.propertySets = [];
            }
            if (metaObject.propertySetIds) {
                for (let i = 0, len = metaObject.propertySetIds.length; i < len; i++) {
                    const propertySetId = metaObject.propertySetIds[i];
                    const propertySet = this.propertySets[propertySetId];
                    metaObject.propertySets.push(propertySet);
                }
            }
        }

        this.metaObjectsByType = {};

        for (let objectId in this.metaObjects) {
            const metaObject = this.metaObjects[objectId];
            const type = metaObject.type;
            if (metaObject.children) {
                metaObject.children = null;
            }
            (this.metaObjectsByType[type] || (this.metaObjectsByType[type] = {}))[objectId] = metaObject;
        }

        for (let objectId in this.metaObjects) {
            const metaObject = this.metaObjects[objectId];
            if (metaObject.parentId) {
                const parentMetaObject = this.metaObjects[metaObject.parentId];
                if (parentMetaObject) {
                    metaObject.parent = parentMetaObject;
                    (parentMetaObject.children || (parentMetaObject.children = [])).push(metaObject);
                }
            }
        }

        delete this.metaModels[metaModelId];

        // Relink MetaObjects to their MetaModels

        for (let objectId in this.metaObjects) {
            const metaObject = this.metaObjects[objectId];
            metaObject.metaModels = [];
        }

        for (let modelId in this.metaModels) {
            const metaModel = this.metaModels[modelId];
            for (let objectId in metaModel.metaObjects) {
                const metaObject = metaModel.metaObjects[objectId];
                metaObject.metaModels.push(metaModel);
            }
        }

        this.fire("metaModelDestroyed", metaModelId);
    }

    /**
     * Gets the {@link MetaObject#id}s of the {@link MetaObject}s that have the given {@link MetaObject#type}.
     *
     * @param {String} type The type.
     * @returns {String[]} Array of {@link MetaObject#id}s.
     */
    getObjectIDsByType(type) {
        const metaObjects = this.metaObjectsByType[type];
        return metaObjects ? Object.keys(metaObjects) : [];
    }

    /**
     * Gets the {@link MetaObject#id}s of the {@link MetaObject}s within the given subtree.
     *
     * @param {String} id  ID of the root {@link MetaObject} of the given subtree.
     * @param {String[]} [includeTypes] Optional list of types to include.
     * @param {String[]} [excludeTypes] Optional list of types to exclude.
     * @returns {String[]} Array of {@link MetaObject#id}s.
     */
    getObjectIDsInSubtree(id, includeTypes, excludeTypes) {

        const list = [];
        const metaObject = this.metaObjects[id];
        const includeMask = (includeTypes && includeTypes.length > 0) ? arrayToMap(includeTypes) : null;
        const excludeMask = (excludeTypes && excludeTypes.length > 0) ? arrayToMap(excludeTypes) : null;

        const visit = (metaObject) => {
            if (!metaObject) {
                return;
            }
            var include = true;
            if (excludeMask && excludeMask[metaObject.type]) {
                include = false;
            } else if (includeMask && (!includeMask[metaObject.type])) {
                include = false;
            }
            if (include) {
                list.push(metaObject.id);
            }
            const children = metaObject.children;
            if (children) {
                for (var i = 0, len = children.length; i < len; i++) {
                    visit(children[i]);
                }
            }
        };

        visit(metaObject);
        return list;
    }

    /**
     * Iterates over the {@link MetaObject}s within the subtree.
     *
     * @param {String} id ID of root {@link MetaObject}.
     * @param {Function} callback Callback fired at each {@link MetaObject}.
     */
    withMetaObjectsInSubtree(id, callback) {
        const metaObject = this.metaObjects[id];
        if (!metaObject) {
            return;
        }
        metaObject.withMetaObjectsInSubtree(callback);
    }
}

function arrayToMap(array) {
    const map = {};
    for (var i = 0, len = array.length; i < len; i++) {
        map[array[i]] = true;
    }
    return map;
}

/*!
 * html2canvas 1.4.1 <https://html2canvas.hertzen.com>
 * Copyright (c) 2022 Niklas von Hertzen <https://hertzen.com>
 * Released under MIT License
 */
/*! *****************************************************************************
Copyright (c) Microsoft Corporation.

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise */

var extendStatics = function(d, b) {
    extendStatics = Object.setPrototypeOf ||
        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
        function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
    return extendStatics(d, b);
};

function __extends(d, b) {
    if (typeof b !== "function" && b !== null)
        throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
    extendStatics(d, b);
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}

var __assign = function() {
    __assign = Object.assign || function __assign(t) {
        for (var s, i = 1, n = arguments.length; i < n; i++) {
            s = arguments[i];
            for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
        }
        return t;
    };
    return __assign.apply(this, arguments);
};

function __awaiter(thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, [])).next());
    });
}

function __generator(thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
}

function __spreadArray(to, from, pack) {
    if (arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
        if (ar || !(i in from)) {
            if (!ar) ar = Array.prototype.slice.call(from, 0, i);
            ar[i] = from[i];
        }
    }
    return to.concat(ar || from);
}

var Bounds = /** @class */ (function () {
    function Bounds(left, top, width, height) {
        this.left = left;
        this.top = top;
        this.width = width;
        this.height = height;
    }
    Bounds.prototype.add = function (x, y, w, h) {
        return new Bounds(this.left + x, this.top + y, this.width + w, this.height + h);
    };
    Bounds.fromClientRect = function (context, clientRect) {
        return new Bounds(clientRect.left + context.windowBounds.left, clientRect.top + context.windowBounds.top, clientRect.width, clientRect.height);
    };
    Bounds.fromDOMRectList = function (context, domRectList) {
        var domRect = Array.from(domRectList).find(function (rect) { return rect.width !== 0; });
        return domRect
            ? new Bounds(domRect.left + context.windowBounds.left, domRect.top + context.windowBounds.top, domRect.width, domRect.height)
            : Bounds.EMPTY;
    };
    Bounds.EMPTY = new Bounds(0, 0, 0, 0);
    return Bounds;
}());
var parseBounds = function (context, node) {
    return Bounds.fromClientRect(context, node.getBoundingClientRect());
};
var parseDocumentSize = function (document) {
    var body = document.body;
    var documentElement = document.documentElement;
    if (!body || !documentElement) {
        throw new Error("Unable to get document size");
    }
    var width = Math.max(Math.max(body.scrollWidth, documentElement.scrollWidth), Math.max(body.offsetWidth, documentElement.offsetWidth), Math.max(body.clientWidth, documentElement.clientWidth));
    var height = Math.max(Math.max(body.scrollHeight, documentElement.scrollHeight), Math.max(body.offsetHeight, documentElement.offsetHeight), Math.max(body.clientHeight, documentElement.clientHeight));
    return new Bounds(0, 0, width, height);
};

/*
 * css-line-break 2.1.0 <https://github.com/niklasvh/css-line-break#readme>
 * Copyright (c) 2022 Niklas von Hertzen <https://hertzen.com>
 * Released under MIT License
 */
var toCodePoints$1 = function (str) {
    var codePoints = [];
    var i = 0;
    var length = str.length;
    while (i < length) {
        var value = str.charCodeAt(i++);
        if (value >= 0xd800 && value <= 0xdbff && i < length) {
            var extra = str.charCodeAt(i++);
            if ((extra & 0xfc00) === 0xdc00) {
                codePoints.push(((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000);
            }
            else {
                codePoints.push(value);
                i--;
            }
        }
        else {
            codePoints.push(value);
        }
    }
    return codePoints;
};
var fromCodePoint$1 = function () {
    var codePoints = [];
    for (var _i = 0; _i < arguments.length; _i++) {
        codePoints[_i] = arguments[_i];
    }
    if (String.fromCodePoint) {
        return String.fromCodePoint.apply(String, codePoints);
    }
    var length = codePoints.length;
    if (!length) {
        return '';
    }
    var codeUnits = [];
    var index = -1;
    var result = '';
    while (++index < length) {
        var codePoint = codePoints[index];
        if (codePoint <= 0xffff) {
            codeUnits.push(codePoint);
        }
        else {
            codePoint -= 0x10000;
            codeUnits.push((codePoint >> 10) + 0xd800, (codePoint % 0x400) + 0xdc00);
        }
        if (index + 1 === length || codeUnits.length > 0x4000) {
            result += String.fromCharCode.apply(String, codeUnits);
            codeUnits.length = 0;
        }
    }
    return result;
};
var chars$2 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
// Use a lookup table to find the index.
var lookup$2 = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);
for (var i$2 = 0; i$2 < chars$2.length; i$2++) {
    lookup$2[chars$2.charCodeAt(i$2)] = i$2;
}

/*
 * utrie 1.0.2 <https://github.com/niklasvh/utrie>
 * Copyright (c) 2022 Niklas von Hertzen <https://hertzen.com>
 * Released under MIT License
 */
var chars$1$1 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
// Use a lookup table to find the index.
var lookup$1$1 = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);
for (var i$1$1 = 0; i$1$1 < chars$1$1.length; i$1$1++) {
    lookup$1$1[chars$1$1.charCodeAt(i$1$1)] = i$1$1;
}
var decode$1$1 = function (base64) {
    var bufferLength = base64.length * 0.75, len = base64.length, i, p = 0, encoded1, encoded2, encoded3, encoded4;
    if (base64[base64.length - 1] === '=') {
        bufferLength--;
        if (base64[base64.length - 2] === '=') {
            bufferLength--;
        }
    }
    var buffer = typeof ArrayBuffer !== 'undefined' &&
        typeof Uint8Array !== 'undefined' &&
        typeof Uint8Array.prototype.slice !== 'undefined'
        ? new ArrayBuffer(bufferLength)
        : new Array(bufferLength);
    var bytes = Array.isArray(buffer) ? buffer : new Uint8Array(buffer);
    for (i = 0; i < len; i += 4) {
        encoded1 = lookup$1$1[base64.charCodeAt(i)];
        encoded2 = lookup$1$1[base64.charCodeAt(i + 1)];
        encoded3 = lookup$1$1[base64.charCodeAt(i + 2)];
        encoded4 = lookup$1$1[base64.charCodeAt(i + 3)];
        bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
        bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
        bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
    }
    return buffer;
};
var polyUint16Array$1 = function (buffer) {
    var length = buffer.length;
    var bytes = [];
    for (var i = 0; i < length; i += 2) {
        bytes.push((buffer[i + 1] << 8) | buffer[i]);
    }
    return bytes;
};
var polyUint32Array$1 = function (buffer) {
    var length = buffer.length;
    var bytes = [];
    for (var i = 0; i < length; i += 4) {
        bytes.push((buffer[i + 3] << 24) | (buffer[i + 2] << 16) | (buffer[i + 1] << 8) | buffer[i]);
    }
    return bytes;
};

/** Shift size for getting the index-2 table offset. */
var UTRIE2_SHIFT_2$1 = 5;
/** Shift size for getting the index-1 table offset. */
var UTRIE2_SHIFT_1$1 = 6 + 5;
/**
 * Shift size for shifting left the index array values.
 * Increases possible data size with 16-bit index values at the cost
 * of compactability.
 * This requires data blocks to be aligned by UTRIE2_DATA_GRANULARITY.
 */
var UTRIE2_INDEX_SHIFT$1 = 2;
/**
 * Difference between the two shift sizes,
 * for getting an index-1 offset from an index-2 offset. 6=11-5
 */
var UTRIE2_SHIFT_1_2$1 = UTRIE2_SHIFT_1$1 - UTRIE2_SHIFT_2$1;
/**
 * The part of the index-2 table for U+D800..U+DBFF stores values for
 * lead surrogate code _units_ not code _points_.
 * Values for lead surrogate code _points_ are indexed with this portion of the table.
 * Length=32=0x20=0x400>>UTRIE2_SHIFT_2. (There are 1024=0x400 lead surrogates.)
 */
var UTRIE2_LSCP_INDEX_2_OFFSET$1 = 0x10000 >> UTRIE2_SHIFT_2$1;
/** Number of entries in a data block. 32=0x20 */
var UTRIE2_DATA_BLOCK_LENGTH$1 = 1 << UTRIE2_SHIFT_2$1;
/** Mask for getting the lower bits for the in-data-block offset. */
var UTRIE2_DATA_MASK$1 = UTRIE2_DATA_BLOCK_LENGTH$1 - 1;
var UTRIE2_LSCP_INDEX_2_LENGTH$1 = 0x400 >> UTRIE2_SHIFT_2$1;
/** Count the lengths of both BMP pieces. 2080=0x820 */
var UTRIE2_INDEX_2_BMP_LENGTH$1 = UTRIE2_LSCP_INDEX_2_OFFSET$1 + UTRIE2_LSCP_INDEX_2_LENGTH$1;
/**
 * The 2-byte UTF-8 version of the index-2 table follows at offset 2080=0x820.
 * Length 32=0x20 for lead bytes C0..DF, regardless of UTRIE2_SHIFT_2.
 */
var UTRIE2_UTF8_2B_INDEX_2_OFFSET$1 = UTRIE2_INDEX_2_BMP_LENGTH$1;
var UTRIE2_UTF8_2B_INDEX_2_LENGTH$1 = 0x800 >> 6; /* U+0800 is the first code point after 2-byte UTF-8 */
/**
 * The index-1 table, only used for supplementary code points, at offset 2112=0x840.
 * Variable length, for code points up to highStart, where the last single-value range starts.
 * Maximum length 512=0x200=0x100000>>UTRIE2_SHIFT_1.
 * (For 0x100000 supplementary code points U+10000..U+10ffff.)
 *
 * The part of the index-2 table for supplementary code points starts
 * after this index-1 table.
 *
 * Both the index-1 table and the following part of the index-2 table
 * are omitted completely if there is only BMP data.
 */
var UTRIE2_INDEX_1_OFFSET$1 = UTRIE2_UTF8_2B_INDEX_2_OFFSET$1 + UTRIE2_UTF8_2B_INDEX_2_LENGTH$1;
/**
 * Number of index-1 entries for the BMP. 32=0x20
 * This part of the index-1 table is omitted from the serialized form.
 */
var UTRIE2_OMITTED_BMP_INDEX_1_LENGTH$1 = 0x10000 >> UTRIE2_SHIFT_1$1;
/** Number of entries in an index-2 block. 64=0x40 */
var UTRIE2_INDEX_2_BLOCK_LENGTH$1 = 1 << UTRIE2_SHIFT_1_2$1;
/** Mask for getting the lower bits for the in-index-2-block offset. */
var UTRIE2_INDEX_2_MASK$1 = UTRIE2_INDEX_2_BLOCK_LENGTH$1 - 1;
var slice16$1 = function (view, start, end) {
    if (view.slice) {
        return view.slice(start, end);
    }
    return new Uint16Array(Array.prototype.slice.call(view, start, end));
};
var slice32$1 = function (view, start, end) {
    if (view.slice) {
        return view.slice(start, end);
    }
    return new Uint32Array(Array.prototype.slice.call(view, start, end));
};
var createTrieFromBase64$1 = function (base64, _byteLength) {
    var buffer = decode$1$1(base64);
    var view32 = Array.isArray(buffer) ? polyUint32Array$1(buffer) : new Uint32Array(buffer);
    var view16 = Array.isArray(buffer) ? polyUint16Array$1(buffer) : new Uint16Array(buffer);
    var headerLength = 24;
    var index = slice16$1(view16, headerLength / 2, view32[4] / 2);
    var data = view32[5] === 2
        ? slice16$1(view16, (headerLength + view32[4]) / 2)
        : slice32$1(view32, Math.ceil((headerLength + view32[4]) / 4));
    return new Trie$1(view32[0], view32[1], view32[2], view32[3], index, data);
};
var Trie$1 = /** @class */ (function () {
    function Trie(initialValue, errorValue, highStart, highValueIndex, index, data) {
        this.initialValue = initialValue;
        this.errorValue = errorValue;
        this.highStart = highStart;
        this.highValueIndex = highValueIndex;
        this.index = index;
        this.data = data;
    }
    /**
     * Get the value for a code point as stored in the Trie.
     *
     * @param codePoint the code point
     * @return the value
     */
    Trie.prototype.get = function (codePoint) {
        var ix;
        if (codePoint >= 0) {
            if (codePoint < 0x0d800 || (codePoint > 0x0dbff && codePoint <= 0x0ffff)) {
                // Ordinary BMP code point, excluding leading surrogates.
                // BMP uses a single level lookup.  BMP index starts at offset 0 in the Trie2 index.
                // 16 bit data is stored in the index array itself.
                ix = this.index[codePoint >> UTRIE2_SHIFT_2$1];
                ix = (ix << UTRIE2_INDEX_SHIFT$1) + (codePoint & UTRIE2_DATA_MASK$1);
                return this.data[ix];
            }
            if (codePoint <= 0xffff) {
                // Lead Surrogate Code Point.  A Separate index section is stored for
                // lead surrogate code units and code points.
                //   The main index has the code unit data.
                //   For this function, we need the code point data.
                // Note: this expression could be refactored for slightly improved efficiency, but
                //       surrogate code points will be so rare in practice that it's not worth it.
                ix = this.index[UTRIE2_LSCP_INDEX_2_OFFSET$1 + ((codePoint - 0xd800) >> UTRIE2_SHIFT_2$1)];
                ix = (ix << UTRIE2_INDEX_SHIFT$1) + (codePoint & UTRIE2_DATA_MASK$1);
                return this.data[ix];
            }
            if (codePoint < this.highStart) {
                // Supplemental code point, use two-level lookup.
                ix = UTRIE2_INDEX_1_OFFSET$1 - UTRIE2_OMITTED_BMP_INDEX_1_LENGTH$1 + (codePoint >> UTRIE2_SHIFT_1$1);
                ix = this.index[ix];
                ix += (codePoint >> UTRIE2_SHIFT_2$1) & UTRIE2_INDEX_2_MASK$1;
                ix = this.index[ix];
                ix = (ix << UTRIE2_INDEX_SHIFT$1) + (codePoint & UTRIE2_DATA_MASK$1);
                return this.data[ix];
            }
            if (codePoint <= 0x10ffff) {
                return this.data[this.highValueIndex];
            }
        }
        // Fall through.  The code point is outside of the legal range of 0..0x10ffff.
        return this.errorValue;
    };
    return Trie;
}());

/*
 * base64-arraybuffer 1.0.2 <https://github.com/niklasvh/base64-arraybuffer>
 * Copyright (c) 2022 Niklas von Hertzen <https://hertzen.com>
 * Released under MIT License
 */
var chars$3 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
// Use a lookup table to find the index.
var lookup$3 = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);
for (var i$3 = 0; i$3 < chars$3.length; i$3++) {
    lookup$3[chars$3.charCodeAt(i$3)] = i$3;
}

var base64$1 = ' ';

var LETTER_NUMBER_MODIFIER = 50;
// Non-tailorable Line Breaking Classes
var BK = 1; //  Cause a line break (after)
var CR$1 = 2; //  Cause a line break (after), except between CR and LF
var LF$1 = 3; //  Cause a line break (after)
var CM = 4; //  Prohibit a line break between the character and the preceding character
var NL = 5; //  Cause a line break (after)
var WJ = 7; //  Prohibit line breaks before and after
var ZW = 8; //  Provide a break opportunity
var GL = 9; //  Prohibit line breaks before and after
var SP = 10; // Enable indirect line breaks
var ZWJ$1 = 11; // Prohibit line breaks within joiner sequences
// Break Opportunities
var B2 = 12; //  Provide a line break opportunity before and after the character
var BA = 13; //  Generally provide a line break opportunity after the character
var BB = 14; //  Generally provide a line break opportunity before the character
var HY = 15; //  Provide a line break opportunity after the character, except in numeric context
var CB = 16; //   Provide a line break opportunity contingent on additional information
// Characters Prohibiting Certain Breaks
var CL = 17; //  Prohibit line breaks before
var CP = 18; //  Prohibit line breaks before
var EX = 19; //  Prohibit line breaks before
var IN = 20; //  Allow only indirect line breaks between pairs
var NS = 21; //  Allow only indirect line breaks before
var OP = 22; //  Prohibit line breaks after
var QU = 23; //  Act like they are both opening and closing
// Numeric Context
var IS = 24; //  Prevent breaks after any and before numeric
var NU = 25; //  Form numeric expressions for line breaking purposes
var PO = 26; //  Do not break following a numeric expression
var PR = 27; //  Do not break in front of a numeric expression
var SY = 28; //  Prevent a break before; and allow a break after
// Other Characters
var AI = 29; //  Act like AL when the resolvedEAW is N; otherwise; act as ID
var AL = 30; //  Are alphabetic characters or symbols that are used with alphabetic characters
var CJ = 31; //  Treat as NS or ID for strict or normal breaking.
var EB = 32; //  Do not break from following Emoji Modifier
var EM = 33; //  Do not break from preceding Emoji Base
var H2 = 34; //  Form Korean syllable blocks
var H3 = 35; //  Form Korean syllable blocks
var HL = 36; //  Do not break around a following hyphen; otherwise act as Alphabetic
var ID = 37; //  Break before or after; except in some numeric context
var JL = 38; //  Form Korean syllable blocks
var JV = 39; //  Form Korean syllable blocks
var JT = 40; //  Form Korean syllable blocks
var RI$1 = 41; //  Keep pairs together. For pairs; break before and after other classes
var SA = 42; //  Provide a line break opportunity contingent on additional, language-specific context analysis
var XX = 43; //  Have as yet unknown line breaking behavior or unassigned code positions
var ea_OP = [0x2329, 0xff08];
var BREAK_MANDATORY = '!';
var BREAK_NOT_ALLOWED$1 = '×';
var BREAK_ALLOWED$1 = '÷';
var UnicodeTrie$1 = createTrieFromBase64$1(base64$1);
var ALPHABETICS = [AL, HL];
var HARD_LINE_BREAKS = [BK, CR$1, LF$1, NL];
var SPACE$1 = [SP, ZW];
var PREFIX_POSTFIX = [PR, PO];
var LINE_BREAKS = HARD_LINE_BREAKS.concat(SPACE$1);
var KOREAN_SYLLABLE_BLOCK = [JL, JV, JT, H2, H3];
var HYPHEN = [HY, BA];
var codePointsToCharacterClasses = function (codePoints, lineBreak) {
    if (lineBreak === void 0) { lineBreak = 'strict'; }
    var types = [];
    var indices = [];
    var categories = [];
    codePoints.forEach(function (codePoint, index) {
        var classType = UnicodeTrie$1.get(codePoint);
        if (classType > LETTER_NUMBER_MODIFIER) {
            categories.push(true);
            classType -= LETTER_NUMBER_MODIFIER;
        }
        else {
            categories.push(false);
        }
        if (['normal', 'auto', 'loose'].indexOf(lineBreak) !== -1) {
            // U+2010, – U+2013, 〜 U+301C, ゠ U+30A0
            if ([0x2010, 0x2013, 0x301c, 0x30a0].indexOf(codePoint) !== -1) {
                indices.push(index);
                return types.push(CB);
            }
        }
        if (classType === CM || classType === ZWJ$1) {
            // LB10 Treat any remaining combining mark or ZWJ as AL.
            if (index === 0) {
                indices.push(index);
                return types.push(AL);
            }
            // LB9 Do not break a combining character sequence; treat it as if it has the line breaking class of
            // the base character in all of the following rules. Treat ZWJ as if it were CM.
            var prev = types[index - 1];
            if (LINE_BREAKS.indexOf(prev) === -1) {
                indices.push(indices[index - 1]);
                return types.push(prev);
            }
            indices.push(index);
            return types.push(AL);
        }
        indices.push(index);
        if (classType === CJ) {
            return types.push(lineBreak === 'strict' ? NS : ID);
        }
        if (classType === SA) {
            return types.push(AL);
        }
        if (classType === AI) {
            return types.push(AL);
        }
        // For supplementary characters, a useful default is to treat characters in the range 10000..1FFFD as AL
        // and characters in the ranges 20000..2FFFD and 30000..3FFFD as ID, until the implementation can be revised
        // to take into account the actual line breaking properties for these characters.
        if (classType === XX) {
            if ((codePoint >= 0x20000 && codePoint <= 0x2fffd) || (codePoint >= 0x30000 && codePoint <= 0x3fffd)) {
                return types.push(ID);
            }
            else {
                return types.push(AL);
            }
        }
        types.push(classType);
    });
    return [indices, types, categories];
};
var isAdjacentWithSpaceIgnored = function (a, b, currentIndex, classTypes) {
    var current = classTypes[currentIndex];
    if (Array.isArray(a) ? a.indexOf(current) !== -1 : a === current) {
        var i = currentIndex;
        while (i <= classTypes.length) {
            i++;
            var next = classTypes[i];
            if (next === b) {
                return true;
            }
            if (next !== SP) {
                break;
            }
        }
    }
    if (current === SP) {
        var i = currentIndex;
        while (i > 0) {
            i--;
            var prev = classTypes[i];
            if (Array.isArray(a) ? a.indexOf(prev) !== -1 : a === prev) {
                var n = currentIndex;
                while (n <= classTypes.length) {
                    n++;
                    var next = classTypes[n];
                    if (next === b) {
                        return true;
                    }
                    if (next !== SP) {
                        break;
                    }
                }
            }
            if (prev !== SP) {
                break;
            }
        }
    }
    return false;
};
var previousNonSpaceClassType = function (currentIndex, classTypes) {
    var i = currentIndex;
    while (i >= 0) {
        var type = classTypes[i];
        if (type === SP) {
            i--;
        }
        else {
            return type;
        }
    }
    return 0;
};
var _lineBreakAtIndex = function (codePoints, classTypes, indicies, index, forbiddenBreaks) {
    if (indicies[index] === 0) {
        return BREAK_NOT_ALLOWED$1;
    }
    var currentIndex = index - 1;
    if (Array.isArray(forbiddenBreaks) && forbiddenBreaks[currentIndex] === true) {
        return BREAK_NOT_ALLOWED$1;
    }
    var beforeIndex = currentIndex - 1;
    var afterIndex = currentIndex + 1;
    var current = classTypes[currentIndex];
    // LB4 Always break after hard line breaks.
    // LB5 Treat CR followed by LF, as well as CR, LF, and NL as hard line breaks.
    var before = beforeIndex >= 0 ? classTypes[beforeIndex] : 0;
    var next = classTypes[afterIndex];
    if (current === CR$1 && next === LF$1) {
        return BREAK_NOT_ALLOWED$1;
    }
    if (HARD_LINE_BREAKS.indexOf(current) !== -1) {
        return BREAK_MANDATORY;
    }
    // LB6 Do not break before hard line breaks.
    if (HARD_LINE_BREAKS.indexOf(next) !== -1) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB7 Do not break before spaces or zero width space.
    if (SPACE$1.indexOf(next) !== -1) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB8 Break before any character following a zero-width space, even if one or more spaces intervene.
    if (previousNonSpaceClassType(currentIndex, classTypes) === ZW) {
        return BREAK_ALLOWED$1;
    }
    // LB8a Do not break after a zero width joiner.
    if (UnicodeTrie$1.get(codePoints[currentIndex]) === ZWJ$1) {
        return BREAK_NOT_ALLOWED$1;
    }
    // zwj emojis
    if ((current === EB || current === EM) && UnicodeTrie$1.get(codePoints[afterIndex]) === ZWJ$1) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB11 Do not break before or after Word joiner and related characters.
    if (current === WJ || next === WJ) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB12 Do not break after NBSP and related characters.
    if (current === GL) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB12a Do not break before NBSP and related characters, except after spaces and hyphens.
    if ([SP, BA, HY].indexOf(current) === -1 && next === GL) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB13 Do not break before ‘]’ or ‘!’ or ‘;’ or ‘/’, even after spaces.
    if ([CL, CP, EX, IS, SY].indexOf(next) !== -1) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB14 Do not break after ‘[’, even after spaces.
    if (previousNonSpaceClassType(currentIndex, classTypes) === OP) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB15 Do not break within ‘”[’, even with intervening spaces.
    if (isAdjacentWithSpaceIgnored(QU, OP, currentIndex, classTypes)) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB16 Do not break between closing punctuation and a nonstarter (lb=NS), even with intervening spaces.
    if (isAdjacentWithSpaceIgnored([CL, CP], NS, currentIndex, classTypes)) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB17 Do not break within ‘——’, even with intervening spaces.
    if (isAdjacentWithSpaceIgnored(B2, B2, currentIndex, classTypes)) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB18 Break after spaces.
    if (current === SP) {
        return BREAK_ALLOWED$1;
    }
    // LB19 Do not break before or after quotation marks, such as ‘ ” ’.
    if (current === QU || next === QU) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB20 Break before and after unresolved CB.
    if (next === CB || current === CB) {
        return BREAK_ALLOWED$1;
    }
    // LB21 Do not break before hyphen-minus, other hyphens, fixed-width spaces, small kana, and other non-starters, or after acute accents.
    if ([BA, HY, NS].indexOf(next) !== -1 || current === BB) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB21a Don't break after Hebrew + Hyphen.
    if (before === HL && HYPHEN.indexOf(current) !== -1) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB21b Don’t break between Solidus and Hebrew letters.
    if (current === SY && next === HL) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB22 Do not break before ellipsis.
    if (next === IN) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB23 Do not break between digits and letters.
    if ((ALPHABETICS.indexOf(next) !== -1 && current === NU) || (ALPHABETICS.indexOf(current) !== -1 && next === NU)) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB23a Do not break between numeric prefixes and ideographs, or between ideographs and numeric postfixes.
    if ((current === PR && [ID, EB, EM].indexOf(next) !== -1) ||
        ([ID, EB, EM].indexOf(current) !== -1 && next === PO)) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB24 Do not break between numeric prefix/postfix and letters, or between letters and prefix/postfix.
    if ((ALPHABETICS.indexOf(current) !== -1 && PREFIX_POSTFIX.indexOf(next) !== -1) ||
        (PREFIX_POSTFIX.indexOf(current) !== -1 && ALPHABETICS.indexOf(next) !== -1)) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB25 Do not break between the following pairs of classes relevant to numbers:
    if (
    // (PR | PO) × ( OP | HY )? NU
    ([PR, PO].indexOf(current) !== -1 &&
        (next === NU || ([OP, HY].indexOf(next) !== -1 && classTypes[afterIndex + 1] === NU))) ||
        // ( OP | HY ) × NU
        ([OP, HY].indexOf(current) !== -1 && next === NU) ||
        // NU ×	(NU | SY | IS)
        (current === NU && [NU, SY, IS].indexOf(next) !== -1)) {
        return BREAK_NOT_ALLOWED$1;
    }
    // NU (NU | SY | IS)* × (NU | SY | IS | CL | CP)
    if ([NU, SY, IS, CL, CP].indexOf(next) !== -1) {
        var prevIndex = currentIndex;
        while (prevIndex >= 0) {
            var type = classTypes[prevIndex];
            if (type === NU) {
                return BREAK_NOT_ALLOWED$1;
            }
            else if ([SY, IS].indexOf(type) !== -1) {
                prevIndex--;
            }
            else {
                break;
            }
        }
    }
    // NU (NU | SY | IS)* (CL | CP)? × (PO | PR))
    if ([PR, PO].indexOf(next) !== -1) {
        var prevIndex = [CL, CP].indexOf(current) !== -1 ? beforeIndex : currentIndex;
        while (prevIndex >= 0) {
            var type = classTypes[prevIndex];
            if (type === NU) {
                return BREAK_NOT_ALLOWED$1;
            }
            else if ([SY, IS].indexOf(type) !== -1) {
                prevIndex--;
            }
            else {
                break;
            }
        }
    }
    // LB26 Do not break a Korean syllable.
    if ((JL === current && [JL, JV, H2, H3].indexOf(next) !== -1) ||
        ([JV, H2].indexOf(current) !== -1 && [JV, JT].indexOf(next) !== -1) ||
        ([JT, H3].indexOf(current) !== -1 && next === JT)) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB27 Treat a Korean Syllable Block the same as ID.
    if ((KOREAN_SYLLABLE_BLOCK.indexOf(current) !== -1 && [IN, PO].indexOf(next) !== -1) ||
        (KOREAN_SYLLABLE_BLOCK.indexOf(next) !== -1 && current === PR)) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB28 Do not break between alphabetics (“at”).
    if (ALPHABETICS.indexOf(current) !== -1 && ALPHABETICS.indexOf(next) !== -1) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB29 Do not break between numeric punctuation and alphabetics (“e.g.”).
    if (current === IS && ALPHABETICS.indexOf(next) !== -1) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB30 Do not break between letters, numbers, or ordinary symbols and opening or closing parentheses.
    if ((ALPHABETICS.concat(NU).indexOf(current) !== -1 &&
        next === OP &&
        ea_OP.indexOf(codePoints[afterIndex]) === -1) ||
        (ALPHABETICS.concat(NU).indexOf(next) !== -1 && current === CP)) {
        return BREAK_NOT_ALLOWED$1;
    }
    // LB30a Break between two regional indicator symbols if and only if there are an even number of regional
    // indicators preceding the position of the break.
    if (current === RI$1 && next === RI$1) {
        var i = indicies[currentIndex];
        var count = 1;
        while (i > 0) {
            i--;
            if (classTypes[i] === RI$1) {
                count++;
            }
            else {
                break;
            }
        }
        if (count % 2 !== 0) {
            return BREAK_NOT_ALLOWED$1;
        }
    }
    // LB30b Do not break between an emoji base and an emoji modifier.
    if (current === EB && next === EM) {
        return BREAK_NOT_ALLOWED$1;
    }
    return BREAK_ALLOWED$1;
};
var cssFormattedClasses = function (codePoints, options) {
    if (!options) {
        options = { lineBreak: 'normal', wordBreak: 'normal' };
    }
    var _a = codePointsToCharacterClasses(codePoints, options.lineBreak), indicies = _a[0], classTypes = _a[1], isLetterNumber = _a[2];
    if (options.wordBreak === 'break-all' || options.wordBreak === 'break-word') {
        classTypes = classTypes.map(function (type) { return ([NU, AL, SA].indexOf(type) !== -1 ? ID : type); });
    }
    var forbiddenBreakpoints = options.wordBreak === 'keep-all'
        ? isLetterNumber.map(function (letterNumber, i) {
            return letterNumber && codePoints[i] >= 0x4e00 && codePoints[i] <= 0x9fff;
        })
        : undefined;
    return [indicies, classTypes, forbiddenBreakpoints];
};
var Break = /** @class */ (function () {
    function Break(codePoints, lineBreak, start, end) {
        this.codePoints = codePoints;
        this.required = lineBreak === BREAK_MANDATORY;
        this.start = start;
        this.end = end;
    }
    Break.prototype.slice = function () {
        return fromCodePoint$1.apply(void 0, this.codePoints.slice(this.start, this.end));
    };
    return Break;
}());
var LineBreaker = function (str, options) {
    var codePoints = toCodePoints$1(str);
    var _a = cssFormattedClasses(codePoints, options), indicies = _a[0], classTypes = _a[1], forbiddenBreakpoints = _a[2];
    var length = codePoints.length;
    var lastEnd = 0;
    var nextIndex = 0;
    return {
        next: function () {
            if (nextIndex >= length) {
                return { done: true, value: null };
            }
            var lineBreak = BREAK_NOT_ALLOWED$1;
            while (nextIndex < length &&
                (lineBreak = _lineBreakAtIndex(codePoints, classTypes, indicies, ++nextIndex, forbiddenBreakpoints)) ===
                    BREAK_NOT_ALLOWED$1) { }
            if (lineBreak !== BREAK_NOT_ALLOWED$1 || nextIndex === length) {
                var value = new Break(codePoints, lineBreak, lastEnd, nextIndex);
                lastEnd = nextIndex;
                return { value: value, done: false };
            }
            return { done: true, value: null };
        },
    };
};

// https://www.w3.org/TR/css-syntax-3
var FLAG_UNRESTRICTED = 1 << 0;
var FLAG_ID = 1 << 1;
var FLAG_INTEGER = 1 << 2;
var FLAG_NUMBER = 1 << 3;
var LINE_FEED = 0x000a;
var SOLIDUS = 0x002f;
var REVERSE_SOLIDUS = 0x005c;
var CHARACTER_TABULATION = 0x0009;
var SPACE = 0x0020;
var QUOTATION_MARK = 0x0022;
var EQUALS_SIGN = 0x003d;
var NUMBER_SIGN = 0x0023;
var DOLLAR_SIGN = 0x0024;
var PERCENTAGE_SIGN = 0x0025;
var APOSTROPHE = 0x0027;
var LEFT_PARENTHESIS = 0x0028;
var RIGHT_PARENTHESIS = 0x0029;
var LOW_LINE = 0x005f;
var HYPHEN_MINUS = 0x002d;
var EXCLAMATION_MARK = 0x0021;
var LESS_THAN_SIGN = 0x003c;
var GREATER_THAN_SIGN = 0x003e;
var COMMERCIAL_AT = 0x0040;
var LEFT_SQUARE_BRACKET = 0x005b;
var RIGHT_SQUARE_BRACKET = 0x005d;
var CIRCUMFLEX_ACCENT = 0x003d;
var LEFT_CURLY_BRACKET = 0x007b;
var QUESTION_MARK = 0x003f;
var RIGHT_CURLY_BRACKET = 0x007d;
var VERTICAL_LINE = 0x007c;
var TILDE = 0x007e;
var CONTROL = 0x0080;
var REPLACEMENT_CHARACTER = 0xfffd;
var ASTERISK = 0x002a;
var PLUS_SIGN = 0x002b;
var COMMA = 0x002c;
var COLON = 0x003a;
var SEMICOLON = 0x003b;
var FULL_STOP = 0x002e;
var NULL = 0x0000;
var BACKSPACE = 0x0008;
var LINE_TABULATION = 0x000b;
var SHIFT_OUT = 0x000e;
var INFORMATION_SEPARATOR_ONE = 0x001f;
var DELETE = 0x007f;
var EOF = -1;
var ZERO$1 = 0x0030;
var a = 0x0061;
var e = 0x0065;
var f = 0x0066;
var u = 0x0075;
var z = 0x007a;
var A = 0x0041;
var E = 0x0045;
var F = 0x0046;
var U = 0x0055;
var Z = 0x005a;
var isDigit = function (codePoint) { return codePoint >= ZERO$1 && codePoint <= 0x0039; };
var isSurrogateCodePoint = function (codePoint) { return codePoint >= 0xd800 && codePoint <= 0xdfff; };
var isHex = function (codePoint) {
    return isDigit(codePoint) || (codePoint >= A && codePoint <= F) || (codePoint >= a && codePoint <= f);
};
var isLowerCaseLetter = function (codePoint) { return codePoint >= a && codePoint <= z; };
var isUpperCaseLetter = function (codePoint) { return codePoint >= A && codePoint <= Z; };
var isLetter = function (codePoint) { return isLowerCaseLetter(codePoint) || isUpperCaseLetter(codePoint); };
var isNonASCIICodePoint = function (codePoint) { return codePoint >= CONTROL; };
var isWhiteSpace = function (codePoint) {
    return codePoint === LINE_FEED || codePoint === CHARACTER_TABULATION || codePoint === SPACE;
};
var isNameStartCodePoint = function (codePoint) {
    return isLetter(codePoint) || isNonASCIICodePoint(codePoint) || codePoint === LOW_LINE;
};
var isNameCodePoint = function (codePoint) {
    return isNameStartCodePoint(codePoint) || isDigit(codePoint) || codePoint === HYPHEN_MINUS;
};
var isNonPrintableCodePoint = function (codePoint) {
    return ((codePoint >= NULL && codePoint <= BACKSPACE) ||
        codePoint === LINE_TABULATION ||
        (codePoint >= SHIFT_OUT && codePoint <= INFORMATION_SEPARATOR_ONE) ||
        codePoint === DELETE);
};
var isValidEscape = function (c1, c2) {
    if (c1 !== REVERSE_SOLIDUS) {
        return false;
    }
    return c2 !== LINE_FEED;
};
var isIdentifierStart = function (c1, c2, c3) {
    if (c1 === HYPHEN_MINUS) {
        return isNameStartCodePoint(c2) || isValidEscape(c2, c3);
    }
    else if (isNameStartCodePoint(c1)) {
        return true;
    }
    else if (c1 === REVERSE_SOLIDUS && isValidEscape(c1, c2)) {
        return true;
    }
    return false;
};
var isNumberStart = function (c1, c2, c3) {
    if (c1 === PLUS_SIGN || c1 === HYPHEN_MINUS) {
        if (isDigit(c2)) {
            return true;
        }
        return c2 === FULL_STOP && isDigit(c3);
    }
    if (c1 === FULL_STOP) {
        return isDigit(c2);
    }
    return isDigit(c1);
};
var stringToNumber = function (codePoints) {
    var c = 0;
    var sign = 1;
    if (codePoints[c] === PLUS_SIGN || codePoints[c] === HYPHEN_MINUS) {
        if (codePoints[c] === HYPHEN_MINUS) {
            sign = -1;
        }
        c++;
    }
    var integers = [];
    while (isDigit(codePoints[c])) {
        integers.push(codePoints[c++]);
    }
    var int = integers.length ? parseInt(fromCodePoint$1.apply(void 0, integers), 10) : 0;
    if (codePoints[c] === FULL_STOP) {
        c++;
    }
    var fraction = [];
    while (isDigit(codePoints[c])) {
        fraction.push(codePoints[c++]);
    }
    var fracd = fraction.length;
    var frac = fracd ? parseInt(fromCodePoint$1.apply(void 0, fraction), 10) : 0;
    if (codePoints[c] === E || codePoints[c] === e) {
        c++;
    }
    var expsign = 1;
    if (codePoints[c] === PLUS_SIGN || codePoints[c] === HYPHEN_MINUS) {
        if (codePoints[c] === HYPHEN_MINUS) {
            expsign = -1;
        }
        c++;
    }
    var exponent = [];
    while (isDigit(codePoints[c])) {
        exponent.push(codePoints[c++]);
    }
    var exp = exponent.length ? parseInt(fromCodePoint$1.apply(void 0, exponent), 10) : 0;
    return sign * (int + frac * Math.pow(10, -fracd)) * Math.pow(10, expsign * exp);
};
var LEFT_PARENTHESIS_TOKEN = {
    type: 2 /* LEFT_PARENTHESIS_TOKEN */
};
var RIGHT_PARENTHESIS_TOKEN = {
    type: 3 /* RIGHT_PARENTHESIS_TOKEN */
};
var COMMA_TOKEN = { type: 4 /* COMMA_TOKEN */ };
var SUFFIX_MATCH_TOKEN = { type: 13 /* SUFFIX_MATCH_TOKEN */ };
var PREFIX_MATCH_TOKEN = { type: 8 /* PREFIX_MATCH_TOKEN */ };
var COLUMN_TOKEN = { type: 21 /* COLUMN_TOKEN */ };
var DASH_MATCH_TOKEN = { type: 9 /* DASH_MATCH_TOKEN */ };
var INCLUDE_MATCH_TOKEN = { type: 10 /* INCLUDE_MATCH_TOKEN */ };
var LEFT_CURLY_BRACKET_TOKEN = {
    type: 11 /* LEFT_CURLY_BRACKET_TOKEN */
};
var RIGHT_CURLY_BRACKET_TOKEN = {
    type: 12 /* RIGHT_CURLY_BRACKET_TOKEN */
};
var SUBSTRING_MATCH_TOKEN = { type: 14 /* SUBSTRING_MATCH_TOKEN */ };
var BAD_URL_TOKEN = { type: 23 /* BAD_URL_TOKEN */ };
var BAD_STRING_TOKEN = { type: 1 /* BAD_STRING_TOKEN */ };
var CDO_TOKEN = { type: 25 /* CDO_TOKEN */ };
var CDC_TOKEN = { type: 24 /* CDC_TOKEN */ };
var COLON_TOKEN = { type: 26 /* COLON_TOKEN */ };
var SEMICOLON_TOKEN = { type: 27 /* SEMICOLON_TOKEN */ };
var LEFT_SQUARE_BRACKET_TOKEN = {
    type: 28 /* LEFT_SQUARE_BRACKET_TOKEN */
};
var RIGHT_SQUARE_BRACKET_TOKEN = {
    type: 29 /* RIGHT_SQUARE_BRACKET_TOKEN */
};
var WHITESPACE_TOKEN = { type: 31 /* WHITESPACE_TOKEN */ };
var EOF_TOKEN = { type: 32 /* EOF_TOKEN */ };
var Tokenizer = /** @class */ (function () {
    function Tokenizer() {
        this._value = [];
    }
    Tokenizer.prototype.write = function (chunk) {
        this._value = this._value.concat(toCodePoints$1(chunk));
    };
    Tokenizer.prototype.read = function () {
        var tokens = [];
        var token = this.consumeToken();
        while (token !== EOF_TOKEN) {
            tokens.push(token);
            token = this.consumeToken();
        }
        return tokens;
    };
    Tokenizer.prototype.consumeToken = function () {
        var codePoint = this.consumeCodePoint();
        switch (codePoint) {
            case QUOTATION_MARK:
                return this.consumeStringToken(QUOTATION_MARK);
            case NUMBER_SIGN:
                var c1 = this.peekCodePoint(0);
                var c2 = this.peekCodePoint(1);
                var c3 = this.peekCodePoint(2);
                if (isNameCodePoint(c1) || isValidEscape(c2, c3)) {
                    var flags = isIdentifierStart(c1, c2, c3) ? FLAG_ID : FLAG_UNRESTRICTED;
                    var value = this.consumeName();
                    return { type: 5 /* HASH_TOKEN */, value: value, flags: flags };
                }
                break;
            case DOLLAR_SIGN:
                if (this.peekCodePoint(0) === EQUALS_SIGN) {
                    this.consumeCodePoint();
                    return SUFFIX_MATCH_TOKEN;
                }
                break;
            case APOSTROPHE:
                return this.consumeStringToken(APOSTROPHE);
            case LEFT_PARENTHESIS:
                return LEFT_PARENTHESIS_TOKEN;
            case RIGHT_PARENTHESIS:
                return RIGHT_PARENTHESIS_TOKEN;
            case ASTERISK:
                if (this.peekCodePoint(0) === EQUALS_SIGN) {
                    this.consumeCodePoint();
                    return SUBSTRING_MATCH_TOKEN;
                }
                break;
            case PLUS_SIGN:
                if (isNumberStart(codePoint, this.peekCodePoint(0), this.peekCodePoint(1))) {
                    this.reconsumeCodePoint(codePoint);
                    return this.consumeNumericToken();
                }
                break;
            case COMMA:
                return COMMA_TOKEN;
            case HYPHEN_MINUS:
                var e1 = codePoint;
                var e2 = this.peekCodePoint(0);
                var e3 = this.peekCodePoint(1);
                if (isNumberStart(e1, e2, e3)) {
                    this.reconsumeCodePoint(codePoint);
                    return this.consumeNumericToken();
                }
                if (isIdentifierStart(e1, e2, e3)) {
                    this.reconsumeCodePoint(codePoint);
                    return this.consumeIdentLikeToken();
                }
                if (e2 === HYPHEN_MINUS && e3 === GREATER_THAN_SIGN) {
                    this.consumeCodePoint();
                    this.consumeCodePoint();
                    return CDC_TOKEN;
                }
                break;
            case FULL_STOP:
                if (isNumberStart(codePoint, this.peekCodePoint(0), this.peekCodePoint(1))) {
                    this.reconsumeCodePoint(codePoint);
                    return this.consumeNumericToken();
                }
                break;
            case SOLIDUS:
                if (this.peekCodePoint(0) === ASTERISK) {
                    this.consumeCodePoint();
                    while (true) {
                        var c = this.consumeCodePoint();
                        if (c === ASTERISK) {
                            c = this.consumeCodePoint();
                            if (c === SOLIDUS) {
                                return this.consumeToken();
                            }
                        }
                        if (c === EOF) {
                            return this.consumeToken();
                        }
                    }
                }
                break;
            case COLON:
                return COLON_TOKEN;
            case SEMICOLON:
                return SEMICOLON_TOKEN;
            case LESS_THAN_SIGN:
                if (this.peekCodePoint(0) === EXCLAMATION_MARK &&
                    this.peekCodePoint(1) === HYPHEN_MINUS &&
                    this.peekCodePoint(2) === HYPHEN_MINUS) {
                    this.consumeCodePoint();
                    this.consumeCodePoint();
                    return CDO_TOKEN;
                }
                break;
            case COMMERCIAL_AT:
                var a1 = this.peekCodePoint(0);
                var a2 = this.peekCodePoint(1);
                var a3 = this.peekCodePoint(2);
                if (isIdentifierStart(a1, a2, a3)) {
                    var value = this.consumeName();
                    return { type: 7 /* AT_KEYWORD_TOKEN */, value: value };
                }
                break;
            case LEFT_SQUARE_BRACKET:
                return LEFT_SQUARE_BRACKET_TOKEN;
            case REVERSE_SOLIDUS:
                if (isValidEscape(codePoint, this.peekCodePoint(0))) {
                    this.reconsumeCodePoint(codePoint);
                    return this.consumeIdentLikeToken();
                }
                break;
            case RIGHT_SQUARE_BRACKET:
                return RIGHT_SQUARE_BRACKET_TOKEN;
            case CIRCUMFLEX_ACCENT:
                if (this.peekCodePoint(0) === EQUALS_SIGN) {
                    this.consumeCodePoint();
                    return PREFIX_MATCH_TOKEN;
                }
                break;
            case LEFT_CURLY_BRACKET:
                return LEFT_CURLY_BRACKET_TOKEN;
            case RIGHT_CURLY_BRACKET:
                return RIGHT_CURLY_BRACKET_TOKEN;
            case u:
            case U:
                var u1 = this.peekCodePoint(0);
                var u2 = this.peekCodePoint(1);
                if (u1 === PLUS_SIGN && (isHex(u2) || u2 === QUESTION_MARK)) {
                    this.consumeCodePoint();
                    this.consumeUnicodeRangeToken();
                }
                this.reconsumeCodePoint(codePoint);
                return this.consumeIdentLikeToken();
            case VERTICAL_LINE:
                if (this.peekCodePoint(0) === EQUALS_SIGN) {
                    this.consumeCodePoint();
                    return DASH_MATCH_TOKEN;
                }
                if (this.peekCodePoint(0) === VERTICAL_LINE) {
                    this.consumeCodePoint();
                    return COLUMN_TOKEN;
                }
                break;
            case TILDE:
                if (this.peekCodePoint(0) === EQUALS_SIGN) {
                    this.consumeCodePoint();
                    return INCLUDE_MATCH_TOKEN;
                }
                break;
            case EOF:
                return EOF_TOKEN;
        }
        if (isWhiteSpace(codePoint)) {
            this.consumeWhiteSpace();
            return WHITESPACE_TOKEN;
        }
        if (isDigit(codePoint)) {
            this.reconsumeCodePoint(codePoint);
            return this.consumeNumericToken();
        }
        if (isNameStartCodePoint(codePoint)) {
            this.reconsumeCodePoint(codePoint);
            return this.consumeIdentLikeToken();
        }
        return { type: 6 /* DELIM_TOKEN */, value: fromCodePoint$1(codePoint) };
    };
    Tokenizer.prototype.consumeCodePoint = function () {
        var value = this._value.shift();
        return typeof value === 'undefined' ? -1 : value;
    };
    Tokenizer.prototype.reconsumeCodePoint = function (codePoint) {
        this._value.unshift(codePoint);
    };
    Tokenizer.prototype.peekCodePoint = function (delta) {
        if (delta >= this._value.length) {
            return -1;
        }
        return this._value[delta];
    };
    Tokenizer.prototype.consumeUnicodeRangeToken = function () {
        var digits = [];
        var codePoint = this.consumeCodePoint();
        while (isHex(codePoint) && digits.length < 6) {
            digits.push(codePoint);
            codePoint = this.consumeCodePoint();
        }
        var questionMarks = false;
        while (codePoint === QUESTION_MARK && digits.length < 6) {
            digits.push(codePoint);
            codePoint = this.consumeCodePoint();
            questionMarks = true;
        }
        if (questionMarks) {
            var start_1 = parseInt(fromCodePoint$1.apply(void 0, digits.map(function (digit) { return (digit === QUESTION_MARK ? ZERO$1 : digit); })), 16);
            var end = parseInt(fromCodePoint$1.apply(void 0, digits.map(function (digit) { return (digit === QUESTION_MARK ? F : digit); })), 16);
            return { type: 30 /* UNICODE_RANGE_TOKEN */, start: start_1, end: end };
        }
        var start = parseInt(fromCodePoint$1.apply(void 0, digits), 16);
        if (this.peekCodePoint(0) === HYPHEN_MINUS && isHex(this.peekCodePoint(1))) {
            this.consumeCodePoint();
            codePoint = this.consumeCodePoint();
            var endDigits = [];
            while (isHex(codePoint) && endDigits.length < 6) {
                endDigits.push(codePoint);
                codePoint = this.consumeCodePoint();
            }
            var end = parseInt(fromCodePoint$1.apply(void 0, endDigits), 16);
            return { type: 30 /* UNICODE_RANGE_TOKEN */, start: start, end: end };
        }
        else {
            return { type: 30 /* UNICODE_RANGE_TOKEN */, start: start, end: start };
        }
    };
    Tokenizer.prototype.consumeIdentLikeToken = function () {
        var value = this.consumeName();
        if (value.toLowerCase() === 'url' && this.peekCodePoint(0) === LEFT_PARENTHESIS) {
            this.consumeCodePoint();
            return this.consumeUrlToken();
        }
        else if (this.peekCodePoint(0) === LEFT_PARENTHESIS) {
            this.consumeCodePoint();
            return { type: 19 /* FUNCTION_TOKEN */, value: value };
        }
        return { type: 20 /* IDENT_TOKEN */, value: value };
    };
    Tokenizer.prototype.consumeUrlToken = function () {
        var value = [];
        this.consumeWhiteSpace();
        if (this.peekCodePoint(0) === EOF) {
            return { type: 22 /* URL_TOKEN */, value: '' };
        }
        var next = this.peekCodePoint(0);
        if (next === APOSTROPHE || next === QUOTATION_MARK) {
            var stringToken = this.consumeStringToken(this.consumeCodePoint());
            if (stringToken.type === 0 /* STRING_TOKEN */) {
                this.consumeWhiteSpace();
                if (this.peekCodePoint(0) === EOF || this.peekCodePoint(0) === RIGHT_PARENTHESIS) {
                    this.consumeCodePoint();
                    return { type: 22 /* URL_TOKEN */, value: stringToken.value };
                }
            }
            this.consumeBadUrlRemnants();
            return BAD_URL_TOKEN;
        }
        while (true) {
            var codePoint = this.consumeCodePoint();
            if (codePoint === EOF || codePoint === RIGHT_PARENTHESIS) {
                return { type: 22 /* URL_TOKEN */, value: fromCodePoint$1.apply(void 0, value) };
            }
            else if (isWhiteSpace(codePoint)) {
                this.consumeWhiteSpace();
                if (this.peekCodePoint(0) === EOF || this.peekCodePoint(0) === RIGHT_PARENTHESIS) {
                    this.consumeCodePoint();
                    return { type: 22 /* URL_TOKEN */, value: fromCodePoint$1.apply(void 0, value) };
                }
                this.consumeBadUrlRemnants();
                return BAD_URL_TOKEN;
            }
            else if (codePoint === QUOTATION_MARK ||
                codePoint === APOSTROPHE ||
                codePoint === LEFT_PARENTHESIS ||
                isNonPrintableCodePoint(codePoint)) {
                this.consumeBadUrlRemnants();
                return BAD_URL_TOKEN;
            }
            else if (codePoint === REVERSE_SOLIDUS) {
                if (isValidEscape(codePoint, this.peekCodePoint(0))) {
                    value.push(this.consumeEscapedCodePoint());
                }
                else {
                    this.consumeBadUrlRemnants();
                    return BAD_URL_TOKEN;
                }
            }
            else {
                value.push(codePoint);
            }
        }
    };
    Tokenizer.prototype.consumeWhiteSpace = function () {
        while (isWhiteSpace(this.peekCodePoint(0))) {
            this.consumeCodePoint();
        }
    };
    Tokenizer.prototype.consumeBadUrlRemnants = function () {
        while (true) {
            var codePoint = this.consumeCodePoint();
            if (codePoint === RIGHT_PARENTHESIS || codePoint === EOF) {
                return;
            }
            if (isValidEscape(codePoint, this.peekCodePoint(0))) {
                this.consumeEscapedCodePoint();
            }
        }
    };
    Tokenizer.prototype.consumeStringSlice = function (count) {
        var SLICE_STACK_SIZE = 50000;
        var value = '';
        while (count > 0) {
            var amount = Math.min(SLICE_STACK_SIZE, count);
            value += fromCodePoint$1.apply(void 0, this._value.splice(0, amount));
            count -= amount;
        }
        this._value.shift();
        return value;
    };
    Tokenizer.prototype.consumeStringToken = function (endingCodePoint) {
        var value = '';
        var i = 0;
        do {
            var codePoint = this._value[i];
            if (codePoint === EOF || codePoint === undefined || codePoint === endingCodePoint) {
                value += this.consumeStringSlice(i);
                return { type: 0 /* STRING_TOKEN */, value: value };
            }
            if (codePoint === LINE_FEED) {
                this._value.splice(0, i);
                return BAD_STRING_TOKEN;
            }
            if (codePoint === REVERSE_SOLIDUS) {
                var next = this._value[i + 1];
                if (next !== EOF && next !== undefined) {
                    if (next === LINE_FEED) {
                        value += this.consumeStringSlice(i);
                        i = -1;
                        this._value.shift();
                    }
                    else if (isValidEscape(codePoint, next)) {
                        value += this.consumeStringSlice(i);
                        value += fromCodePoint$1(this.consumeEscapedCodePoint());
                        i = -1;
                    }
                }
            }
            i++;
        } while (true);
    };
    Tokenizer.prototype.consumeNumber = function () {
        var repr = [];
        var type = FLAG_INTEGER;
        var c1 = this.peekCodePoint(0);
        if (c1 === PLUS_SIGN || c1 === HYPHEN_MINUS) {
            repr.push(this.consumeCodePoint());
        }
        while (isDigit(this.peekCodePoint(0))) {
            repr.push(this.consumeCodePoint());
        }
        c1 = this.peekCodePoint(0);
        var c2 = this.peekCodePoint(1);
        if (c1 === FULL_STOP && isDigit(c2)) {
            repr.push(this.consumeCodePoint(), this.consumeCodePoint());
            type = FLAG_NUMBER;
            while (isDigit(this.peekCodePoint(0))) {
                repr.push(this.consumeCodePoint());
            }
        }
        c1 = this.peekCodePoint(0);
        c2 = this.peekCodePoint(1);
        var c3 = this.peekCodePoint(2);
        if ((c1 === E || c1 === e) && (((c2 === PLUS_SIGN || c2 === HYPHEN_MINUS) && isDigit(c3)) || isDigit(c2))) {
            repr.push(this.consumeCodePoint(), this.consumeCodePoint());
            type = FLAG_NUMBER;
            while (isDigit(this.peekCodePoint(0))) {
                repr.push(this.consumeCodePoint());
            }
        }
        return [stringToNumber(repr), type];
    };
    Tokenizer.prototype.consumeNumericToken = function () {
        var _a = this.consumeNumber(), number = _a[0], flags = _a[1];
        var c1 = this.peekCodePoint(0);
        var c2 = this.peekCodePoint(1);
        var c3 = this.peekCodePoint(2);
        if (isIdentifierStart(c1, c2, c3)) {
            var unit = this.consumeName();
            return { type: 15 /* DIMENSION_TOKEN */, number: number, flags: flags, unit: unit };
        }
        if (c1 === PERCENTAGE_SIGN) {
            this.consumeCodePoint();
            return { type: 16 /* PERCENTAGE_TOKEN */, number: number, flags: flags };
        }
        return { type: 17 /* NUMBER_TOKEN */, number: number, flags: flags };
    };
    Tokenizer.prototype.consumeEscapedCodePoint = function () {
        var codePoint = this.consumeCodePoint();
        if (isHex(codePoint)) {
            var hex = fromCodePoint$1(codePoint);
            while (isHex(this.peekCodePoint(0)) && hex.length < 6) {
                hex += fromCodePoint$1(this.consumeCodePoint());
            }
            if (isWhiteSpace(this.peekCodePoint(0))) {
                this.consumeCodePoint();
            }
            var hexCodePoint = parseInt(hex, 16);
            if (hexCodePoint === 0 || isSurrogateCodePoint(hexCodePoint) || hexCodePoint > 0x10ffff) {
                return REPLACEMENT_CHARACTER;
            }
            return hexCodePoint;
        }
        if (codePoint === EOF) {
            return REPLACEMENT_CHARACTER;
        }
        return codePoint;
    };
    Tokenizer.prototype.consumeName = function () {
        var result = '';
        while (true) {
            var codePoint = this.consumeCodePoint();
            if (isNameCodePoint(codePoint)) {
                result += fromCodePoint$1(codePoint);
            }
            else if (isValidEscape(codePoint, this.peekCodePoint(0))) {
                result += fromCodePoint$1(this.consumeEscapedCodePoint());
            }
            else {
                this.reconsumeCodePoint(codePoint);
                return result;
            }
        }
    };
    return Tokenizer;
}());

var Parser = /** @class */ (function () {
    function Parser(tokens) {
        this._tokens = tokens;
    }
    Parser.create = function (value) {
        var tokenizer = new Tokenizer();
        tokenizer.write(value);
        return new Parser(tokenizer.read());
    };
    Parser.parseValue = function (value) {
        return Parser.create(value).parseComponentValue();
    };
    Parser.parseValues = function (value) {
        return Parser.create(value).parseComponentValues();
    };
    Parser.prototype.parseComponentValue = function () {
        var token = this.consumeToken();
        while (token.type === 31 /* WHITESPACE_TOKEN */) {
            token = this.consumeToken();
        }
        if (token.type === 32 /* EOF_TOKEN */) {
            throw new SyntaxError("Error parsing CSS component value, unexpected EOF");
        }
        this.reconsumeToken(token);
        var value = this.consumeComponentValue();
        do {
            token = this.consumeToken();
        } while (token.type === 31 /* WHITESPACE_TOKEN */);
        if (token.type === 32 /* EOF_TOKEN */) {
            return value;
        }
        throw new SyntaxError("Error parsing CSS component value, multiple values found when expecting only one");
    };
    Parser.prototype.parseComponentValues = function () {
        var values = [];
        while (true) {
            var value = this.consumeComponentValue();
            if (value.type === 32 /* EOF_TOKEN */) {
                return values;
            }
            values.push(value);
            values.push();
        }
    };
    Parser.prototype.consumeComponentValue = function () {
        var token = this.consumeToken();
        switch (token.type) {
            case 11 /* LEFT_CURLY_BRACKET_TOKEN */:
            case 28 /* LEFT_SQUARE_BRACKET_TOKEN */:
            case 2 /* LEFT_PARENTHESIS_TOKEN */:
                return this.consumeSimpleBlock(token.type);
            case 19 /* FUNCTION_TOKEN */:
                return this.consumeFunction(token);
        }
        return token;
    };
    Parser.prototype.consumeSimpleBlock = function (type) {
        var block = { type: type, values: [] };
        var token = this.consumeToken();
        while (true) {
            if (token.type === 32 /* EOF_TOKEN */ || isEndingTokenFor(token, type)) {
                return block;
            }
            this.reconsumeToken(token);
            block.values.push(this.consumeComponentValue());
            token = this.consumeToken();
        }
    };
    Parser.prototype.consumeFunction = function (functionToken) {
        var cssFunction = {
            name: functionToken.value,
            values: [],
            type: 18 /* FUNCTION */
        };
        while (true) {
            var token = this.consumeToken();
            if (token.type === 32 /* EOF_TOKEN */ || token.type === 3 /* RIGHT_PARENTHESIS_TOKEN */) {
                return cssFunction;
            }
            this.reconsumeToken(token);
            cssFunction.values.push(this.consumeComponentValue());
        }
    };
    Parser.prototype.consumeToken = function () {
        var token = this._tokens.shift();
        return typeof token === 'undefined' ? EOF_TOKEN : token;
    };
    Parser.prototype.reconsumeToken = function (token) {
        this._tokens.unshift(token);
    };
    return Parser;
}());
var isDimensionToken = function (token) { return token.type === 15 /* DIMENSION_TOKEN */; };
var isNumberToken = function (token) { return token.type === 17 /* NUMBER_TOKEN */; };
var isIdentToken = function (token) { return token.type === 20 /* IDENT_TOKEN */; };
var isStringToken = function (token) { return token.type === 0 /* STRING_TOKEN */; };
var isIdentWithValue = function (token, value) {
    return isIdentToken(token) && token.value === value;
};
var nonWhiteSpace = function (token) { return token.type !== 31 /* WHITESPACE_TOKEN */; };
var nonFunctionArgSeparator = function (token) {
    return token.type !== 31 /* WHITESPACE_TOKEN */ && token.type !== 4 /* COMMA_TOKEN */;
};
var parseFunctionArgs = function (tokens) {
    var args = [];
    var arg = [];
    tokens.forEach(function (token) {
        if (token.type === 4 /* COMMA_TOKEN */) {
            if (arg.length === 0) {
                throw new Error("Error parsing function args, zero tokens for arg");
            }
            args.push(arg);
            arg = [];
            return;
        }
        if (token.type !== 31 /* WHITESPACE_TOKEN */) {
            arg.push(token);
        }
    });
    if (arg.length) {
        args.push(arg);
    }
    return args;
};
var isEndingTokenFor = function (token, type) {
    if (type === 11 /* LEFT_CURLY_BRACKET_TOKEN */ && token.type === 12 /* RIGHT_CURLY_BRACKET_TOKEN */) {
        return true;
    }
    if (type === 28 /* LEFT_SQUARE_BRACKET_TOKEN */ && token.type === 29 /* RIGHT_SQUARE_BRACKET_TOKEN */) {
        return true;
    }
    return type === 2 /* LEFT_PARENTHESIS_TOKEN */ && token.type === 3 /* RIGHT_PARENTHESIS_TOKEN */;
};

var isLength = function (token) {
    return token.type === 17 /* NUMBER_TOKEN */ || token.type === 15 /* DIMENSION_TOKEN */;
};

var isLengthPercentage = function (token) {
    return token.type === 16 /* PERCENTAGE_TOKEN */ || isLength(token);
};
var parseLengthPercentageTuple = function (tokens) {
    return tokens.length > 1 ? [tokens[0], tokens[1]] : [tokens[0]];
};
var ZERO_LENGTH = {
    type: 17 /* NUMBER_TOKEN */,
    number: 0,
    flags: FLAG_INTEGER
};
var FIFTY_PERCENT = {
    type: 16 /* PERCENTAGE_TOKEN */,
    number: 50,
    flags: FLAG_INTEGER
};
var HUNDRED_PERCENT = {
    type: 16 /* PERCENTAGE_TOKEN */,
    number: 100,
    flags: FLAG_INTEGER
};
var getAbsoluteValueForTuple = function (tuple, width, height) {
    var x = tuple[0], y = tuple[1];
    return [getAbsoluteValue(x, width), getAbsoluteValue(typeof y !== 'undefined' ? y : x, height)];
};
var getAbsoluteValue = function (token, parent) {
    if (token.type === 16 /* PERCENTAGE_TOKEN */) {
        return (token.number / 100) * parent;
    }
    if (isDimensionToken(token)) {
        switch (token.unit) {
            case 'rem':
            case 'em':
                return 16 * token.number; // TODO use correct font-size
            case 'px':
            default:
                return token.number;
        }
    }
    return token.number;
};

var DEG = 'deg';
var GRAD = 'grad';
var RAD = 'rad';
var TURN = 'turn';
var angle$1 = {
    name: 'angle',
    parse: function (_context, value) {
        if (value.type === 15 /* DIMENSION_TOKEN */) {
            switch (value.unit) {
                case DEG:
                    return (Math.PI * value.number) / 180;
                case GRAD:
                    return (Math.PI / 200) * value.number;
                case RAD:
                    return value.number;
                case TURN:
                    return Math.PI * 2 * value.number;
            }
        }
        throw new Error("Unsupported angle type");
    }
};
var isAngle = function (value) {
    if (value.type === 15 /* DIMENSION_TOKEN */) {
        if (value.unit === DEG || value.unit === GRAD || value.unit === RAD || value.unit === TURN) {
            return true;
        }
    }
    return false;
};
var parseNamedSide = function (tokens) {
    var sideOrCorner = tokens
        .filter(isIdentToken)
        .map(function (ident) { return ident.value; })
        .join(' ');
    switch (sideOrCorner) {
        case 'to bottom right':
        case 'to right bottom':
        case 'left top':
        case 'top left':
            return [ZERO_LENGTH, ZERO_LENGTH];
        case 'to top':
        case 'bottom':
            return deg(0);
        case 'to bottom left':
        case 'to left bottom':
        case 'right top':
        case 'top right':
            return [ZERO_LENGTH, HUNDRED_PERCENT];
        case 'to right':
        case 'left':
            return deg(90);
        case 'to top left':
        case 'to left top':
        case 'right bottom':
        case 'bottom right':
            return [HUNDRED_PERCENT, HUNDRED_PERCENT];
        case 'to bottom':
        case 'top':
            return deg(180);
        case 'to top right':
        case 'to right top':
        case 'left bottom':
        case 'bottom left':
            return [HUNDRED_PERCENT, ZERO_LENGTH];
        case 'to left':
        case 'right':
            return deg(270);
    }
    return 0;
};
var deg = function (deg) { return (Math.PI * deg) / 180; };

var color$1 = {
    name: 'color',
    parse: function (context, value) {
        if (value.type === 18 /* FUNCTION */) {
            var colorFunction = SUPPORTED_COLOR_FUNCTIONS[value.name];
            if (typeof colorFunction === 'undefined') {
                throw new Error("Attempting to parse an unsupported color function \"" + value.name + "\"");
            }
            return colorFunction(context, value.values);
        }
        if (value.type === 5 /* HASH_TOKEN */) {
            if (value.value.length === 3) {
                var r = value.value.substring(0, 1);
                var g = value.value.substring(1, 2);
                var b = value.value.substring(2, 3);
                return pack(parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16), 1);
            }
            if (value.value.length === 4) {
                var r = value.value.substring(0, 1);
                var g = value.value.substring(1, 2);
                var b = value.value.substring(2, 3);
                var a = value.value.substring(3, 4);
                return pack(parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16), parseInt(a + a, 16) / 255);
            }
            if (value.value.length === 6) {
                var r = value.value.substring(0, 2);
                var g = value.value.substring(2, 4);
                var b = value.value.substring(4, 6);
                return pack(parseInt(r, 16), parseInt(g, 16), parseInt(b, 16), 1);
            }
            if (value.value.length === 8) {
                var r = value.value.substring(0, 2);
                var g = value.value.substring(2, 4);
                var b = value.value.substring(4, 6);
                var a = value.value.substring(6, 8);
                return pack(parseInt(r, 16), parseInt(g, 16), parseInt(b, 16), parseInt(a, 16) / 255);
            }
        }
        if (value.type === 20 /* IDENT_TOKEN */) {
            var namedColor = COLORS[value.value.toUpperCase()];
            if (typeof namedColor !== 'undefined') {
                return namedColor;
            }
        }
        return COLORS.TRANSPARENT;
    }
};
var isTransparent = function (color) { return (0xff & color) === 0; };
var asString = function (color) {
    var alpha = 0xff & color;
    var blue = 0xff & (color >> 8);
    var green = 0xff & (color >> 16);
    var red = 0xff & (color >> 24);
    return alpha < 255 ? "rgba(" + red + "," + green + "," + blue + "," + alpha / 255 + ")" : "rgb(" + red + "," + green + "," + blue + ")";
};
var pack = function (r, g, b, a) {
    return ((r << 24) | (g << 16) | (b << 8) | (Math.round(a * 255) << 0)) >>> 0;
};
var getTokenColorValue = function (token, i) {
    if (token.type === 17 /* NUMBER_TOKEN */) {
        return token.number;
    }
    if (token.type === 16 /* PERCENTAGE_TOKEN */) {
        var max = i === 3 ? 1 : 255;
        return i === 3 ? (token.number / 100) * max : Math.round((token.number / 100) * max);
    }
    return 0;
};
var rgb = function (_context, args) {
    var tokens = args.filter(nonFunctionArgSeparator);
    if (tokens.length === 3) {
        var _a = tokens.map(getTokenColorValue), r = _a[0], g = _a[1], b = _a[2];
        return pack(r, g, b, 1);
    }
    if (tokens.length === 4) {
        var _b = tokens.map(getTokenColorValue), r = _b[0], g = _b[1], b = _b[2], a = _b[3];
        return pack(r, g, b, a);
    }
    return 0;
};
function hue2rgb(t1, t2, hue) {
    if (hue < 0) {
        hue += 1;
    }
    if (hue >= 1) {
        hue -= 1;
    }
    if (hue < 1 / 6) {
        return (t2 - t1) * hue * 6 + t1;
    }
    else if (hue < 1 / 2) {
        return t2;
    }
    else if (hue < 2 / 3) {
        return (t2 - t1) * 6 * (2 / 3 - hue) + t1;
    }
    else {
        return t1;
    }
}
var hsl = function (context, args) {
    var tokens = args.filter(nonFunctionArgSeparator);
    var hue = tokens[0], saturation = tokens[1], lightness = tokens[2], alpha = tokens[3];
    var h = (hue.type === 17 /* NUMBER_TOKEN */ ? deg(hue.number) : angle$1.parse(context, hue)) / (Math.PI * 2);
    var s = isLengthPercentage(saturation) ? saturation.number / 100 : 0;
    var l = isLengthPercentage(lightness) ? lightness.number / 100 : 0;
    var a = typeof alpha !== 'undefined' && isLengthPercentage(alpha) ? getAbsoluteValue(alpha, 1) : 1;
    if (s === 0) {
        return pack(l * 255, l * 255, l * 255, 1);
    }
    var t2 = l <= 0.5 ? l * (s + 1) : l + s - l * s;
    var t1 = l * 2 - t2;
    var r = hue2rgb(t1, t2, h + 1 / 3);
    var g = hue2rgb(t1, t2, h);
    var b = hue2rgb(t1, t2, h - 1 / 3);
    return pack(r * 255, g * 255, b * 255, a);
};
var SUPPORTED_COLOR_FUNCTIONS = {
    hsl: hsl,
    hsla: hsl,
    rgb: rgb,
    rgba: rgb
};
var parseColor = function (context, value) {
    return color$1.parse(context, Parser.create(value).parseComponentValue());
};
var COLORS = {
    ALICEBLUE: 0xf0f8ffff,
    ANTIQUEWHITE: 0xfaebd7ff,
    AQUA: 0x00ffffff,
    AQUAMARINE: 0x7fffd4ff,
    AZURE: 0xf0ffffff,
    BEIGE: 0xf5f5dcff,
    BISQUE: 0xffe4c4ff,
    BLACK: 0x000000ff,
    BLANCHEDALMOND: 0xffebcdff,
    BLUE: 0x0000ffff,
    BLUEVIOLET: 0x8a2be2ff,
    BROWN: 0xa52a2aff,
    BURLYWOOD: 0xdeb887ff,
    CADETBLUE: 0x5f9ea0ff,
    CHARTREUSE: 0x7fff00ff,
    CHOCOLATE: 0xd2691eff,
    CORAL: 0xff7f50ff,
    CORNFLOWERBLUE: 0x6495edff,
    CORNSILK: 0xfff8dcff,
    CRIMSON: 0xdc143cff,
    CYAN: 0x00ffffff,
    DARKBLUE: 0x00008bff,
    DARKCYAN: 0x008b8bff,
    DARKGOLDENROD: 0xb886bbff,
    DARKGRAY: 0xa9a9a9ff,
    DARKGREEN: 0x006400ff,
    DARKGREY: 0xa9a9a9ff,
    DARKKHAKI: 0xbdb76bff,
    DARKMAGENTA: 0x8b008bff,
    DARKOLIVEGREEN: 0x556b2fff,
    DARKORANGE: 0xff8c00ff,
    DARKORCHID: 0x9932ccff,
    DARKRED: 0x8b0000ff,
    DARKSALMON: 0xe9967aff,
    DARKSEAGREEN: 0x8fbc8fff,
    DARKSLATEBLUE: 0x483d8bff,
    DARKSLATEGRAY: 0x2f4f4fff,
    DARKSLATEGREY: 0x2f4f4fff,
    DARKTURQUOISE: 0x00ced1ff,
    DARKVIOLET: 0x9400d3ff,
    DEEPPINK: 0xff1493ff,
    DEEPSKYBLUE: 0x00bfffff,
    DIMGRAY: 0x696969ff,
    DIMGREY: 0x696969ff,
    DODGERBLUE: 0x1e90ffff,
    FIREBRICK: 0xb22222ff,
    FLORALWHITE: 0xfffaf0ff,
    FORESTGREEN: 0x228b22ff,
    FUCHSIA: 0xff00ffff,
    GAINSBORO: 0xdcdcdcff,
    GHOSTWHITE: 0xf8f8ffff,
    GOLD: 0xffd700ff,
    GOLDENROD: 0xdaa520ff,
    GRAY: 0x808080ff,
    GREEN: 0x008000ff,
    GREENYELLOW: 0xadff2fff,
    GREY: 0x808080ff,
    HONEYDEW: 0xf0fff0ff,
    HOTPINK: 0xff69b4ff,
    INDIANRED: 0xcd5c5cff,
    INDIGO: 0x4b0082ff,
    IVORY: 0xfffff0ff,
    KHAKI: 0xf0e68cff,
    LAVENDER: 0xe6e6faff,
    LAVENDERBLUSH: 0xfff0f5ff,
    LAWNGREEN: 0x7cfc00ff,
    LEMONCHIFFON: 0xfffacdff,
    LIGHTBLUE: 0xadd8e6ff,
    LIGHTCORAL: 0xf08080ff,
    LIGHTCYAN: 0xe0ffffff,
    LIGHTGOLDENRODYELLOW: 0xfafad2ff,
    LIGHTGRAY: 0xd3d3d3ff,
    LIGHTGREEN: 0x90ee90ff,
    LIGHTGREY: 0xd3d3d3ff,
    LIGHTPINK: 0xffb6c1ff,
    LIGHTSALMON: 0xffa07aff,
    LIGHTSEAGREEN: 0x20b2aaff,
    LIGHTSKYBLUE: 0x87cefaff,
    LIGHTSLATEGRAY: 0x778899ff,
    LIGHTSLATEGREY: 0x778899ff,
    LIGHTSTEELBLUE: 0xb0c4deff,
    LIGHTYELLOW: 0xffffe0ff,
    LIME: 0x00ff00ff,
    LIMEGREEN: 0x32cd32ff,
    LINEN: 0xfaf0e6ff,
    MAGENTA: 0xff00ffff,
    MAROON: 0x800000ff,
    MEDIUMAQUAMARINE: 0x66cdaaff,
    MEDIUMBLUE: 0x0000cdff,
    MEDIUMORCHID: 0xba55d3ff,
    MEDIUMPURPLE: 0x9370dbff,
    MEDIUMSEAGREEN: 0x3cb371ff,
    MEDIUMSLATEBLUE: 0x7b68eeff,
    MEDIUMSPRINGGREEN: 0x00fa9aff,
    MEDIUMTURQUOISE: 0x48d1ccff,
    MEDIUMVIOLETRED: 0xc71585ff,
    MIDNIGHTBLUE: 0x191970ff,
    MINTCREAM: 0xf5fffaff,
    MISTYROSE: 0xffe4e1ff,
    MOCCASIN: 0xffe4b5ff,
    NAVAJOWHITE: 0xffdeadff,
    NAVY: 0x000080ff,
    OLDLACE: 0xfdf5e6ff,
    OLIVE: 0x808000ff,
    OLIVEDRAB: 0x6b8e23ff,
    ORANGE: 0xffa500ff,
    ORANGERED: 0xff4500ff,
    ORCHID: 0xda70d6ff,
    PALEGOLDENROD: 0xeee8aaff,
    PALEGREEN: 0x98fb98ff,
    PALETURQUOISE: 0xafeeeeff,
    PALEVIOLETRED: 0xdb7093ff,
    PAPAYAWHIP: 0xffefd5ff,
    PEACHPUFF: 0xffdab9ff,
    PERU: 0xcd853fff,
    PINK: 0xffc0cbff,
    PLUM: 0xdda0ddff,
    POWDERBLUE: 0xb0e0e6ff,
    PURPLE: 0x800080ff,
    REBECCAPURPLE: 0x663399ff,
    RED: 0xff0000ff,
    ROSYBROWN: 0xbc8f8fff,
    ROYALBLUE: 0x4169e1ff,
    SADDLEBROWN: 0x8b4513ff,
    SALMON: 0xfa8072ff,
    SANDYBROWN: 0xf4a460ff,
    SEAGREEN: 0x2e8b57ff,
    SEASHELL: 0xfff5eeff,
    SIENNA: 0xa0522dff,
    SILVER: 0xc0c0c0ff,
    SKYBLUE: 0x87ceebff,
    SLATEBLUE: 0x6a5acdff,
    SLATEGRAY: 0x708090ff,
    SLATEGREY: 0x708090ff,
    SNOW: 0xfffafaff,
    SPRINGGREEN: 0x00ff7fff,
    STEELBLUE: 0x4682b4ff,
    TAN: 0xd2b48cff,
    TEAL: 0x008080ff,
    THISTLE: 0xd8bfd8ff,
    TOMATO: 0xff6347ff,
    TRANSPARENT: 0x00000000,
    TURQUOISE: 0x40e0d0ff,
    VIOLET: 0xee82eeff,
    WHEAT: 0xf5deb3ff,
    WHITE: 0xffffffff,
    WHITESMOKE: 0xf5f5f5ff,
    YELLOW: 0xffff00ff,
    YELLOWGREEN: 0x9acd32ff
};

var backgroundClip = {
    name: 'background-clip',
    initialValue: 'border-box',
    prefix: false,
    type: 1 /* LIST */,
    parse: function (_context, tokens) {
        return tokens.map(function (token) {
            if (isIdentToken(token)) {
                switch (token.value) {
                    case 'padding-box':
                        return 1 /* PADDING_BOX */;
                    case 'content-box':
                        return 2 /* CONTENT_BOX */;
                }
            }
            return 0 /* BORDER_BOX */;
        });
    }
};

var backgroundColor = {
    name: "background-color",
    initialValue: 'transparent',
    prefix: false,
    type: 3 /* TYPE_VALUE */,
    format: 'color'
};

var parseColorStop = function (context, args) {
    var color = color$1.parse(context, args[0]);
    var stop = args[1];
    return stop && isLengthPercentage(stop) ? { color: color, stop: stop } : { color: color, stop: null };
};
var processColorStops = function (stops, lineLength) {
    var first = stops[0];
    var last = stops[stops.length - 1];
    if (first.stop === null) {
        first.stop = ZERO_LENGTH;
    }
    if (last.stop === null) {
        last.stop = HUNDRED_PERCENT;
    }
    var processStops = [];
    var previous = 0;
    for (var i = 0; i < stops.length; i++) {
        var stop_1 = stops[i].stop;
        if (stop_1 !== null) {
            var absoluteValue = getAbsoluteValue(stop_1, lineLength);
            if (absoluteValue > previous) {
                processStops.push(absoluteValue);
            }
            else {
                processStops.push(previous);
            }
            previous = absoluteValue;
        }
        else {
            processStops.push(null);
        }
    }
    var gapBegin = null;
    for (var i = 0; i < processStops.length; i++) {
        var stop_2 = processStops[i];
        if (stop_2 === null) {
            if (gapBegin === null) {
                gapBegin = i;
            }
        }
        else if (gapBegin !== null) {
            var gapLength = i - gapBegin;
            var beforeGap = processStops[gapBegin - 1];
            var gapValue = (stop_2 - beforeGap) / (gapLength + 1);
            for (var g = 1; g <= gapLength; g++) {
                processStops[gapBegin + g - 1] = gapValue * g;
            }
            gapBegin = null;
        }
    }
    return stops.map(function (_a, i) {
        var color = _a.color;
        return { color: color, stop: Math.max(Math.min(1, processStops[i] / lineLength), 0) };
    });
};
var getAngleFromCorner = function (corner, width, height) {
    var centerX = width / 2;
    var centerY = height / 2;
    var x = getAbsoluteValue(corner[0], width) - centerX;
    var y = centerY - getAbsoluteValue(corner[1], height);
    return (Math.atan2(y, x) + Math.PI * 2) % (Math.PI * 2);
};
var calculateGradientDirection = function (angle, width, height) {
    var radian = typeof angle === 'number' ? angle : getAngleFromCorner(angle, width, height);
    var lineLength = Math.abs(width * Math.sin(radian)) + Math.abs(height * Math.cos(radian));
    var halfWidth = width / 2;
    var halfHeight = height / 2;
    var halfLineLength = lineLength / 2;
    var yDiff = Math.sin(radian - Math.PI / 2) * halfLineLength;
    var xDiff = Math.cos(radian - Math.PI / 2) * halfLineLength;
    return [lineLength, halfWidth - xDiff, halfWidth + xDiff, halfHeight - yDiff, halfHeight + yDiff];
};
var distance = function (a, b) { return Math.sqrt(a * a + b * b); };
var findCorner = function (width, height, x, y, closest) {
    var corners = [
        [0, 0],
        [0, height],
        [width, 0],
        [width, height]
    ];
    return corners.reduce(function (stat, corner) {
        var cx = corner[0], cy = corner[1];
        var d = distance(x - cx, y - cy);
        if (closest ? d < stat.optimumDistance : d > stat.optimumDistance) {
            return {
                optimumCorner: corner,
                optimumDistance: d
            };
        }
        return stat;
    }, {
        optimumDistance: closest ? Infinity : -Infinity,
        optimumCorner: null
    }).optimumCorner;
};
var calculateRadius = function (gradient, x, y, width, height) {
    var rx = 0;
    var ry = 0;
    switch (gradient.size) {
        case 0 /* CLOSEST_SIDE */:
            // The ending shape is sized so that that it exactly meets the side of the gradient box closest to the gradient’s center.
            // If the shape is an ellipse, it exactly meets the closest side in each dimension.
            if (gradient.shape === 0 /* CIRCLE */) {
                rx = ry = Math.min(Math.abs(x), Math.abs(x - width), Math.abs(y), Math.abs(y - height));
            }
            else if (gradient.shape === 1 /* ELLIPSE */) {
                rx = Math.min(Math.abs(x), Math.abs(x - width));
                ry = Math.min(Math.abs(y), Math.abs(y - height));
            }
            break;
        case 2 /* CLOSEST_CORNER */:
            // The ending shape is sized so that that it passes through the corner of the gradient box closest to the gradient’s center.
            // If the shape is an ellipse, the ending shape is given the same aspect-ratio it would have if closest-side were specified.
            if (gradient.shape === 0 /* CIRCLE */) {
                rx = ry = Math.min(distance(x, y), distance(x, y - height), distance(x - width, y), distance(x - width, y - height));
            }
            else if (gradient.shape === 1 /* ELLIPSE */) {
                // Compute the ratio ry/rx (which is to be the same as for "closest-side")
                var c = Math.min(Math.abs(y), Math.abs(y - height)) / Math.min(Math.abs(x), Math.abs(x - width));
                var _a = findCorner(width, height, x, y, true), cx = _a[0], cy = _a[1];
                rx = distance(cx - x, (cy - y) / c);
                ry = c * rx;
            }
            break;
        case 1 /* FARTHEST_SIDE */:
            // Same as closest-side, except the ending shape is sized based on the farthest side(s)
            if (gradient.shape === 0 /* CIRCLE */) {
                rx = ry = Math.max(Math.abs(x), Math.abs(x - width), Math.abs(y), Math.abs(y - height));
            }
            else if (gradient.shape === 1 /* ELLIPSE */) {
                rx = Math.max(Math.abs(x), Math.abs(x - width));
                ry = Math.max(Math.abs(y), Math.abs(y - height));
            }
            break;
        case 3 /* FARTHEST_CORNER */:
            // Same as closest-corner, except the ending shape is sized based on the farthest corner.
            // If the shape is an ellipse, the ending shape is given the same aspect ratio it would have if farthest-side were specified.
            if (gradient.shape === 0 /* CIRCLE */) {
                rx = ry = Math.max(distance(x, y), distance(x, y - height), distance(x - width, y), distance(x - width, y - height));
            }
            else if (gradient.shape === 1 /* ELLIPSE */) {
                // Compute the ratio ry/rx (which is to be the same as for "farthest-side")
                var c = Math.max(Math.abs(y), Math.abs(y - height)) / Math.max(Math.abs(x), Math.abs(x - width));
                var _b = findCorner(width, height, x, y, false), cx = _b[0], cy = _b[1];
                rx = distance(cx - x, (cy - y) / c);
                ry = c * rx;
            }
            break;
    }
    if (Array.isArray(gradient.size)) {
        rx = getAbsoluteValue(gradient.size[0], width);
        ry = gradient.size.length === 2 ? getAbsoluteValue(gradient.size[1], height) : rx;
    }
    return [rx, ry];
};

var linearGradient = function (context, tokens) {
    var angle$1$1 = deg(180);
    var stops = [];
    parseFunctionArgs(tokens).forEach(function (arg, i) {
        if (i === 0) {
            var firstToken = arg[0];
            if (firstToken.type === 20 /* IDENT_TOKEN */ && firstToken.value === 'to') {
                angle$1$1 = parseNamedSide(arg);
                return;
            }
            else if (isAngle(firstToken)) {
                angle$1$1 = angle$1.parse(context, firstToken);
                return;
            }
        }
        var colorStop = parseColorStop(context, arg);
        stops.push(colorStop);
    });
    return { angle: angle$1$1, stops: stops, type: 1 /* LINEAR_GRADIENT */ };
};

var prefixLinearGradient = function (context, tokens) {
    var angle$1$1 = deg(180);
    var stops = [];
    parseFunctionArgs(tokens).forEach(function (arg, i) {
        if (i === 0) {
            var firstToken = arg[0];
            if (firstToken.type === 20 /* IDENT_TOKEN */ &&
                ['top', 'left', 'right', 'bottom'].indexOf(firstToken.value) !== -1) {
                angle$1$1 = parseNamedSide(arg);
                return;
            }
            else if (isAngle(firstToken)) {
                angle$1$1 = (angle$1.parse(context, firstToken) + deg(270)) % deg(360);
                return;
            }
        }
        var colorStop = parseColorStop(context, arg);
        stops.push(colorStop);
    });
    return {
        angle: angle$1$1,
        stops: stops,
        type: 1 /* LINEAR_GRADIENT */
    };
};

var webkitGradient = function (context, tokens) {
    var angle = deg(180);
    var stops = [];
    var type = 1 /* LINEAR_GRADIENT */;
    var shape = 0 /* CIRCLE */;
    var size = 3 /* FARTHEST_CORNER */;
    var position = [];
    parseFunctionArgs(tokens).forEach(function (arg, i) {
        var firstToken = arg[0];
        if (i === 0) {
            if (isIdentToken(firstToken) && firstToken.value === 'linear') {
                type = 1 /* LINEAR_GRADIENT */;
                return;
            }
            else if (isIdentToken(firstToken) && firstToken.value === 'radial') {
                type = 2 /* RADIAL_GRADIENT */;
                return;
            }
        }
        if (firstToken.type === 18 /* FUNCTION */) {
            if (firstToken.name === 'from') {
                var color = color$1.parse(context, firstToken.values[0]);
                stops.push({ stop: ZERO_LENGTH, color: color });
            }
            else if (firstToken.name === 'to') {
                var color = color$1.parse(context, firstToken.values[0]);
                stops.push({ stop: HUNDRED_PERCENT, color: color });
            }
            else if (firstToken.name === 'color-stop') {
                var values = firstToken.values.filter(nonFunctionArgSeparator);
                if (values.length === 2) {
                    var color = color$1.parse(context, values[1]);
                    var stop_1 = values[0];
                    if (isNumberToken(stop_1)) {
                        stops.push({
                            stop: { type: 16 /* PERCENTAGE_TOKEN */, number: stop_1.number * 100, flags: stop_1.flags },
                            color: color
                        });
                    }
                }
            }
        }
    });
    return type === 1 /* LINEAR_GRADIENT */
        ? {
            angle: (angle + deg(180)) % deg(360),
            stops: stops,
            type: type
        }
        : { size: size, shape: shape, stops: stops, position: position, type: type };
};

var CLOSEST_SIDE = 'closest-side';
var FARTHEST_SIDE = 'farthest-side';
var CLOSEST_CORNER = 'closest-corner';
var FARTHEST_CORNER = 'farthest-corner';
var CIRCLE = 'circle';
var ELLIPSE = 'ellipse';
var COVER = 'cover';
var CONTAIN = 'contain';
var radialGradient = function (context, tokens) {
    var shape = 0 /* CIRCLE */;
    var size = 3 /* FARTHEST_CORNER */;
    var stops = [];
    var position = [];
    parseFunctionArgs(tokens).forEach(function (arg, i) {
        var isColorStop = true;
        if (i === 0) {
            var isAtPosition_1 = false;
            isColorStop = arg.reduce(function (acc, token) {
                if (isAtPosition_1) {
                    if (isIdentToken(token)) {
                        switch (token.value) {
                            case 'center':
                                position.push(FIFTY_PERCENT);
                                return acc;
                            case 'top':
                            case 'left':
                                position.push(ZERO_LENGTH);
                                return acc;
                            case 'right':
                            case 'bottom':
                                position.push(HUNDRED_PERCENT);
                                return acc;
                        }
                    }
                    else if (isLengthPercentage(token) || isLength(token)) {
                        position.push(token);
                    }
                }
                else if (isIdentToken(token)) {
                    switch (token.value) {
                        case CIRCLE:
                            shape = 0 /* CIRCLE */;
                            return false;
                        case ELLIPSE:
                            shape = 1 /* ELLIPSE */;
                            return false;
                        case 'at':
                            isAtPosition_1 = true;
                            return false;
                        case CLOSEST_SIDE:
                            size = 0 /* CLOSEST_SIDE */;
                            return false;
                        case COVER:
                        case FARTHEST_SIDE:
                            size = 1 /* FARTHEST_SIDE */;
                            return false;
                        case CONTAIN:
                        case CLOSEST_CORNER:
                            size = 2 /* CLOSEST_CORNER */;
                            return false;
                        case FARTHEST_CORNER:
                            size = 3 /* FARTHEST_CORNER */;
                            return false;
                    }
                }
                else if (isLength(token) || isLengthPercentage(token)) {
                    if (!Array.isArray(size)) {
                        size = [];
                    }
                    size.push(token);
                    return false;
                }
                return acc;
            }, isColorStop);
        }
        if (isColorStop) {
            var colorStop = parseColorStop(context, arg);
            stops.push(colorStop);
        }
    });
    return { size: size, shape: shape, stops: stops, position: position, type: 2 /* RADIAL_GRADIENT */ };
};

var prefixRadialGradient = function (context, tokens) {
    var shape = 0 /* CIRCLE */;
    var size = 3 /* FARTHEST_CORNER */;
    var stops = [];
    var position = [];
    parseFunctionArgs(tokens).forEach(function (arg, i) {
        var isColorStop = true;
        if (i === 0) {
            isColorStop = arg.reduce(function (acc, token) {
                if (isIdentToken(token)) {
                    switch (token.value) {
                        case 'center':
                            position.push(FIFTY_PERCENT);
                            return false;
                        case 'top':
                        case 'left':
                            position.push(ZERO_LENGTH);
                            return false;
                        case 'right':
                        case 'bottom':
                            position.push(HUNDRED_PERCENT);
                            return false;
                    }
                }
                else if (isLengthPercentage(token) || isLength(token)) {
                    position.push(token);
                    return false;
                }
                return acc;
            }, isColorStop);
        }
        else if (i === 1) {
            isColorStop = arg.reduce(function (acc, token) {
                if (isIdentToken(token)) {
                    switch (token.value) {
                        case CIRCLE:
                            shape = 0 /* CIRCLE */;
                            return false;
                        case ELLIPSE:
                            shape = 1 /* ELLIPSE */;
                            return false;
                        case CONTAIN:
                        case CLOSEST_SIDE:
                            size = 0 /* CLOSEST_SIDE */;
                            return false;
                        case FARTHEST_SIDE:
                            size = 1 /* FARTHEST_SIDE */;
                            return false;
                        case CLOSEST_CORNER:
                            size = 2 /* CLOSEST_CORNER */;
                            return false;
                        case COVER:
                        case FARTHEST_CORNER:
                            size = 3 /* FARTHEST_CORNER */;
                            return false;
                    }
                }
                else if (isLength(token) || isLengthPercentage(token)) {
                    if (!Array.isArray(size)) {
                        size = [];
                    }
                    size.push(token);
                    return false;
                }
                return acc;
            }, isColorStop);
        }
        if (isColorStop) {
            var colorStop = parseColorStop(context, arg);
            stops.push(colorStop);
        }
    });
    return { size: size, shape: shape, stops: stops, position: position, type: 2 /* RADIAL_GRADIENT */ };
};

var isLinearGradient = function (background) {
    return background.type === 1 /* LINEAR_GRADIENT */;
};
var isRadialGradient = function (background) {
    return background.type === 2 /* RADIAL_GRADIENT */;
};
var image$1 = {
    name: 'image',
    parse: function (context, value) {
        if (value.type === 22 /* URL_TOKEN */) {
            var image_1 = { url: value.value, type: 0 /* URL */ };
            context.cache.addImage(value.value);
            return image_1;
        }
        if (value.type === 18 /* FUNCTION */) {
            var imageFunction = SUPPORTED_IMAGE_FUNCTIONS[value.name];
            if (typeof imageFunction === 'undefined') {
                throw new Error("Attempting to parse an unsupported image function \"" + value.name + "\"");
            }
            return imageFunction(context, value.values);
        }
        throw new Error("Unsupported image type " + value.type);
    }
};
function isSupportedImage(value) {
    return (!(value.type === 20 /* IDENT_TOKEN */ && value.value === 'none') &&
        (value.type !== 18 /* FUNCTION */ || !!SUPPORTED_IMAGE_FUNCTIONS[value.name]));
}
var SUPPORTED_IMAGE_FUNCTIONS = {
    'linear-gradient': linearGradient,
    '-moz-linear-gradient': prefixLinearGradient,
    '-ms-linear-gradient': prefixLinearGradient,
    '-o-linear-gradient': prefixLinearGradient,
    '-webkit-linear-gradient': prefixLinearGradient,
    'radial-gradient': radialGradient,
    '-moz-radial-gradient': prefixRadialGradient,
    '-ms-radial-gradient': prefixRadialGradient,
    '-o-radial-gradient': prefixRadialGradient,
    '-webkit-radial-gradient': prefixRadialGradient,
    '-webkit-gradient': webkitGradient
};

var backgroundImage = {
    name: 'background-image',
    initialValue: 'none',
    type: 1 /* LIST */,
    prefix: false,
    parse: function (context, tokens) {
        if (tokens.length === 0) {
            return [];
        }
        var first = tokens[0];
        if (first.type === 20 /* IDENT_TOKEN */ && first.value === 'none') {
            return [];
        }
        return tokens
            .filter(function (value) { return nonFunctionArgSeparator(value) && isSupportedImage(value); })
            .map(function (value) { return image$1.parse(context, value); });
    }
};

var backgroundOrigin = {
    name: 'background-origin',
    initialValue: 'border-box',
    prefix: false,
    type: 1 /* LIST */,
    parse: function (_context, tokens) {
        return tokens.map(function (token) {
            if (isIdentToken(token)) {
                switch (token.value) {
                    case 'padding-box':
                        return 1 /* PADDING_BOX */;
                    case 'content-box':
                        return 2 /* CONTENT_BOX */;
                }
            }
            return 0 /* BORDER_BOX */;
        });
    }
};

var backgroundPosition = {
    name: 'background-position',
    initialValue: '0% 0%',
    type: 1 /* LIST */,
    prefix: false,
    parse: function (_context, tokens) {
        return parseFunctionArgs(tokens)
            .map(function (values) { return values.filter(isLengthPercentage); })
            .map(parseLengthPercentageTuple);
    }
};

var backgroundRepeat = {
    name: 'background-repeat',
    initialValue: 'repeat',
    prefix: false,
    type: 1 /* LIST */,
    parse: function (_context, tokens) {
        return parseFunctionArgs(tokens)
            .map(function (values) {
            return values
                .filter(isIdentToken)
                .map(function (token) { return token.value; })
                .join(' ');
        })
            .map(parseBackgroundRepeat);
    }
};
var parseBackgroundRepeat = function (value) {
    switch (value) {
        case 'no-repeat':
            return 1 /* NO_REPEAT */;
        case 'repeat-x':
        case 'repeat no-repeat':
            return 2 /* REPEAT_X */;
        case 'repeat-y':
        case 'no-repeat repeat':
            return 3 /* REPEAT_Y */;
        case 'repeat':
        default:
            return 0 /* REPEAT */;
    }
};

var BACKGROUND_SIZE;
(function (BACKGROUND_SIZE) {
    BACKGROUND_SIZE["AUTO"] = "auto";
    BACKGROUND_SIZE["CONTAIN"] = "contain";
    BACKGROUND_SIZE["COVER"] = "cover";
})(BACKGROUND_SIZE || (BACKGROUND_SIZE = {}));
var backgroundSize = {
    name: 'background-size',
    initialValue: '0',
    prefix: false,
    type: 1 /* LIST */,
    parse: function (_context, tokens) {
        return parseFunctionArgs(tokens).map(function (values) { return values.filter(isBackgroundSizeInfoToken); });
    }
};
var isBackgroundSizeInfoToken = function (value) {
    return isIdentToken(value) || isLengthPercentage(value);
};

var borderColorForSide = function (side) { return ({
    name: "border-" + side + "-color",
    initialValue: 'transparent',
    prefix: false,
    type: 3 /* TYPE_VALUE */,
    format: 'color'
}); };
var borderTopColor = borderColorForSide('top');
var borderRightColor = borderColorForSide('right');
var borderBottomColor = borderColorForSide('bottom');
var borderLeftColor = borderColorForSide('left');

var borderRadiusForSide = function (side) { return ({
    name: "border-radius-" + side,
    initialValue: '0 0',
    prefix: false,
    type: 1 /* LIST */,
    parse: function (_context, tokens) {
        return parseLengthPercentageTuple(tokens.filter(isLengthPercentage));
    }
}); };
var borderTopLeftRadius = borderRadiusForSide('top-left');
var borderTopRightRadius = borderRadiusForSide('top-right');
var borderBottomRightRadius = borderRadiusForSide('bottom-right');
var borderBottomLeftRadius = borderRadiusForSide('bottom-left');

var borderStyleForSide = function (side) { return ({
    name: "border-" + side + "-style",
    initialValue: 'solid',
    prefix: false,
    type: 2 /* IDENT_VALUE */,
    parse: function (_context, style) {
        switch (style) {
            case 'none':
                return 0 /* NONE */;
            case 'dashed':
                return 2 /* DASHED */;
            case 'dotted':
                return 3 /* DOTTED */;
            case 'double':
                return 4 /* DOUBLE */;
        }
        return 1 /* SOLID */;
    }
}); };
var borderTopStyle = borderStyleForSide('top');
var borderRightStyle = borderStyleForSide('right');
var borderBottomStyle = borderStyleForSide('bottom');
var borderLeftStyle = borderStyleForSide('left');

var borderWidthForSide = function (side) { return ({
    name: "border-" + side + "-width",
    initialValue: '0',
    type: 0 /* VALUE */,
    prefix: false,
    parse: function (_context, token) {
        if (isDimensionToken(token)) {
            return token.number;
        }
        return 0;
    }
}); };
var borderTopWidth = borderWidthForSide('top');
var borderRightWidth = borderWidthForSide('right');
var borderBottomWidth = borderWidthForSide('bottom');
var borderLeftWidth = borderWidthForSide('left');

var color = {
    name: "color",
    initialValue: 'transparent',
    prefix: false,
    type: 3 /* TYPE_VALUE */,
    format: 'color'
};

var direction = {
    name: 'direction',
    initialValue: 'ltr',
    prefix: false,
    type: 2 /* IDENT_VALUE */,
    parse: function (_context, direction) {
        switch (direction) {
            case 'rtl':
                return 1 /* RTL */;
            case 'ltr':
            default:
                return 0 /* LTR */;
        }
    }
};

var display = {
    name: 'display',
    initialValue: 'inline-block',
    prefix: false,
    type: 1 /* LIST */,
    parse: function (_context, tokens) {
        return tokens.filter(isIdentToken).reduce(function (bit, token) {
            return bit | parseDisplayValue(token.value);
        }, 0 /* NONE */);
    }
};
var parseDisplayValue = function (display) {
    switch (display) {
        case 'block':
        case '-webkit-box':
            return 2 /* BLOCK */;
        case 'inline':
            return 4 /* INLINE */;
        case 'run-in':
            return 8 /* RUN_IN */;
        case 'flow':
            return 16 /* FLOW */;
        case 'flow-root':
            return 32 /* FLOW_ROOT */;
        case 'table':
            return 64 /* TABLE */;
        case 'flex':
        case '-webkit-flex':
            return 128 /* FLEX */;
        case 'grid':
        case '-ms-grid':
            return 256 /* GRID */;
        case 'ruby':
            return 512 /* RUBY */;
        case 'subgrid':
            return 1024 /* SUBGRID */;
        case 'list-item':
            return 2048 /* LIST_ITEM */;
        case 'table-row-group':
            return 4096 /* TABLE_ROW_GROUP */;
        case 'table-header-group':
            return 8192 /* TABLE_HEADER_GROUP */;
        case 'table-footer-group':
            return 16384 /* TABLE_FOOTER_GROUP */;
        case 'table-row':
            return 32768 /* TABLE_ROW */;
        case 'table-cell':
            return 65536 /* TABLE_CELL */;
        case 'table-column-group':
            return 131072 /* TABLE_COLUMN_GROUP */;
        case 'table-column':
            return 262144 /* TABLE_COLUMN */;
        case 'table-caption':
            return 524288 /* TABLE_CAPTION */;
        case 'ruby-base':
            return 1048576 /* RUBY_BASE */;
        case 'ruby-text':
            return 2097152 /* RUBY_TEXT */;
        case 'ruby-base-container':
            return 4194304 /* RUBY_BASE_CONTAINER */;
        case 'ruby-text-container':
            return 8388608 /* RUBY_TEXT_CONTAINER */;
        case 'contents':
            return 16777216 /* CONTENTS */;
        case 'inline-block':
            return 33554432 /* INLINE_BLOCK */;
        case 'inline-list-item':
            return 67108864 /* INLINE_LIST_ITEM */;
        case 'inline-table':
            return 134217728 /* INLINE_TABLE */;
        case 'inline-flex':
            return 268435456 /* INLINE_FLEX */;
        case 'inline-grid':
            return 536870912 /* INLINE_GRID */;
    }
    return 0 /* NONE */;
};

var float = {
    name: 'float',
    initialValue: 'none',
    prefix: false,
    type: 2 /* IDENT_VALUE */,
    parse: function (_context, float) {
        switch (float) {
            case 'left':
                return 1 /* LEFT */;
            case 'right':
                return 2 /* RIGHT */;
            case 'inline-start':
                return 3 /* INLINE_START */;
            case 'inline-end':
                return 4 /* INLINE_END */;
        }
        return 0 /* NONE */;
    }
};

var letterSpacing = {
    name: 'letter-spacing',
    initialValue: '0',
    prefix: false,
    type: 0 /* VALUE */,
    parse: function (_context, token) {
        if (token.type === 20 /* IDENT_TOKEN */ && token.value === 'normal') {
            return 0;
        }
        if (token.type === 17 /* NUMBER_TOKEN */) {
            return token.number;
        }
        if (token.type === 15 /* DIMENSION_TOKEN */) {
            return token.number;
        }
        return 0;
    }
};

var LINE_BREAK;
(function (LINE_BREAK) {
    LINE_BREAK["NORMAL"] = "normal";
    LINE_BREAK["STRICT"] = "strict";
})(LINE_BREAK || (LINE_BREAK = {}));
var lineBreak = {
    name: 'line-break',
    initialValue: 'normal',
    prefix: false,
    type: 2 /* IDENT_VALUE */,
    parse: function (_context, lineBreak) {
        switch (lineBreak) {
            case 'strict':
                return LINE_BREAK.STRICT;
            case 'normal':
            default:
                return LINE_BREAK.NORMAL;
        }
    }
};

var lineHeight = {
    name: 'line-height',
    initialValue: 'normal',
    prefix: false,
    type: 4 /* TOKEN_VALUE */
};
var computeLineHeight = function (token, fontSize) {
    if (isIdentToken(token) && token.value === 'normal') {
        return 1.2 * fontSize;
    }
    else if (token.type === 17 /* NUMBER_TOKEN */) {
        return fontSize * token.number;
    }
    else if (isLengthPercentage(token)) {
        return getAbsoluteValue(token, fontSize);
    }
    return fontSize;
};

var listStyleImage = {
    name: 'list-style-image',
    initialValue: 'none',
    type: 0 /* VALUE */,
    prefix: false,
    parse: function (context, token) {
        if (token.type === 20 /* IDENT_TOKEN */ && token.value === 'none') {
            return null;
        }
        return image$1.parse(context, token);
    }
};

var listStylePosition = {
    name: 'list-style-position',
    initialValue: 'outside',
    prefix: false,
    type: 2 /* IDENT_VALUE */,
    parse: function (_context, position) {
        switch (position) {
            case 'inside':
                return 0 /* INSIDE */;
            case 'outside':
            default:
                return 1 /* OUTSIDE */;
        }
    }
};

var listStyleType = {
    name: 'list-style-type',
    initialValue: 'none',
    prefix: false,
    type: 2 /* IDENT_VALUE */,
    parse: function (_context, type) {
        switch (type) {
            case 'disc':
                return 0 /* DISC */;
            case 'circle':
                return 1 /* CIRCLE */;
            case 'square':
                return 2 /* SQUARE */;
            case 'decimal':
                return 3 /* DECIMAL */;
            case 'cjk-decimal':
                return 4 /* CJK_DECIMAL */;
            case 'decimal-leading-zero':
                return 5 /* DECIMAL_LEADING_ZERO */;
            case 'lower-roman':
                return 6 /* LOWER_ROMAN */;
            case 'upper-roman':
                return 7 /* UPPER_ROMAN */;
            case 'lower-greek':
                return 8 /* LOWER_GREEK */;
            case 'lower-alpha':
                return 9 /* LOWER_ALPHA */;
            case 'upper-alpha':
                return 10 /* UPPER_ALPHA */;
            case 'arabic-indic':
                return 11 /* ARABIC_INDIC */;
            case 'armenian':
                return 12 /* ARMENIAN */;
            case 'bengali':
                return 13 /* BENGALI */;
            case 'cambodian':
                return 14 /* CAMBODIAN */;
            case 'cjk-earthly-branch':
                return 15 /* CJK_EARTHLY_BRANCH */;
            case 'cjk-heavenly-stem':
                return 16 /* CJK_HEAVENLY_STEM */;
            case 'cjk-ideographic':
                return 17 /* CJK_IDEOGRAPHIC */;
            case 'devanagari':
                return 18 /* DEVANAGARI */;
            case 'ethiopic-numeric':
                return 19 /* ETHIOPIC_NUMERIC */;
            case 'georgian':
                return 20 /* GEORGIAN */;
            case 'gujarati':
                return 21 /* GUJARATI */;
            case 'gurmukhi':
                return 22 /* GURMUKHI */;
            case 'hebrew':
                return 22 /* HEBREW */;
            case 'hiragana':
                return 23 /* HIRAGANA */;
            case 'hiragana-iroha':
                return 24 /* HIRAGANA_IROHA */;
            case 'japanese-formal':
                return 25 /* JAPANESE_FORMAL */;
            case 'japanese-informal':
                return 26 /* JAPANESE_INFORMAL */;
            case 'kannada':
                return 27 /* KANNADA */;
            case 'katakana':
                return 28 /* KATAKANA */;
            case 'katakana-iroha':
                return 29 /* KATAKANA_IROHA */;
            case 'khmer':
                return 30 /* KHMER */;
            case 'korean-hangul-formal':
                return 31 /* KOREAN_HANGUL_FORMAL */;
            case 'korean-hanja-formal':
                return 32 /* KOREAN_HANJA_FORMAL */;
            case 'korean-hanja-informal':
                return 33 /* KOREAN_HANJA_INFORMAL */;
            case 'lao':
                return 34 /* LAO */;
            case 'lower-armenian':
                return 35 /* LOWER_ARMENIAN */;
            case 'malayalam':
                return 36 /* MALAYALAM */;
            case 'mongolian':
                return 37 /* MONGOLIAN */;
            case 'myanmar':
                return 38 /* MYANMAR */;
            case 'oriya':
                return 39 /* ORIYA */;
            case 'persian':
                return 40 /* PERSIAN */;
            case 'simp-chinese-formal':
                return 41 /* SIMP_CHINESE_FORMAL */;
            case 'simp-chinese-informal':
                return 42 /* SIMP_CHINESE_INFORMAL */;
            case 'tamil':
                return 43 /* TAMIL */;
            case 'telugu':
                return 44 /* TELUGU */;
            case 'thai':
                return 45 /* THAI */;
            case 'tibetan':
                return 46 /* TIBETAN */;
            case 'trad-chinese-formal':
                return 47 /* TRAD_CHINESE_FORMAL */;
            case 'trad-chinese-informal':
                return 48 /* TRAD_CHINESE_INFORMAL */;
            case 'upper-armenian':
                return 49 /* UPPER_ARMENIAN */;
            case 'disclosure-open':
                return 50 /* DISCLOSURE_OPEN */;
            case 'disclosure-closed':
                return 51 /* DISCLOSURE_CLOSED */;
            case 'none':
            default:
                return -1 /* NONE */;
        }
    }
};

var marginForSide = function (side) { return ({
    name: "margin-" + side,
    initialValue: '0',
    prefix: false,
    type: 4 /* TOKEN_VALUE */
}); };
var marginTop = marginForSide('top');
var marginRight = marginForSide('right');
var marginBottom = marginForSide('bottom');
var marginLeft = marginForSide('left');

var overflow = {
    name: 'overflow',
    initialValue: 'visible',
    prefix: false,
    type: 1 /* LIST */,
    parse: function (_context, tokens) {
        return tokens.filter(isIdentToken).map(function (overflow) {
            switch (overflow.value) {
                case 'hidden':
                    return 1 /* HIDDEN */;
                case 'scroll':
                    return 2 /* SCROLL */;
                case 'clip':
                    return 3 /* CLIP */;
                case 'auto':
                    return 4 /* AUTO */;
                case 'visible':
                default:
                    return 0 /* VISIBLE */;
            }
        });
    }
};

var overflowWrap = {
    name: 'overflow-wrap',
    initialValue: 'normal',
    prefix: false,
    type: 2 /* IDENT_VALUE */,
    parse: function (_context, overflow) {
        switch (overflow) {
            case 'break-word':
                return "break-word" /* BREAK_WORD */;
            case 'normal':
            default:
                return "normal" /* NORMAL */;
        }
    }
};

var paddingForSide = function (side) { return ({
    name: "padding-" + side,
    initialValue: '0',
    prefix: false,
    type: 3 /* TYPE_VALUE */,
    format: 'length-percentage'
}); };
var paddingTop = paddingForSide('top');
var paddingRight = paddingForSide('right');
var paddingBottom = paddingForSide('bottom');
var paddingLeft = paddingForSide('left');

var textAlign = {
    name: 'text-align',
    initialValue: 'left',
    prefix: false,
    type: 2 /* IDENT_VALUE */,
    parse: function (_context, textAlign) {
        switch (textAlign) {
            case 'right':
                return 2 /* RIGHT */;
            case 'center':
            case 'justify':
                return 1 /* CENTER */;
            case 'left':
            default:
                return 0 /* LEFT */;
        }
    }
};

var position = {
    name: 'position',
    initialValue: 'static',
    prefix: false,
    type: 2 /* IDENT_VALUE */,
    parse: function (_context, position) {
        switch (position) {
            case 'relative':
                return 1 /* RELATIVE */;
            case 'absolute':
                return 2 /* ABSOLUTE */;
            case 'fixed':
                return 3 /* FIXED */;
            case 'sticky':
                return 4 /* STICKY */;
        }
        return 0 /* STATIC */;
    }
};

var textShadow = {
    name: 'text-shadow',
    initialValue: 'none',
    type: 1 /* LIST */,
    prefix: false,
    parse: function (context, tokens) {
        if (tokens.length === 1 && isIdentWithValue(tokens[0], 'none')) {
            return [];
        }
        return parseFunctionArgs(tokens).map(function (values) {
            var shadow = {
                color: COLORS.TRANSPARENT,
                offsetX: ZERO_LENGTH,
                offsetY: ZERO_LENGTH,
                blur: ZERO_LENGTH
            };
            var c = 0;
            for (var i = 0; i < values.length; i++) {
                var token = values[i];
                if (isLength(token)) {
                    if (c === 0) {
                        shadow.offsetX = token;
                    }
                    else if (c === 1) {
                        shadow.offsetY = token;
                    }
                    else {
                        shadow.blur = token;
                    }
                    c++;
                }
                else {
                    shadow.color = color$1.parse(context, token);
                }
            }
            return shadow;
        });
    }
};

var textTransform = {
    name: 'text-transform',
    initialValue: 'none',
    prefix: false,
    type: 2 /* IDENT_VALUE */,
    parse: function (_context, textTransform) {
        switch (textTransform) {
            case 'uppercase':
                return 2 /* UPPERCASE */;
            case 'lowercase':
                return 1 /* LOWERCASE */;
            case 'capitalize':
                return 3 /* CAPITALIZE */;
        }
        return 0 /* NONE */;
    }
};

var transform$1 = {
    name: 'transform',
    initialValue: 'none',
    prefix: true,
    type: 0 /* VALUE */,
    parse: function (_context, token) {
        if (token.type === 20 /* IDENT_TOKEN */ && token.value === 'none') {
            return null;
        }
        if (token.type === 18 /* FUNCTION */) {
            var transformFunction = SUPPORTED_TRANSFORM_FUNCTIONS[token.name];
            if (typeof transformFunction === 'undefined') {
                throw new Error("Attempting to parse an unsupported transform function \"" + token.name + "\"");
            }
            return transformFunction(token.values);
        }
        return null;
    }
};
var matrix = function (args) {
    var values = args.filter(function (arg) { return arg.type === 17 /* NUMBER_TOKEN */; }).map(function (arg) { return arg.number; });
    return values.length === 6 ? values : null;
};
// doesn't support 3D transforms at the moment
var matrix3d = function (args) {
    var values = args.filter(function (arg) { return arg.type === 17 /* NUMBER_TOKEN */; }).map(function (arg) { return arg.number; });
    var a1 = values[0], b1 = values[1]; values[2]; values[3]; var a2 = values[4], b2 = values[5]; values[6]; values[7]; values[8]; values[9]; values[10]; values[11]; var a4 = values[12], b4 = values[13]; values[14]; values[15];
    return values.length === 16 ? [a1, b1, a2, b2, a4, b4] : null;
};
var SUPPORTED_TRANSFORM_FUNCTIONS = {
    matrix: matrix,
    matrix3d: matrix3d
};

var DEFAULT_VALUE = {
    type: 16 /* PERCENTAGE_TOKEN */,
    number: 50,
    flags: FLAG_INTEGER
};
var DEFAULT = [DEFAULT_VALUE, DEFAULT_VALUE];
var transformOrigin = {
    name: 'transform-origin',
    initialValue: '50% 50%',
    prefix: true,
    type: 1 /* LIST */,
    parse: function (_context, tokens) {
        var origins = tokens.filter(isLengthPercentage);
        if (origins.length !== 2) {
            return DEFAULT;
        }
        return [origins[0], origins[1]];
    }
};

var visibility = {
    name: 'visible',
    initialValue: 'none',
    prefix: false,
    type: 2 /* IDENT_VALUE */,
    parse: function (_context, visibility) {
        switch (visibility) {
            case 'hidden':
                return 1 /* HIDDEN */;
            case 'collapse':
                return 2 /* COLLAPSE */;
            case 'visible':
            default:
                return 0 /* VISIBLE */;
        }
    }
};

var WORD_BREAK;
(function (WORD_BREAK) {
    WORD_BREAK["NORMAL"] = "normal";
    WORD_BREAK["BREAK_ALL"] = "break-all";
    WORD_BREAK["KEEP_ALL"] = "keep-all";
})(WORD_BREAK || (WORD_BREAK = {}));
var wordBreak = {
    name: 'word-break',
    initialValue: 'normal',
    prefix: false,
    type: 2 /* IDENT_VALUE */,
    parse: function (_context, wordBreak) {
        switch (wordBreak) {
            case 'break-all':
                return WORD_BREAK.BREAK_ALL;
            case 'keep-all':
                return WORD_BREAK.KEEP_ALL;
            case 'normal':
            default:
                return WORD_BREAK.NORMAL;
        }
    }
};

var zIndex = {
    name: 'z-index',
    initialValue: 'auto',
    prefix: false,
    type: 0 /* VALUE */,
    parse: function (_context, token) {
        if (token.type === 20 /* IDENT_TOKEN */) {
            return { auto: true, order: 0 };
        }
        if (isNumberToken(token)) {
            return { auto: false, order: token.number };
        }
        throw new Error("Invalid z-index number parsed");
    }
};

var time = {
    name: 'time',
    parse: function (_context, value) {
        if (value.type === 15 /* DIMENSION_TOKEN */) {
            switch (value.unit.toLowerCase()) {
                case 's':
                    return 1000 * value.number;
                case 'ms':
                    return value.number;
            }
        }
        throw new Error("Unsupported time type");
    }
};

var opacity = {
    name: 'opacity',
    initialValue: '1',
    type: 0 /* VALUE */,
    prefix: false,
    parse: function (_context, token) {
        if (isNumberToken(token)) {
            return token.number;
        }
        return 1;
    }
};

var textDecorationColor = {
    name: "text-decoration-color",
    initialValue: 'transparent',
    prefix: false,
    type: 3 /* TYPE_VALUE */,
    format: 'color'
};

var textDecorationLine = {
    name: 'text-decoration-line',
    initialValue: 'none',
    prefix: false,
    type: 1 /* LIST */,
    parse: function (_context, tokens) {
        return tokens
            .filter(isIdentToken)
            .map(function (token) {
            switch (token.value) {
                case 'underline':
                    return 1 /* UNDERLINE */;
                case 'overline':
                    return 2 /* OVERLINE */;
                case 'line-through':
                    return 3 /* LINE_THROUGH */;
                case 'none':
                    return 4 /* BLINK */;
            }
            return 0 /* NONE */;
        })
            .filter(function (line) { return line !== 0 /* NONE */; });
    }
};

var fontFamily = {
    name: "font-family",
    initialValue: '',
    prefix: false,
    type: 1 /* LIST */,
    parse: function (_context, tokens) {
        var accumulator = [];
        var results = [];
        tokens.forEach(function (token) {
            switch (token.type) {
                case 20 /* IDENT_TOKEN */:
                case 0 /* STRING_TOKEN */:
                    accumulator.push(token.value);
                    break;
                case 17 /* NUMBER_TOKEN */:
                    accumulator.push(token.number.toString());
                    break;
                case 4 /* COMMA_TOKEN */:
                    results.push(accumulator.join(' '));
                    accumulator.length = 0;
                    break;
            }
        });
        if (accumulator.length) {
            results.push(accumulator.join(' '));
        }
        return results.map(function (result) { return (result.indexOf(' ') === -1 ? result : "'" + result + "'"); });
    }
};

var fontSize = {
    name: "font-size",
    initialValue: '0',
    prefix: false,
    type: 3 /* TYPE_VALUE */,
    format: 'length'
};

var fontWeight = {
    name: 'font-weight',
    initialValue: 'normal',
    type: 0 /* VALUE */,
    prefix: false,
    parse: function (_context, token) {
        if (isNumberToken(token)) {
            return token.number;
        }
        if (isIdentToken(token)) {
            switch (token.value) {
                case 'bold':
                    return 700;
                case 'normal':
                default:
                    return 400;
            }
        }
        return 400;
    }
};

var fontVariant = {
    name: 'font-variant',
    initialValue: 'none',
    type: 1 /* LIST */,
    prefix: false,
    parse: function (_context, tokens) {
        return tokens.filter(isIdentToken).map(function (token) { return token.value; });
    }
};

var fontStyle$1 = {
    name: 'font-style',
    initialValue: 'normal',
    prefix: false,
    type: 2 /* IDENT_VALUE */,
    parse: function (_context, overflow) {
        switch (overflow) {
            case 'oblique':
                return "oblique" /* OBLIQUE */;
            case 'italic':
                return "italic" /* ITALIC */;
            case 'normal':
            default:
                return "normal" /* NORMAL */;
        }
    }
};

var contains = function (bit, value) { return (bit & value) !== 0; };

var content = {
    name: 'content',
    initialValue: 'none',
    type: 1 /* LIST */,
    prefix: false,
    parse: function (_context, tokens) {
        if (tokens.length === 0) {
            return [];
        }
        var first = tokens[0];
        if (first.type === 20 /* IDENT_TOKEN */ && first.value === 'none') {
            return [];
        }
        return tokens;
    }
};

var counterIncrement = {
    name: 'counter-increment',
    initialValue: 'none',
    prefix: true,
    type: 1 /* LIST */,
    parse: function (_context, tokens) {
        if (tokens.length === 0) {
            return null;
        }
        var first = tokens[0];
        if (first.type === 20 /* IDENT_TOKEN */ && first.value === 'none') {
            return null;
        }
        var increments = [];
        var filtered = tokens.filter(nonWhiteSpace);
        for (var i = 0; i < filtered.length; i++) {
            var counter = filtered[i];
            var next = filtered[i + 1];
            if (counter.type === 20 /* IDENT_TOKEN */) {
                var increment = next && isNumberToken(next) ? next.number : 1;
                increments.push({ counter: counter.value, increment: increment });
            }
        }
        return increments;
    }
};

var counterReset = {
    name: 'counter-reset',
    initialValue: 'none',
    prefix: true,
    type: 1 /* LIST */,
    parse: function (_context, tokens) {
        if (tokens.length === 0) {
            return [];
        }
        var resets = [];
        var filtered = tokens.filter(nonWhiteSpace);
        for (var i = 0; i < filtered.length; i++) {
            var counter = filtered[i];
            var next = filtered[i + 1];
            if (isIdentToken(counter) && counter.value !== 'none') {
                var reset = next && isNumberToken(next) ? next.number : 0;
                resets.push({ counter: counter.value, reset: reset });
            }
        }
        return resets;
    }
};

var duration = {
    name: 'duration',
    initialValue: '0s',
    prefix: false,
    type: 1 /* LIST */,
    parse: function (context, tokens) {
        return tokens.filter(isDimensionToken).map(function (token) { return time.parse(context, token); });
    }
};

var quotes = {
    name: 'quotes',
    initialValue: 'none',
    prefix: true,
    type: 1 /* LIST */,
    parse: function (_context, tokens) {
        if (tokens.length === 0) {
            return null;
        }
        var first = tokens[0];
        if (first.type === 20 /* IDENT_TOKEN */ && first.value === 'none') {
            return null;
        }
        var quotes = [];
        var filtered = tokens.filter(isStringToken);
        if (filtered.length % 2 !== 0) {
            return null;
        }
        for (var i = 0; i < filtered.length; i += 2) {
            var open_1 = filtered[i].value;
            var close_1 = filtered[i + 1].value;
            quotes.push({ open: open_1, close: close_1 });
        }
        return quotes;
    }
};
var getQuote = function (quotes, depth, open) {
    if (!quotes) {
        return '';
    }
    var quote = quotes[Math.min(depth, quotes.length - 1)];
    if (!quote) {
        return '';
    }
    return open ? quote.open : quote.close;
};

var boxShadow = {
    name: 'box-shadow',
    initialValue: 'none',
    type: 1 /* LIST */,
    prefix: false,
    parse: function (context, tokens) {
        if (tokens.length === 1 && isIdentWithValue(tokens[0], 'none')) {
            return [];
        }
        return parseFunctionArgs(tokens).map(function (values) {
            var shadow = {
                color: 0x000000ff,
                offsetX: ZERO_LENGTH,
                offsetY: ZERO_LENGTH,
                blur: ZERO_LENGTH,
                spread: ZERO_LENGTH,
                inset: false
            };
            var c = 0;
            for (var i = 0; i < values.length; i++) {
                var token = values[i];
                if (isIdentWithValue(token, 'inset')) {
                    shadow.inset = true;
                }
                else if (isLength(token)) {
                    if (c === 0) {
                        shadow.offsetX = token;
                    }
                    else if (c === 1) {
                        shadow.offsetY = token;
                    }
                    else if (c === 2) {
                        shadow.blur = token;
                    }
                    else {
                        shadow.spread = token;
                    }
                    c++;
                }
                else {
                    shadow.color = color$1.parse(context, token);
                }
            }
            return shadow;
        });
    }
};

var paintOrder = {
    name: 'paint-order',
    initialValue: 'normal',
    prefix: false,
    type: 1 /* LIST */,
    parse: function (_context, tokens) {
        var DEFAULT_VALUE = [0 /* FILL */, 1 /* STROKE */, 2 /* MARKERS */];
        var layers = [];
        tokens.filter(isIdentToken).forEach(function (token) {
            switch (token.value) {
                case 'stroke':
                    layers.push(1 /* STROKE */);
                    break;
                case 'fill':
                    layers.push(0 /* FILL */);
                    break;
                case 'markers':
                    layers.push(2 /* MARKERS */);
                    break;
            }
        });
        DEFAULT_VALUE.forEach(function (value) {
            if (layers.indexOf(value) === -1) {
                layers.push(value);
            }
        });
        return layers;
    }
};

var webkitTextStrokeColor = {
    name: "-webkit-text-stroke-color",
    initialValue: 'currentcolor',
    prefix: false,
    type: 3 /* TYPE_VALUE */,
    format: 'color'
};

var webkitTextStrokeWidth = {
    name: "-webkit-text-stroke-width",
    initialValue: '0',
    type: 0 /* VALUE */,
    prefix: false,
    parse: function (_context, token) {
        if (isDimensionToken(token)) {
            return token.number;
        }
        return 0;
    }
};

var CSSParsedDeclaration = /** @class */ (function () {
    function CSSParsedDeclaration(context, declaration) {
        var _a, _b;
        this.animationDuration = parse$4(context, duration, declaration.animationDuration);
        this.backgroundClip = parse$4(context, backgroundClip, declaration.backgroundClip);
        this.backgroundColor = parse$4(context, backgroundColor, declaration.backgroundColor);
        this.backgroundImage = parse$4(context, backgroundImage, declaration.backgroundImage);
        this.backgroundOrigin = parse$4(context, backgroundOrigin, declaration.backgroundOrigin);
        this.backgroundPosition = parse$4(context, backgroundPosition, declaration.backgroundPosition);
        this.backgroundRepeat = parse$4(context, backgroundRepeat, declaration.backgroundRepeat);
        this.backgroundSize = parse$4(context, backgroundSize, declaration.backgroundSize);
        this.borderTopColor = parse$4(context, borderTopColor, declaration.borderTopColor);
        this.borderRightColor = parse$4(context, borderRightColor, declaration.borderRightColor);
        this.borderBottomColor = parse$4(context, borderBottomColor, declaration.borderBottomColor);
        this.borderLeftColor = parse$4(context, borderLeftColor, declaration.borderLeftColor);
        this.borderTopLeftRadius = parse$4(context, borderTopLeftRadius, declaration.borderTopLeftRadius);
        this.borderTopRightRadius = parse$4(context, borderTopRightRadius, declaration.borderTopRightRadius);
        this.borderBottomRightRadius = parse$4(context, borderBottomRightRadius, declaration.borderBottomRightRadius);
        this.borderBottomLeftRadius = parse$4(context, borderBottomLeftRadius, declaration.borderBottomLeftRadius);
        this.borderTopStyle = parse$4(context, borderTopStyle, declaration.borderTopStyle);
        this.borderRightStyle = parse$4(context, borderRightStyle, declaration.borderRightStyle);
        this.borderBottomStyle = parse$4(context, borderBottomStyle, declaration.borderBottomStyle);
        this.borderLeftStyle = parse$4(context, borderLeftStyle, declaration.borderLeftStyle);
        this.borderTopWidth = parse$4(context, borderTopWidth, declaration.borderTopWidth);
        this.borderRightWidth = parse$4(context, borderRightWidth, declaration.borderRightWidth);
        this.borderBottomWidth = parse$4(context, borderBottomWidth, declaration.borderBottomWidth);
        this.borderLeftWidth = parse$4(context, borderLeftWidth, declaration.borderLeftWidth);
        this.boxShadow = parse$4(context, boxShadow, declaration.boxShadow);
        this.color = parse$4(context, color, declaration.color);
        this.direction = parse$4(context, direction, declaration.direction);
        this.display = parse$4(context, display, declaration.display);
        this.float = parse$4(context, float, declaration.cssFloat);
        this.fontFamily = parse$4(context, fontFamily, declaration.fontFamily);
        this.fontSize = parse$4(context, fontSize, declaration.fontSize);
        this.fontStyle = parse$4(context, fontStyle$1, declaration.fontStyle);
        this.fontVariant = parse$4(context, fontVariant, declaration.fontVariant);
        this.fontWeight = parse$4(context, fontWeight, declaration.fontWeight);
        this.letterSpacing = parse$4(context, letterSpacing, declaration.letterSpacing);
        this.lineBreak = parse$4(context, lineBreak, declaration.lineBreak);
        this.lineHeight = parse$4(context, lineHeight, declaration.lineHeight);
        this.listStyleImage = parse$4(context, listStyleImage, declaration.listStyleImage);
        this.listStylePosition = parse$4(context, listStylePosition, declaration.listStylePosition);
        this.listStyleType = parse$4(context, listStyleType, declaration.listStyleType);
        this.marginTop = parse$4(context, marginTop, declaration.marginTop);
        this.marginRight = parse$4(context, marginRight, declaration.marginRight);
        this.marginBottom = parse$4(context, marginBottom, declaration.marginBottom);
        this.marginLeft = parse$4(context, marginLeft, declaration.marginLeft);
        this.opacity = parse$4(context, opacity, declaration.opacity);
        var overflowTuple = parse$4(context, overflow, declaration.overflow);
        this.overflowX = overflowTuple[0];
        this.overflowY = overflowTuple[overflowTuple.length > 1 ? 1 : 0];
        this.overflowWrap = parse$4(context, overflowWrap, declaration.overflowWrap);
        this.paddingTop = parse$4(context, paddingTop, declaration.paddingTop);
        this.paddingRight = parse$4(context, paddingRight, declaration.paddingRight);
        this.paddingBottom = parse$4(context, paddingBottom, declaration.paddingBottom);
        this.paddingLeft = parse$4(context, paddingLeft, declaration.paddingLeft);
        this.paintOrder = parse$4(context, paintOrder, declaration.paintOrder);
        this.position = parse$4(context, position, declaration.position);
        this.textAlign = parse$4(context, textAlign, declaration.textAlign);
        this.textDecorationColor = parse$4(context, textDecorationColor, (_a = declaration.textDecorationColor) !== null && _a !== void 0 ? _a : declaration.color);
        this.textDecorationLine = parse$4(context, textDecorationLine, (_b = declaration.textDecorationLine) !== null && _b !== void 0 ? _b : declaration.textDecoration);
        this.textShadow = parse$4(context, textShadow, declaration.textShadow);
        this.textTransform = parse$4(context, textTransform, declaration.textTransform);
        this.transform = parse$4(context, transform$1, declaration.transform);
        this.transformOrigin = parse$4(context, transformOrigin, declaration.transformOrigin);
        this.visibility = parse$4(context, visibility, declaration.visibility);
        this.webkitTextStrokeColor = parse$4(context, webkitTextStrokeColor, declaration.webkitTextStrokeColor);
        this.webkitTextStrokeWidth = parse$4(context, webkitTextStrokeWidth, declaration.webkitTextStrokeWidth);
        this.wordBreak = parse$4(context, wordBreak, declaration.wordBreak);
        this.zIndex = parse$4(context, zIndex, declaration.zIndex);
    }
    CSSParsedDeclaration.prototype.isVisible = function () {
        return this.display > 0 && this.opacity > 0 && this.visibility === 0 /* VISIBLE */;
    };
    CSSParsedDeclaration.prototype.isTransparent = function () {
        return isTransparent(this.backgroundColor);
    };
    CSSParsedDeclaration.prototype.isTransformed = function () {
        return this.transform !== null;
    };
    CSSParsedDeclaration.prototype.isPositioned = function () {
        return this.position !== 0 /* STATIC */;
    };
    CSSParsedDeclaration.prototype.isPositionedWithZIndex = function () {
        return this.isPositioned() && !this.zIndex.auto;
    };
    CSSParsedDeclaration.prototype.isFloating = function () {
        return this.float !== 0 /* NONE */;
    };
    CSSParsedDeclaration.prototype.isInlineLevel = function () {
        return (contains(this.display, 4 /* INLINE */) ||
            contains(this.display, 33554432 /* INLINE_BLOCK */) ||
            contains(this.display, 268435456 /* INLINE_FLEX */) ||
            contains(this.display, 536870912 /* INLINE_GRID */) ||
            contains(this.display, 67108864 /* INLINE_LIST_ITEM */) ||
            contains(this.display, 134217728 /* INLINE_TABLE */));
    };
    return CSSParsedDeclaration;
}());
var CSSParsedPseudoDeclaration = /** @class */ (function () {
    function CSSParsedPseudoDeclaration(context, declaration) {
        this.content = parse$4(context, content, declaration.content);
        this.quotes = parse$4(context, quotes, declaration.quotes);
    }
    return CSSParsedPseudoDeclaration;
}());
var CSSParsedCounterDeclaration = /** @class */ (function () {
    function CSSParsedCounterDeclaration(context, declaration) {
        this.counterIncrement = parse$4(context, counterIncrement, declaration.counterIncrement);
        this.counterReset = parse$4(context, counterReset, declaration.counterReset);
    }
    return CSSParsedCounterDeclaration;
}());
// eslint-disable-next-line @typescript-eslint/no-explicit-any
var parse$4 = function (context, descriptor, style) {
    var tokenizer = new Tokenizer();
    var value = style !== null && typeof style !== 'undefined' ? style.toString() : descriptor.initialValue;
    tokenizer.write(value);
    var parser = new Parser(tokenizer.read());
    switch (descriptor.type) {
        case 2 /* IDENT_VALUE */:
            var token = parser.parseComponentValue();
            return descriptor.parse(context, isIdentToken(token) ? token.value : descriptor.initialValue);
        case 0 /* VALUE */:
            return descriptor.parse(context, parser.parseComponentValue());
        case 1 /* LIST */:
            return descriptor.parse(context, parser.parseComponentValues());
        case 4 /* TOKEN_VALUE */:
            return parser.parseComponentValue();
        case 3 /* TYPE_VALUE */:
            switch (descriptor.format) {
                case 'angle':
                    return angle$1.parse(context, parser.parseComponentValue());
                case 'color':
                    return color$1.parse(context, parser.parseComponentValue());
                case 'image':
                    return image$1.parse(context, parser.parseComponentValue());
                case 'length':
                    var length_1 = parser.parseComponentValue();
                    return isLength(length_1) ? length_1 : ZERO_LENGTH;
                case 'length-percentage':
                    var value_1 = parser.parseComponentValue();
                    return isLengthPercentage(value_1) ? value_1 : ZERO_LENGTH;
                case 'time':
                    return time.parse(context, parser.parseComponentValue());
            }
            break;
    }
};

var elementDebuggerAttribute = 'data-html2canvas-debug';
var getElementDebugType = function (element) {
    var attribute = element.getAttribute(elementDebuggerAttribute);
    switch (attribute) {
        case 'all':
            return 1 /* ALL */;
        case 'clone':
            return 2 /* CLONE */;
        case 'parse':
            return 3 /* PARSE */;
        case 'render':
            return 4 /* RENDER */;
        default:
            return 0 /* NONE */;
    }
};
var isDebugging = function (element, type) {
    var elementType = getElementDebugType(element);
    return elementType === 1 /* ALL */ || type === elementType;
};

var ElementContainer = /** @class */ (function () {
    function ElementContainer(context, element) {
        this.context = context;
        this.textNodes = [];
        this.elements = [];
        this.flags = 0;
        if (isDebugging(element, 3 /* PARSE */)) {
            debugger;
        }
        this.styles = new CSSParsedDeclaration(context, window.getComputedStyle(element, null));
        if (isHTMLElementNode(element)) {
            if (this.styles.animationDuration.some(function (duration) { return duration > 0; })) {
                element.style.animationDuration = '0s';
            }
            if (this.styles.transform !== null) {
                // getBoundingClientRect takes transforms into account
                element.style.transform = 'none';
            }
        }
        this.bounds = parseBounds(this.context, element);
        if (isDebugging(element, 4 /* RENDER */)) {
            this.flags |= 16 /* DEBUG_RENDER */;
        }
    }
    return ElementContainer;
}());

/*
 * text-segmentation 1.0.3 <https://github.com/niklasvh/text-segmentation>
 * Copyright (c) 2022 Niklas von Hertzen <https://hertzen.com>
 * Released under MIT License
 */
var base64 = ' ';

/*
 * utrie 1.0.2 <https://github.com/niklasvh/utrie>
 * Copyright (c) 2022 Niklas von Hertzen <https://hertzen.com>
 * Released under MIT License
 */
var chars$1 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
// Use a lookup table to find the index.
var lookup$1 = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);
for (var i$1 = 0; i$1 < chars$1.length; i$1++) {
    lookup$1[chars$1.charCodeAt(i$1)] = i$1;
}
var decode$a = function (base64) {
    var bufferLength = base64.length * 0.75, len = base64.length, i, p = 0, encoded1, encoded2, encoded3, encoded4;
    if (base64[base64.length - 1] === '=') {
        bufferLength--;
        if (base64[base64.length - 2] === '=') {
            bufferLength--;
        }
    }
    var buffer = typeof ArrayBuffer !== 'undefined' &&
        typeof Uint8Array !== 'undefined' &&
        typeof Uint8Array.prototype.slice !== 'undefined'
        ? new ArrayBuffer(bufferLength)
        : new Array(bufferLength);
    var bytes = Array.isArray(buffer) ? buffer : new Uint8Array(buffer);
    for (i = 0; i < len; i += 4) {
        encoded1 = lookup$1[base64.charCodeAt(i)];
        encoded2 = lookup$1[base64.charCodeAt(i + 1)];
        encoded3 = lookup$1[base64.charCodeAt(i + 2)];
        encoded4 = lookup$1[base64.charCodeAt(i + 3)];
        bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
        bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
        bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
    }
    return buffer;
};
var polyUint16Array = function (buffer) {
    var length = buffer.length;
    var bytes = [];
    for (var i = 0; i < length; i += 2) {
        bytes.push((buffer[i + 1] << 8) | buffer[i]);
    }
    return bytes;
};
var polyUint32Array = function (buffer) {
    var length = buffer.length;
    var bytes = [];
    for (var i = 0; i < length; i += 4) {
        bytes.push((buffer[i + 3] << 24) | (buffer[i + 2] << 16) | (buffer[i + 1] << 8) | buffer[i]);
    }
    return bytes;
};

/** Shift size for getting the index-2 table offset. */
var UTRIE2_SHIFT_2 = 5;
/** Shift size for getting the index-1 table offset. */
var UTRIE2_SHIFT_1 = 6 + 5;
/**
 * Shift size for shifting left the index array values.
 * Increases possible data size with 16-bit index values at the cost
 * of compactability.
 * This requires data blocks to be aligned by UTRIE2_DATA_GRANULARITY.
 */
var UTRIE2_INDEX_SHIFT = 2;
/**
 * Difference between the two shift sizes,
 * for getting an index-1 offset from an index-2 offset. 6=11-5
 */
var UTRIE2_SHIFT_1_2 = UTRIE2_SHIFT_1 - UTRIE2_SHIFT_2;
/**
 * The part of the index-2 table for U+D800..U+DBFF stores values for
 * lead surrogate code _units_ not code _points_.
 * Values for lead surrogate code _points_ are indexed with this portion of the table.
 * Length=32=0x20=0x400>>UTRIE2_SHIFT_2. (There are 1024=0x400 lead surrogates.)
 */
var UTRIE2_LSCP_INDEX_2_OFFSET = 0x10000 >> UTRIE2_SHIFT_2;
/** Number of entries in a data block. 32=0x20 */
var UTRIE2_DATA_BLOCK_LENGTH = 1 << UTRIE2_SHIFT_2;
/** Mask for getting the lower bits for the in-data-block offset. */
var UTRIE2_DATA_MASK = UTRIE2_DATA_BLOCK_LENGTH - 1;
var UTRIE2_LSCP_INDEX_2_LENGTH = 0x400 >> UTRIE2_SHIFT_2;
/** Count the lengths of both BMP pieces. 2080=0x820 */
var UTRIE2_INDEX_2_BMP_LENGTH = UTRIE2_LSCP_INDEX_2_OFFSET + UTRIE2_LSCP_INDEX_2_LENGTH;
/**
 * The 2-byte UTF-8 version of the index-2 table follows at offset 2080=0x820.
 * Length 32=0x20 for lead bytes C0..DF, regardless of UTRIE2_SHIFT_2.
 */
var UTRIE2_UTF8_2B_INDEX_2_OFFSET = UTRIE2_INDEX_2_BMP_LENGTH;
var UTRIE2_UTF8_2B_INDEX_2_LENGTH = 0x800 >> 6; /* U+0800 is the first code point after 2-byte UTF-8 */
/**
 * The index-1 table, only used for supplementary code points, at offset 2112=0x840.
 * Variable length, for code points up to highStart, where the last single-value range starts.
 * Maximum length 512=0x200=0x100000>>UTRIE2_SHIFT_1.
 * (For 0x100000 supplementary code points U+10000..U+10ffff.)
 *
 * The part of the index-2 table for supplementary code points starts
 * after this index-1 table.
 *
 * Both the index-1 table and the following part of the index-2 table
 * are omitted completely if there is only BMP data.
 */
var UTRIE2_INDEX_1_OFFSET = UTRIE2_UTF8_2B_INDEX_2_OFFSET + UTRIE2_UTF8_2B_INDEX_2_LENGTH;
/**
 * Number of index-1 entries for the BMP. 32=0x20
 * This part of the index-1 table is omitted from the serialized form.
 */
var UTRIE2_OMITTED_BMP_INDEX_1_LENGTH = 0x10000 >> UTRIE2_SHIFT_1;
/** Number of entries in an index-2 block. 64=0x40 */
var UTRIE2_INDEX_2_BLOCK_LENGTH = 1 << UTRIE2_SHIFT_1_2;
/** Mask for getting the lower bits for the in-index-2-block offset. */
var UTRIE2_INDEX_2_MASK = UTRIE2_INDEX_2_BLOCK_LENGTH - 1;
var slice16 = function (view, start, end) {
    if (view.slice) {
        return view.slice(start, end);
    }
    return new Uint16Array(Array.prototype.slice.call(view, start, end));
};
var slice32 = function (view, start, end) {
    if (view.slice) {
        return view.slice(start, end);
    }
    return new Uint32Array(Array.prototype.slice.call(view, start, end));
};
var createTrieFromBase64 = function (base64, _byteLength) {
    var buffer = decode$a(base64);
    var view32 = Array.isArray(buffer) ? polyUint32Array(buffer) : new Uint32Array(buffer);
    var view16 = Array.isArray(buffer) ? polyUint16Array(buffer) : new Uint16Array(buffer);
    var headerLength = 24;
    var index = slice16(view16, headerLength / 2, view32[4] / 2);
    var data = view32[5] === 2
        ? slice16(view16, (headerLength + view32[4]) / 2)
        : slice32(view32, Math.ceil((headerLength + view32[4]) / 4));
    return new Trie(view32[0], view32[1], view32[2], view32[3], index, data);
};
var Trie = /** @class */ (function () {
    function Trie(initialValue, errorValue, highStart, highValueIndex, index, data) {
        this.initialValue = initialValue;
        this.errorValue = errorValue;
        this.highStart = highStart;
        this.highValueIndex = highValueIndex;
        this.index = index;
        this.data = data;
    }
    /**
     * Get the value for a code point as stored in the Trie.
     *
     * @param codePoint the code point
     * @return the value
     */
    Trie.prototype.get = function (codePoint) {
        var ix;
        if (codePoint >= 0) {
            if (codePoint < 0x0d800 || (codePoint > 0x0dbff && codePoint <= 0x0ffff)) {
                // Ordinary BMP code point, excluding leading surrogates.
                // BMP uses a single level lookup.  BMP index starts at offset 0 in the Trie2 index.
                // 16 bit data is stored in the index array itself.
                ix = this.index[codePoint >> UTRIE2_SHIFT_2];
                ix = (ix << UTRIE2_INDEX_SHIFT) + (codePoint & UTRIE2_DATA_MASK);
                return this.data[ix];
            }
            if (codePoint <= 0xffff) {
                // Lead Surrogate Code Point.  A Separate index section is stored for
                // lead surrogate code units and code points.
                //   The main index has the code unit data.
                //   For this function, we need the code point data.
                // Note: this expression could be refactored for slightly improved efficiency, but
                //       surrogate code points will be so rare in practice that it's not worth it.
                ix = this.index[UTRIE2_LSCP_INDEX_2_OFFSET + ((codePoint - 0xd800) >> UTRIE2_SHIFT_2)];
                ix = (ix << UTRIE2_INDEX_SHIFT) + (codePoint & UTRIE2_DATA_MASK);
                return this.data[ix];
            }
            if (codePoint < this.highStart) {
                // Supplemental code point, use two-level lookup.
                ix = UTRIE2_INDEX_1_OFFSET - UTRIE2_OMITTED_BMP_INDEX_1_LENGTH + (codePoint >> UTRIE2_SHIFT_1);
                ix = this.index[ix];
                ix += (codePoint >> UTRIE2_SHIFT_2) & UTRIE2_INDEX_2_MASK;
                ix = this.index[ix];
                ix = (ix << UTRIE2_INDEX_SHIFT) + (codePoint & UTRIE2_DATA_MASK);
                return this.data[ix];
            }
            if (codePoint <= 0x10ffff) {
                return this.data[this.highValueIndex];
            }
        }
        // Fall through.  The code point is outside of the legal range of 0..0x10ffff.
        return this.errorValue;
    };
    return Trie;
}());

/*
 * base64-arraybuffer 1.0.2 <https://github.com/niklasvh/base64-arraybuffer>
 * Copyright (c) 2022 Niklas von Hertzen <https://hertzen.com>
 * Released under MIT License
 */
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
// Use a lookup table to find the index.
var lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);
for (var i = 0; i < chars.length; i++) {
    lookup[chars.charCodeAt(i)] = i;
}

var Prepend = 1;
var CR = 2;
var LF = 3;
var Control = 4;
var Extend = 5;
var SpacingMark = 7;
var L = 8;
var V = 9;
var T = 10;
var LV = 11;
var LVT = 12;
var ZWJ = 13;
var Extended_Pictographic = 14;
var RI = 15;
var toCodePoints = function (str) {
    var codePoints = [];
    var i = 0;
    var length = str.length;
    while (i < length) {
        var value = str.charCodeAt(i++);
        if (value >= 0xd800 && value <= 0xdbff && i < length) {
            var extra = str.charCodeAt(i++);
            if ((extra & 0xfc00) === 0xdc00) {
                codePoints.push(((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000);
            }
            else {
                codePoints.push(value);
                i--;
            }
        }
        else {
            codePoints.push(value);
        }
    }
    return codePoints;
};
var fromCodePoint = function () {
    var codePoints = [];
    for (var _i = 0; _i < arguments.length; _i++) {
        codePoints[_i] = arguments[_i];
    }
    if (String.fromCodePoint) {
        return String.fromCodePoint.apply(String, codePoints);
    }
    var length = codePoints.length;
    if (!length) {
        return '';
    }
    var codeUnits = [];
    var index = -1;
    var result = '';
    while (++index < length) {
        var codePoint = codePoints[index];
        if (codePoint <= 0xffff) {
            codeUnits.push(codePoint);
        }
        else {
            codePoint -= 0x10000;
            codeUnits.push((codePoint >> 10) + 0xd800, (codePoint % 0x400) + 0xdc00);
        }
        if (index + 1 === length || codeUnits.length > 0x4000) {
            result += String.fromCharCode.apply(String, codeUnits);
            codeUnits.length = 0;
        }
    }
    return result;
};
var UnicodeTrie = createTrieFromBase64(base64);
var BREAK_NOT_ALLOWED = '×';
var BREAK_ALLOWED = '÷';
var codePointToClass = function (codePoint) { return UnicodeTrie.get(codePoint); };
var _graphemeBreakAtIndex = function (_codePoints, classTypes, index) {
    var prevIndex = index - 2;
    var prev = classTypes[prevIndex];
    var current = classTypes[index - 1];
    var next = classTypes[index];
    // GB3 Do not break between a CR and LF
    if (current === CR && next === LF) {
        return BREAK_NOT_ALLOWED;
    }
    // GB4 Otherwise, break before and after controls.
    if (current === CR || current === LF || current === Control) {
        return BREAK_ALLOWED;
    }
    // GB5
    if (next === CR || next === LF || next === Control) {
        return BREAK_ALLOWED;
    }
    // Do not break Hangul syllable sequences.
    // GB6
    if (current === L && [L, V, LV, LVT].indexOf(next) !== -1) {
        return BREAK_NOT_ALLOWED;
    }
    // GB7
    if ((current === LV || current === V) && (next === V || next === T)) {
        return BREAK_NOT_ALLOWED;
    }
    // GB8
    if ((current === LVT || current === T) && next === T) {
        return BREAK_NOT_ALLOWED;
    }
    // GB9 Do not break before extending characters or ZWJ.
    if (next === ZWJ || next === Extend) {
        return BREAK_NOT_ALLOWED;
    }
    // Do not break before SpacingMarks, or after Prepend characters.
    // GB9a
    if (next === SpacingMark) {
        return BREAK_NOT_ALLOWED;
    }
    // GB9a
    if (current === Prepend) {
        return BREAK_NOT_ALLOWED;
    }
    // GB11 Do not break within emoji modifier sequences or emoji zwj sequences.
    if (current === ZWJ && next === Extended_Pictographic) {
        while (prev === Extend) {
            prev = classTypes[--prevIndex];
        }
        if (prev === Extended_Pictographic) {
            return BREAK_NOT_ALLOWED;
        }
    }
    // GB12 Do not break within emoji flag sequences.
    // That is, do not break between regional indicator (RI) symbols
    // if there is an odd number of RI characters before the break point.
    if (current === RI && next === RI) {
        var countRI = 0;
        while (prev === RI) {
            countRI++;
            prev = classTypes[--prevIndex];
        }
        if (countRI % 2 === 0) {
            return BREAK_NOT_ALLOWED;
        }
    }
    return BREAK_ALLOWED;
};
var GraphemeBreaker = function (str) {
    var codePoints = toCodePoints(str);
    var length = codePoints.length;
    var index = 0;
    var lastEnd = 0;
    var classTypes = codePoints.map(codePointToClass);
    return {
        next: function () {
            if (index >= length) {
                return { done: true, value: null };
            }
            var graphemeBreak = BREAK_NOT_ALLOWED;
            while (index < length &&
                (graphemeBreak = _graphemeBreakAtIndex(codePoints, classTypes, ++index)) === BREAK_NOT_ALLOWED) { }
            if (graphemeBreak !== BREAK_NOT_ALLOWED || index === length) {
                var value = fromCodePoint.apply(null, codePoints.slice(lastEnd, index));
                lastEnd = index;
                return { value: value, done: false };
            }
            return { done: true, value: null };
        },
    };
};
var splitGraphemes = function (str) {
    var breaker = GraphemeBreaker(str);
    var graphemes = [];
    var bk;
    while (!(bk = breaker.next()).done) {
        if (bk.value) {
            graphemes.push(bk.value.slice());
        }
    }
    return graphemes;
};

var testRangeBounds = function (document) {
    var TEST_HEIGHT = 123;
    if (document.createRange) {
        var range = document.createRange();
        if (range.getBoundingClientRect) {
            var testElement = document.createElement('boundtest');
            testElement.style.height = TEST_HEIGHT + "px";
            testElement.style.display = 'block';
            document.body.appendChild(testElement);
            range.selectNode(testElement);
            var rangeBounds = range.getBoundingClientRect();
            var rangeHeight = Math.round(rangeBounds.height);
            document.body.removeChild(testElement);
            if (rangeHeight === TEST_HEIGHT) {
                return true;
            }
        }
    }
    return false;
};
var testIOSLineBreak = function (document) {
    var testElement = document.createElement('boundtest');
    testElement.style.width = '50px';
    testElement.style.display = 'block';
    testElement.style.fontSize = '12px';
    testElement.style.letterSpacing = '0px';
    testElement.style.wordSpacing = '0px';
    document.body.appendChild(testElement);
    var range = document.createRange();
    testElement.innerHTML = typeof ''.repeat === 'function' ? '&#128104;'.repeat(10) : '';
    var node = testElement.firstChild;
    var textList = toCodePoints$1(node.data).map(function (i) { return fromCodePoint$1(i); });
    var offset = 0;
    var prev = {};
    // ios 13 does not handle range getBoundingClientRect line changes correctly #2177
    var supports = textList.every(function (text, i) {
        range.setStart(node, offset);
        range.setEnd(node, offset + text.length);
        var rect = range.getBoundingClientRect();
        offset += text.length;
        var boundAhead = rect.x > prev.x || rect.y > prev.y;
        prev = rect;
        if (i === 0) {
            return true;
        }
        return boundAhead;
    });
    document.body.removeChild(testElement);
    return supports;
};
var testCORS = function () { return typeof new Image().crossOrigin !== 'undefined'; };
var testResponseType = function () { return typeof new XMLHttpRequest().responseType === 'string'; };
var testSVG = function (document) {
    var img = new Image();
    var canvas = document.createElement('canvas');
    var ctx = canvas.getContext('2d');
    if (!ctx) {
        return false;
    }
    img.src = "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'></svg>";
    try {
        ctx.drawImage(img, 0, 0);
        canvas.toDataURL();
    }
    catch (e) {
        return false;
    }
    return true;
};
var isGreenPixel = function (data) {
    return data[0] === 0 && data[1] === 255 && data[2] === 0 && data[3] === 255;
};
var testForeignObject = function (document) {
    var canvas = document.createElement('canvas');
    var size = 100;
    canvas.width = size;
    canvas.height = size;
    var ctx = canvas.getContext('2d');
    if (!ctx) {
        return Promise.reject(false);
    }
    ctx.fillStyle = 'rgb(0, 255, 0)';
    ctx.fillRect(0, 0, size, size);
    var img = new Image();
    var greenImageSrc = canvas.toDataURL();
    img.src = greenImageSrc;
    var svg = createForeignObjectSVG(size, size, 0, 0, img);
    ctx.fillStyle = 'red';
    ctx.fillRect(0, 0, size, size);
    return loadSerializedSVG$1(svg)
        .then(function (img) {
        ctx.drawImage(img, 0, 0);
        var data = ctx.getImageData(0, 0, size, size).data;
        ctx.fillStyle = 'red';
        ctx.fillRect(0, 0, size, size);
        var node = document.createElement('div');
        node.style.backgroundImage = "url(" + greenImageSrc + ")";
        node.style.height = size + "px";
        // Firefox 55 does not render inline <img /> tags
        return isGreenPixel(data)
            ? loadSerializedSVG$1(createForeignObjectSVG(size, size, 0, 0, node))
            : Promise.reject(false);
    })
        .then(function (img) {
        ctx.drawImage(img, 0, 0);
        // Edge does not render background-images
        return isGreenPixel(ctx.getImageData(0, 0, size, size).data);
    })
        .catch(function () { return false; });
};
var createForeignObjectSVG = function (width, height, x, y, node) {
    var xmlns = 'http://www.w3.org/2000/svg';
    var svg = document.createElementNS(xmlns, 'svg');
    var foreignObject = document.createElementNS(xmlns, 'foreignObject');
    svg.setAttributeNS(null, 'width', width.toString());
    svg.setAttributeNS(null, 'height', height.toString());
    foreignObject.setAttributeNS(null, 'width', '100%');
    foreignObject.setAttributeNS(null, 'height', '100%');
    foreignObject.setAttributeNS(null, 'x', x.toString());
    foreignObject.setAttributeNS(null, 'y', y.toString());
    foreignObject.setAttributeNS(null, 'externalResourcesRequired', 'true');
    svg.appendChild(foreignObject);
    foreignObject.appendChild(node);
    return svg;
};
var loadSerializedSVG$1 = function (svg) {
    return new Promise(function (resolve, reject) {
        var img = new Image();
        img.onload = function () { return resolve(img); };
        img.onerror = reject;
        img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(new XMLSerializer().serializeToString(svg));
    });
};
var FEATURES = {
    get SUPPORT_RANGE_BOUNDS() {
        var value = testRangeBounds(document);
        Object.defineProperty(FEATURES, 'SUPPORT_RANGE_BOUNDS', { value: value });
        return value;
    },
    get SUPPORT_WORD_BREAKING() {
        var value = FEATURES.SUPPORT_RANGE_BOUNDS && testIOSLineBreak(document);
        Object.defineProperty(FEATURES, 'SUPPORT_WORD_BREAKING', { value: value });
        return value;
    },
    get SUPPORT_SVG_DRAWING() {
        var value = testSVG(document);
        Object.defineProperty(FEATURES, 'SUPPORT_SVG_DRAWING', { value: value });
        return value;
    },
    get SUPPORT_FOREIGNOBJECT_DRAWING() {
        var value = typeof Array.from === 'function' && typeof window.fetch === 'function'
            ? testForeignObject(document)
            : Promise.resolve(false);
        Object.defineProperty(FEATURES, 'SUPPORT_FOREIGNOBJECT_DRAWING', { value: value });
        return value;
    },
    get SUPPORT_CORS_IMAGES() {
        var value = testCORS();
        Object.defineProperty(FEATURES, 'SUPPORT_CORS_IMAGES', { value: value });
        return value;
    },
    get SUPPORT_RESPONSE_TYPE() {
        var value = testResponseType();
        Object.defineProperty(FEATURES, 'SUPPORT_RESPONSE_TYPE', { value: value });
        return value;
    },
    get SUPPORT_CORS_XHR() {
        var value = 'withCredentials' in new XMLHttpRequest();
        Object.defineProperty(FEATURES, 'SUPPORT_CORS_XHR', { value: value });
        return value;
    },
    get SUPPORT_NATIVE_TEXT_SEGMENTATION() {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        var value = !!(typeof Intl !== 'undefined' && Intl.Segmenter);
        Object.defineProperty(FEATURES, 'SUPPORT_NATIVE_TEXT_SEGMENTATION', { value: value });
        return value;
    }
};

var TextBounds = /** @class */ (function () {
    function TextBounds(text, bounds) {
        this.text = text;
        this.bounds = bounds;
    }
    return TextBounds;
}());
var parseTextBounds = function (context, value, styles, node) {
    var textList = breakText(value, styles);
    var textBounds = [];
    var offset = 0;
    textList.forEach(function (text) {
        if (styles.textDecorationLine.length || text.trim().length > 0) {
            if (FEATURES.SUPPORT_RANGE_BOUNDS) {
                var clientRects = createRange(node, offset, text.length).getClientRects();
                if (clientRects.length > 1) {
                    var subSegments = segmentGraphemes(text);
                    var subOffset_1 = 0;
                    subSegments.forEach(function (subSegment) {
                        textBounds.push(new TextBounds(subSegment, Bounds.fromDOMRectList(context, createRange(node, subOffset_1 + offset, subSegment.length).getClientRects())));
                        subOffset_1 += subSegment.length;
                    });
                }
                else {
                    textBounds.push(new TextBounds(text, Bounds.fromDOMRectList(context, clientRects)));
                }
            }
            else {
                var replacementNode = node.splitText(text.length);
                textBounds.push(new TextBounds(text, getWrapperBounds(context, node)));
                node = replacementNode;
            }
        }
        else if (!FEATURES.SUPPORT_RANGE_BOUNDS) {
            node = node.splitText(text.length);
        }
        offset += text.length;
    });
    return textBounds;
};
var getWrapperBounds = function (context, node) {
    var ownerDocument = node.ownerDocument;
    if (ownerDocument) {
        var wrapper = ownerDocument.createElement('html2canvaswrapper');
        wrapper.appendChild(node.cloneNode(true));
        var parentNode = node.parentNode;
        if (parentNode) {
            parentNode.replaceChild(wrapper, node);
            var bounds = parseBounds(context, wrapper);
            if (wrapper.firstChild) {
                parentNode.replaceChild(wrapper.firstChild, wrapper);
            }
            return bounds;
        }
    }
    return Bounds.EMPTY;
};
var createRange = function (node, offset, length) {
    var ownerDocument = node.ownerDocument;
    if (!ownerDocument) {
        throw new Error('Node has no owner document');
    }
    var range = ownerDocument.createRange();
    range.setStart(node, offset);
    range.setEnd(node, offset + length);
    return range;
};
var segmentGraphemes = function (value) {
    if (FEATURES.SUPPORT_NATIVE_TEXT_SEGMENTATION) {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        var segmenter = new Intl.Segmenter(void 0, { granularity: 'grapheme' });
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        return Array.from(segmenter.segment(value)).map(function (segment) { return segment.segment; });
    }
    return splitGraphemes(value);
};
var segmentWords = function (value, styles) {
    if (FEATURES.SUPPORT_NATIVE_TEXT_SEGMENTATION) {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        var segmenter = new Intl.Segmenter(void 0, {
            granularity: 'word'
        });
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        return Array.from(segmenter.segment(value)).map(function (segment) { return segment.segment; });
    }
    return breakWords(value, styles);
};
var breakText = function (value, styles) {
    return styles.letterSpacing !== 0 ? segmentGraphemes(value) : segmentWords(value, styles);
};
// https://drafts.csswg.org/css-text/#word-separator
var wordSeparators = [0x0020, 0x00a0, 0x1361, 0x10100, 0x10101, 0x1039, 0x1091];
var breakWords = function (str, styles) {
    var breaker = LineBreaker(str, {
        lineBreak: styles.lineBreak,
        wordBreak: styles.overflowWrap === "break-word" /* BREAK_WORD */ ? 'break-word' : styles.wordBreak
    });
    var words = [];
    var bk;
    var _loop_1 = function () {
        if (bk.value) {
            var value = bk.value.slice();
            var codePoints = toCodePoints$1(value);
            var word_1 = '';
            codePoints.forEach(function (codePoint) {
                if (wordSeparators.indexOf(codePoint) === -1) {
                    word_1 += fromCodePoint$1(codePoint);
                }
                else {
                    if (word_1.length) {
                        words.push(word_1);
                    }
                    words.push(fromCodePoint$1(codePoint));
                    word_1 = '';
                }
            });
            if (word_1.length) {
                words.push(word_1);
            }
        }
    };
    while (!(bk = breaker.next()).done) {
        _loop_1();
    }
    return words;
};

var TextContainer = /** @class */ (function () {
    function TextContainer(context, node, styles) {
        this.text = transform(node.data, styles.textTransform);
        this.textBounds = parseTextBounds(context, this.text, styles, node);
    }
    return TextContainer;
}());
var transform = function (text, transform) {
    switch (transform) {
        case 1 /* LOWERCASE */:
            return text.toLowerCase();
        case 3 /* CAPITALIZE */:
            return text.replace(CAPITALIZE, capitalize);
        case 2 /* UPPERCASE */:
            return text.toUpperCase();
        default:
            return text;
    }
};
var CAPITALIZE = /(^|\s|:|-|\(|\))([a-z])/g;
var capitalize = function (m, p1, p2) {
    if (m.length > 0) {
        return p1 + p2.toUpperCase();
    }
    return m;
};

var ImageElementContainer = /** @class */ (function (_super) {
    __extends(ImageElementContainer, _super);
    function ImageElementContainer(context, img) {
        var _this = _super.call(this, context, img) || this;
        _this.src = img.currentSrc || img.src;
        _this.intrinsicWidth = img.naturalWidth;
        _this.intrinsicHeight = img.naturalHeight;
        _this.context.cache.addImage(_this.src);
        return _this;
    }
    return ImageElementContainer;
}(ElementContainer));

var CanvasElementContainer = /** @class */ (function (_super) {
    __extends(CanvasElementContainer, _super);
    function CanvasElementContainer(context, canvas) {
        var _this = _super.call(this, context, canvas) || this;
        _this.canvas = canvas;
        _this.intrinsicWidth = canvas.width;
        _this.intrinsicHeight = canvas.height;
        return _this;
    }
    return CanvasElementContainer;
}(ElementContainer));

var SVGElementContainer = /** @class */ (function (_super) {
    __extends(SVGElementContainer, _super);
    function SVGElementContainer(context, img) {
        var _this = _super.call(this, context, img) || this;
        var s = new XMLSerializer();
        var bounds = parseBounds(context, img);
        img.setAttribute('width', bounds.width + "px");
        img.setAttribute('height', bounds.height + "px");
        _this.svg = "data:image/svg+xml," + encodeURIComponent(s.serializeToString(img));
        _this.intrinsicWidth = img.width.baseVal.value;
        _this.intrinsicHeight = img.height.baseVal.value;
        _this.context.cache.addImage(_this.svg);
        return _this;
    }
    return SVGElementContainer;
}(ElementContainer));

var LIElementContainer = /** @class */ (function (_super) {
    __extends(LIElementContainer, _super);
    function LIElementContainer(context, element) {
        var _this = _super.call(this, context, element) || this;
        _this.value = element.value;
        return _this;
    }
    return LIElementContainer;
}(ElementContainer));

var OLElementContainer = /** @class */ (function (_super) {
    __extends(OLElementContainer, _super);
    function OLElementContainer(context, element) {
        var _this = _super.call(this, context, element) || this;
        _this.start = element.start;
        _this.reversed = typeof element.reversed === 'boolean' && element.reversed === true;
        return _this;
    }
    return OLElementContainer;
}(ElementContainer));

var CHECKBOX_BORDER_RADIUS = [
    {
        type: 15 /* DIMENSION_TOKEN */,
        flags: 0,
        unit: 'px',
        number: 3
    }
];
var RADIO_BORDER_RADIUS = [
    {
        type: 16 /* PERCENTAGE_TOKEN */,
        flags: 0,
        number: 50
    }
];
var reformatInputBounds = function (bounds) {
    if (bounds.width > bounds.height) {
        return new Bounds(bounds.left + (bounds.width - bounds.height) / 2, bounds.top, bounds.height, bounds.height);
    }
    else if (bounds.width < bounds.height) {
        return new Bounds(bounds.left, bounds.top + (bounds.height - bounds.width) / 2, bounds.width, bounds.width);
    }
    return bounds;
};
var getInputValue = function (node) {
    var value = node.type === PASSWORD ? new Array(node.value.length + 1).join('\u2022') : node.value;
    return value.length === 0 ? node.placeholder || '' : value;
};
var CHECKBOX = 'checkbox';
var RADIO = 'radio';
var PASSWORD = 'password';
var INPUT_COLOR = 0x2a2a2aff;
var InputElementContainer = /** @class */ (function (_super) {
    __extends(InputElementContainer, _super);
    function InputElementContainer(context, input) {
        var _this = _super.call(this, context, input) || this;
        _this.type = input.type.toLowerCase();
        _this.checked = input.checked;
        _this.value = getInputValue(input);
        if (_this.type === CHECKBOX || _this.type === RADIO) {
            _this.styles.backgroundColor = 0xdededeff;
            _this.styles.borderTopColor =
                _this.styles.borderRightColor =
                    _this.styles.borderBottomColor =
                        _this.styles.borderLeftColor =
                            0xa5a5a5ff;
            _this.styles.borderTopWidth =
                _this.styles.borderRightWidth =
                    _this.styles.borderBottomWidth =
                        _this.styles.borderLeftWidth =
                            1;
            _this.styles.borderTopStyle =
                _this.styles.borderRightStyle =
                    _this.styles.borderBottomStyle =
                        _this.styles.borderLeftStyle =
                            1 /* SOLID */;
            _this.styles.backgroundClip = [0 /* BORDER_BOX */];
            _this.styles.backgroundOrigin = [0 /* BORDER_BOX */];
            _this.bounds = reformatInputBounds(_this.bounds);
        }
        switch (_this.type) {
            case CHECKBOX:
                _this.styles.borderTopRightRadius =
                    _this.styles.borderTopLeftRadius =
                        _this.styles.borderBottomRightRadius =
                            _this.styles.borderBottomLeftRadius =
                                CHECKBOX_BORDER_RADIUS;
                break;
            case RADIO:
                _this.styles.borderTopRightRadius =
                    _this.styles.borderTopLeftRadius =
                        _this.styles.borderBottomRightRadius =
                            _this.styles.borderBottomLeftRadius =
                                RADIO_BORDER_RADIUS;
                break;
        }
        return _this;
    }
    return InputElementContainer;
}(ElementContainer));

var SelectElementContainer = /** @class */ (function (_super) {
    __extends(SelectElementContainer, _super);
    function SelectElementContainer(context, element) {
        var _this = _super.call(this, context, element) || this;
        var option = element.options[element.selectedIndex || 0];
        _this.value = option ? option.text || '' : '';
        return _this;
    }
    return SelectElementContainer;
}(ElementContainer));

var TextareaElementContainer = /** @class */ (function (_super) {
    __extends(TextareaElementContainer, _super);
    function TextareaElementContainer(context, element) {
        var _this = _super.call(this, context, element) || this;
        _this.value = element.value;
        return _this;
    }
    return TextareaElementContainer;
}(ElementContainer));

var IFrameElementContainer = /** @class */ (function (_super) {
    __extends(IFrameElementContainer, _super);
    function IFrameElementContainer(context, iframe) {
        var _this = _super.call(this, context, iframe) || this;
        _this.src = iframe.src;
        _this.width = parseInt(iframe.width, 10) || 0;
        _this.height = parseInt(iframe.height, 10) || 0;
        _this.backgroundColor = _this.styles.backgroundColor;
        try {
            if (iframe.contentWindow &&
                iframe.contentWindow.document &&
                iframe.contentWindow.document.documentElement) {
                _this.tree = parseTree(context, iframe.contentWindow.document.documentElement);
                // http://www.w3.org/TR/css3-background/#special-backgrounds
                var documentBackgroundColor = iframe.contentWindow.document.documentElement
                    ? parseColor(context, getComputedStyle(iframe.contentWindow.document.documentElement).backgroundColor)
                    : COLORS.TRANSPARENT;
                var bodyBackgroundColor = iframe.contentWindow.document.body
                    ? parseColor(context, getComputedStyle(iframe.contentWindow.document.body).backgroundColor)
                    : COLORS.TRANSPARENT;
                _this.backgroundColor = isTransparent(documentBackgroundColor)
                    ? isTransparent(bodyBackgroundColor)
                        ? _this.styles.backgroundColor
                        : bodyBackgroundColor
                    : documentBackgroundColor;
            }
        }
        catch (e) { }
        return _this;
    }
    return IFrameElementContainer;
}(ElementContainer));

var LIST_OWNERS = ['OL', 'UL', 'MENU'];
var parseNodeTree = function (context, node, parent, root) {
    for (var childNode = node.firstChild, nextNode = void 0; childNode; childNode = nextNode) {
        nextNode = childNode.nextSibling;
        if (isTextNode(childNode) && childNode.data.trim().length > 0) {
            parent.textNodes.push(new TextContainer(context, childNode, parent.styles));
        }
        else if (isElementNode(childNode)) {
            if (isSlotElement(childNode) && childNode.assignedNodes) {
                childNode.assignedNodes().forEach(function (childNode) { return parseNodeTree(context, childNode, parent, root); });
            }
            else {
                var container = createContainer(context, childNode);
                if (container.styles.isVisible()) {
                    if (createsRealStackingContext(childNode, container, root)) {
                        container.flags |= 4 /* CREATES_REAL_STACKING_CONTEXT */;
                    }
                    else if (createsStackingContext(container.styles)) {
                        container.flags |= 2 /* CREATES_STACKING_CONTEXT */;
                    }
                    if (LIST_OWNERS.indexOf(childNode.tagName) !== -1) {
                        container.flags |= 8 /* IS_LIST_OWNER */;
                    }
                    parent.elements.push(container);
                    childNode.slot;
                    if (childNode.shadowRoot) {
                        parseNodeTree(context, childNode.shadowRoot, container, root);
                    }
                    else if (!isTextareaElement(childNode) &&
                        !isSVGElement(childNode) &&
                        !isSelectElement(childNode)) {
                        parseNodeTree(context, childNode, container, root);
                    }
                }
            }
        }
    }
};
var createContainer = function (context, element) {
    if (isImageElement(element)) {
        return new ImageElementContainer(context, element);
    }
    if (isCanvasElement(element)) {
        return new CanvasElementContainer(context, element);
    }
    if (isSVGElement(element)) {
        return new SVGElementContainer(context, element);
    }
    if (isLIElement(element)) {
        return new LIElementContainer(context, element);
    }
    if (isOLElement(element)) {
        return new OLElementContainer(context, element);
    }
    if (isInputElement(element)) {
        return new InputElementContainer(context, element);
    }
    if (isSelectElement(element)) {
        return new SelectElementContainer(context, element);
    }
    if (isTextareaElement(element)) {
        return new TextareaElementContainer(context, element);
    }
    if (isIFrameElement(element)) {
        return new IFrameElementContainer(context, element);
    }
    return new ElementContainer(context, element);
};
var parseTree = function (context, element) {
    var container = createContainer(context, element);
    container.flags |= 4 /* CREATES_REAL_STACKING_CONTEXT */;
    parseNodeTree(context, element, container, container);
    return container;
};
var createsRealStackingContext = function (node, container, root) {
    return (container.styles.isPositionedWithZIndex() ||
        container.styles.opacity < 1 ||
        container.styles.isTransformed() ||
        (isBodyElement(node) && root.styles.isTransparent()));
};
var createsStackingContext = function (styles) { return styles.isPositioned() || styles.isFloating(); };
var isTextNode = function (node) { return node.nodeType === Node.TEXT_NODE; };
var isElementNode = function (node) { return node.nodeType === Node.ELEMENT_NODE; };
var isHTMLElementNode = function (node) {
    return isElementNode(node) && typeof node.style !== 'undefined' && !isSVGElementNode(node);
};
var isSVGElementNode = function (element) {
    return typeof element.className === 'object';
};
var isLIElement = function (node) { return node.tagName === 'LI'; };
var isOLElement = function (node) { return node.tagName === 'OL'; };
var isInputElement = function (node) { return node.tagName === 'INPUT'; };
var isHTMLElement = function (node) { return node.tagName === 'HTML'; };
var isSVGElement = function (node) { return node.tagName === 'svg'; };
var isBodyElement = function (node) { return node.tagName === 'BODY'; };
var isCanvasElement = function (node) { return node.tagName === 'CANVAS'; };
var isVideoElement = function (node) { return node.tagName === 'VIDEO'; };
var isImageElement = function (node) { return node.tagName === 'IMG'; };
var isIFrameElement = function (node) { return node.tagName === 'IFRAME'; };
var isStyleElement = function (node) { return node.tagName === 'STYLE'; };
var isScriptElement = function (node) { return node.tagName === 'SCRIPT'; };
var isTextareaElement = function (node) { return node.tagName === 'TEXTAREA'; };
var isSelectElement = function (node) { return node.tagName === 'SELECT'; };
var isSlotElement = function (node) { return node.tagName === 'SLOT'; };
// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
var isCustomElement = function (node) { return node.tagName.indexOf('-') > 0; };

var CounterState = /** @class */ (function () {
    function CounterState() {
        this.counters = {};
    }
    CounterState.prototype.getCounterValue = function (name) {
        var counter = this.counters[name];
        if (counter && counter.length) {
            return counter[counter.length - 1];
        }
        return 1;
    };
    CounterState.prototype.getCounterValues = function (name) {
        var counter = this.counters[name];
        return counter ? counter : [];
    };
    CounterState.prototype.pop = function (counters) {
        var _this = this;
        counters.forEach(function (counter) { return _this.counters[counter].pop(); });
    };
    CounterState.prototype.parse = function (style) {
        var _this = this;
        var counterIncrement = style.counterIncrement;
        var counterReset = style.counterReset;
        var canReset = true;
        if (counterIncrement !== null) {
            counterIncrement.forEach(function (entry) {
                var counter = _this.counters[entry.counter];
                if (counter && entry.increment !== 0) {
                    canReset = false;
                    if (!counter.length) {
                        counter.push(1);
                    }
                    counter[Math.max(0, counter.length - 1)] += entry.increment;
                }
            });
        }
        var counterNames = [];
        if (canReset) {
            counterReset.forEach(function (entry) {
                var counter = _this.counters[entry.counter];
                counterNames.push(entry.counter);
                if (!counter) {
                    counter = _this.counters[entry.counter] = [];
                }
                counter.push(entry.reset);
            });
        }
        return counterNames;
    };
    return CounterState;
}());
var ROMAN_UPPER = {
    integers: [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1],
    values: ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I']
};
var ARMENIAN = {
    integers: [
        9000, 8000, 7000, 6000, 5000, 4000, 3000, 2000, 1000, 900, 800, 700, 600, 500, 400, 300, 200, 100, 90, 80, 70,
        60, 50, 40, 30, 20, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
    ],
    values: [
        'Ք',
        'Փ',
        'Ւ',
        'Ց',
        'Ր',
        'Տ',
        'Վ',
        'Ս',
        'Ռ',
        'Ջ',
        'Պ',
        'Չ',
        'Ո',
        'Շ',
        'Ն',
        'Յ',
        'Մ',
        'Ճ',
        'Ղ',
        'Ձ',
        'Հ',
        'Կ',
        'Ծ',
        'Խ',
        'Լ',
        'Ի',
        'Ժ',
        'Թ',
        'Ը',
        'Է',
        'Զ',
        'Ե',
        'Դ',
        'Գ',
        'Բ',
        'Ա'
    ]
};
var HEBREW = {
    integers: [
        10000, 9000, 8000, 7000, 6000, 5000, 4000, 3000, 2000, 1000, 400, 300, 200, 100, 90, 80, 70, 60, 50, 40, 30, 20,
        19, 18, 17, 16, 15, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
    ],
    values: [
        'י׳',
        'ט׳',
        'ח׳',
        'ז׳',
        'ו׳',
        'ה׳',
        'ד׳',
        'ג׳',
        'ב׳',
        'א׳',
        'ת',
        'ש',
        'ר',
        'ק',
        'צ',
        'פ',
        'ע',
        'ס',
        'נ',
        'מ',
        'ל',
        'כ',
        'יט',
        'יח',
        'יז',
        'טז',
        'טו',
        'י',
        'ט',
        'ח',
        'ז',
        'ו',
        'ה',
        'ד',
        'ג',
        'ב',
        'א'
    ]
};
var GEORGIAN = {
    integers: [
        10000, 9000, 8000, 7000, 6000, 5000, 4000, 3000, 2000, 1000, 900, 800, 700, 600, 500, 400, 300, 200, 100, 90,
        80, 70, 60, 50, 40, 30, 20, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
    ],
    values: [
        'ჵ',
        'ჰ',
        'ჯ',
        'ჴ',
        'ხ',
        'ჭ',
        'წ',
        'ძ',
        'ც',
        'ჩ',
        'შ',
        'ყ',
        'ღ',
        'ქ',
        'ფ',
        'ჳ',
        'ტ',
        'ს',
        'რ',
        'ჟ',
        'პ',
        'ო',
        'ჲ',
        'ნ',
        'მ',
        'ლ',
        'კ',
        'ი',
        'თ',
        'ჱ',
        'ზ',
        'ვ',
        'ე',
        'დ',
        'გ',
        'ბ',
        'ა'
    ]
};
var createAdditiveCounter = function (value, min, max, symbols, fallback, suffix) {
    if (value < min || value > max) {
        return createCounterText(value, fallback, suffix.length > 0);
    }
    return (symbols.integers.reduce(function (string, integer, index) {
        while (value >= integer) {
            value -= integer;
            string += symbols.values[index];
        }
        return string;
    }, '') + suffix);
};
var createCounterStyleWithSymbolResolver = function (value, codePointRangeLength, isNumeric, resolver) {
    var string = '';
    do {
        if (!isNumeric) {
            value--;
        }
        string = resolver(value) + string;
        value /= codePointRangeLength;
    } while (value * codePointRangeLength >= codePointRangeLength);
    return string;
};
var createCounterStyleFromRange = function (value, codePointRangeStart, codePointRangeEnd, isNumeric, suffix) {
    var codePointRangeLength = codePointRangeEnd - codePointRangeStart + 1;
    return ((value < 0 ? '-' : '') +
        (createCounterStyleWithSymbolResolver(Math.abs(value), codePointRangeLength, isNumeric, function (codePoint) {
            return fromCodePoint$1(Math.floor(codePoint % codePointRangeLength) + codePointRangeStart);
        }) +
            suffix));
};
var createCounterStyleFromSymbols = function (value, symbols, suffix) {
    if (suffix === void 0) { suffix = '. '; }
    var codePointRangeLength = symbols.length;
    return (createCounterStyleWithSymbolResolver(Math.abs(value), codePointRangeLength, false, function (codePoint) { return symbols[Math.floor(codePoint % codePointRangeLength)]; }) + suffix);
};
var CJK_ZEROS = 1 << 0;
var CJK_TEN_COEFFICIENTS = 1 << 1;
var CJK_TEN_HIGH_COEFFICIENTS = 1 << 2;
var CJK_HUNDRED_COEFFICIENTS = 1 << 3;
var createCJKCounter = function (value, numbers, multipliers, negativeSign, suffix, flags) {
    if (value < -9999 || value > 9999) {
        return createCounterText(value, 4 /* CJK_DECIMAL */, suffix.length > 0);
    }
    var tmp = Math.abs(value);
    var string = suffix;
    if (tmp === 0) {
        return numbers[0] + string;
    }
    for (var digit = 0; tmp > 0 && digit <= 4; digit++) {
        var coefficient = tmp % 10;
        if (coefficient === 0 && contains(flags, CJK_ZEROS) && string !== '') {
            string = numbers[coefficient] + string;
        }
        else if (coefficient > 1 ||
            (coefficient === 1 && digit === 0) ||
            (coefficient === 1 && digit === 1 && contains(flags, CJK_TEN_COEFFICIENTS)) ||
            (coefficient === 1 && digit === 1 && contains(flags, CJK_TEN_HIGH_COEFFICIENTS) && value > 100) ||
            (coefficient === 1 && digit > 1 && contains(flags, CJK_HUNDRED_COEFFICIENTS))) {
            string = numbers[coefficient] + (digit > 0 ? multipliers[digit - 1] : '') + string;
        }
        else if (coefficient === 1 && digit > 0) {
            string = multipliers[digit - 1] + string;
        }
        tmp = Math.floor(tmp / 10);
    }
    return (value < 0 ? negativeSign : '') + string;
};
var CHINESE_INFORMAL_MULTIPLIERS = '十百千萬';
var CHINESE_FORMAL_MULTIPLIERS = '拾佰仟萬';
var JAPANESE_NEGATIVE = 'マイナス';
var KOREAN_NEGATIVE = '마이너스';
var createCounterText = function (value, type, appendSuffix) {
    var defaultSuffix = appendSuffix ? '. ' : '';
    var cjkSuffix = appendSuffix ? '、' : '';
    var koreanSuffix = appendSuffix ? ', ' : '';
    var spaceSuffix = appendSuffix ? ' ' : '';
    switch (type) {
        case 0 /* DISC */:
            return '•' + spaceSuffix;
        case 1 /* CIRCLE */:
            return '◦' + spaceSuffix;
        case 2 /* SQUARE */:
            return '◾' + spaceSuffix;
        case 5 /* DECIMAL_LEADING_ZERO */:
            var string = createCounterStyleFromRange(value, 48, 57, true, defaultSuffix);
            return string.length < 4 ? "0" + string : string;
        case 4 /* CJK_DECIMAL */:
            return createCounterStyleFromSymbols(value, '〇一二三四五六七八九', cjkSuffix);
        case 6 /* LOWER_ROMAN */:
            return createAdditiveCounter(value, 1, 3999, ROMAN_UPPER, 3 /* DECIMAL */, defaultSuffix).toLowerCase();
        case 7 /* UPPER_ROMAN */:
            return createAdditiveCounter(value, 1, 3999, ROMAN_UPPER, 3 /* DECIMAL */, defaultSuffix);
        case 8 /* LOWER_GREEK */:
            return createCounterStyleFromRange(value, 945, 969, false, defaultSuffix);
        case 9 /* LOWER_ALPHA */:
            return createCounterStyleFromRange(value, 97, 122, false, defaultSuffix);
        case 10 /* UPPER_ALPHA */:
            return createCounterStyleFromRange(value, 65, 90, false, defaultSuffix);
        case 11 /* ARABIC_INDIC */:
            return createCounterStyleFromRange(value, 1632, 1641, true, defaultSuffix);
        case 12 /* ARMENIAN */:
        case 49 /* UPPER_ARMENIAN */:
            return createAdditiveCounter(value, 1, 9999, ARMENIAN, 3 /* DECIMAL */, defaultSuffix);
        case 35 /* LOWER_ARMENIAN */:
            return createAdditiveCounter(value, 1, 9999, ARMENIAN, 3 /* DECIMAL */, defaultSuffix).toLowerCase();
        case 13 /* BENGALI */:
            return createCounterStyleFromRange(value, 2534, 2543, true, defaultSuffix);
        case 14 /* CAMBODIAN */:
        case 30 /* KHMER */:
            return createCounterStyleFromRange(value, 6112, 6121, true, defaultSuffix);
        case 15 /* CJK_EARTHLY_BRANCH */:
            return createCounterStyleFromSymbols(value, '子丑寅卯辰巳午未申酉戌亥', cjkSuffix);
        case 16 /* CJK_HEAVENLY_STEM */:
            return createCounterStyleFromSymbols(value, '甲乙丙丁戊己庚辛壬癸', cjkSuffix);
        case 17 /* CJK_IDEOGRAPHIC */:
        case 48 /* TRAD_CHINESE_INFORMAL */:
            return createCJKCounter(value, '零一二三四五六七八九', CHINESE_INFORMAL_MULTIPLIERS, '負', cjkSuffix, CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS);
        case 47 /* TRAD_CHINESE_FORMAL */:
            return createCJKCounter(value, '零壹貳參肆伍陸柒捌玖', CHINESE_FORMAL_MULTIPLIERS, '負', cjkSuffix, CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS);
        case 42 /* SIMP_CHINESE_INFORMAL */:
            return createCJKCounter(value, '零一二三四五六七八九', CHINESE_INFORMAL_MULTIPLIERS, '负', cjkSuffix, CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS);
        case 41 /* SIMP_CHINESE_FORMAL */:
            return createCJKCounter(value, '零壹贰叁肆伍陆柒捌玖', CHINESE_FORMAL_MULTIPLIERS, '负', cjkSuffix, CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS);
        case 26 /* JAPANESE_INFORMAL */:
            return createCJKCounter(value, '〇一二三四五六七八九', '十百千万', JAPANESE_NEGATIVE, cjkSuffix, 0);
        case 25 /* JAPANESE_FORMAL */:
            return createCJKCounter(value, '零壱弐参四伍六七八九', '拾百千万', JAPANESE_NEGATIVE, cjkSuffix, CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS);
        case 31 /* KOREAN_HANGUL_FORMAL */:
            return createCJKCounter(value, '영일이삼사오육칠팔구', '십백천만', KOREAN_NEGATIVE, koreanSuffix, CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS);
        case 33 /* KOREAN_HANJA_INFORMAL */:
            return createCJKCounter(value, '零一二三四五六七八九', '十百千萬', KOREAN_NEGATIVE, koreanSuffix, 0);
        case 32 /* KOREAN_HANJA_FORMAL */:
            return createCJKCounter(value, '零壹貳參四五六七八九', '拾百千', KOREAN_NEGATIVE, koreanSuffix, CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS);
        case 18 /* DEVANAGARI */:
            return createCounterStyleFromRange(value, 0x966, 0x96f, true, defaultSuffix);
        case 20 /* GEORGIAN */:
            return createAdditiveCounter(value, 1, 19999, GEORGIAN, 3 /* DECIMAL */, defaultSuffix);
        case 21 /* GUJARATI */:
            return createCounterStyleFromRange(value, 0xae6, 0xaef, true, defaultSuffix);
        case 22 /* GURMUKHI */:
            return createCounterStyleFromRange(value, 0xa66, 0xa6f, true, defaultSuffix);
        case 22 /* HEBREW */:
            return createAdditiveCounter(value, 1, 10999, HEBREW, 3 /* DECIMAL */, defaultSuffix);
        case 23 /* HIRAGANA */:
            return createCounterStyleFromSymbols(value, 'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわゐゑをん');
        case 24 /* HIRAGANA_IROHA */:
            return createCounterStyleFromSymbols(value, 'いろはにほへとちりぬるをわかよたれそつねならむうゐのおくやまけふこえてあさきゆめみしゑひもせす');
        case 27 /* KANNADA */:
            return createCounterStyleFromRange(value, 0xce6, 0xcef, true, defaultSuffix);
        case 28 /* KATAKANA */:
            return createCounterStyleFromSymbols(value, 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヰヱヲン', cjkSuffix);
        case 29 /* KATAKANA_IROHA */:
            return createCounterStyleFromSymbols(value, 'イロハニホヘトチリヌルヲワカヨタレソツネナラムウヰノオクヤマケフコエテアサキユメミシヱヒモセス', cjkSuffix);
        case 34 /* LAO */:
            return createCounterStyleFromRange(value, 0xed0, 0xed9, true, defaultSuffix);
        case 37 /* MONGOLIAN */:
            return createCounterStyleFromRange(value, 0x1810, 0x1819, true, defaultSuffix);
        case 38 /* MYANMAR */:
            return createCounterStyleFromRange(value, 0x1040, 0x1049, true, defaultSuffix);
        case 39 /* ORIYA */:
            return createCounterStyleFromRange(value, 0xb66, 0xb6f, true, defaultSuffix);
        case 40 /* PERSIAN */:
            return createCounterStyleFromRange(value, 0x6f0, 0x6f9, true, defaultSuffix);
        case 43 /* TAMIL */:
            return createCounterStyleFromRange(value, 0xbe6, 0xbef, true, defaultSuffix);
        case 44 /* TELUGU */:
            return createCounterStyleFromRange(value, 0xc66, 0xc6f, true, defaultSuffix);
        case 45 /* THAI */:
            return createCounterStyleFromRange(value, 0xe50, 0xe59, true, defaultSuffix);
        case 46 /* TIBETAN */:
            return createCounterStyleFromRange(value, 0xf20, 0xf29, true, defaultSuffix);
        case 3 /* DECIMAL */:
        default:
            return createCounterStyleFromRange(value, 48, 57, true, defaultSuffix);
    }
};

var IGNORE_ATTRIBUTE = 'data-html2canvas-ignore';
var DocumentCloner = /** @class */ (function () {
    function DocumentCloner(context, element, options) {
        this.context = context;
        this.options = options;
        this.scrolledElements = [];
        this.referenceElement = element;
        this.counters = new CounterState();
        this.quoteDepth = 0;
        if (!element.ownerDocument) {
            throw new Error('Cloned element does not have an owner document');
        }
        this.documentElement = this.cloneNode(element.ownerDocument.documentElement, false);
    }
    DocumentCloner.prototype.toIFrame = function (ownerDocument, windowSize) {
        var _this = this;
        var iframe = createIFrameContainer(ownerDocument, windowSize);
        if (!iframe.contentWindow) {
            return Promise.reject("Unable to find iframe window");
        }
        var scrollX = ownerDocument.defaultView.pageXOffset;
        var scrollY = ownerDocument.defaultView.pageYOffset;
        var cloneWindow = iframe.contentWindow;
        var documentClone = cloneWindow.document;
        /* Chrome doesn't detect relative background-images assigned in inline <style> sheets when fetched through getComputedStyle
         if window url is about:blank, we can assign the url to current by writing onto the document
         */
        var iframeLoad = iframeLoader(iframe).then(function () { return __awaiter(_this, void 0, void 0, function () {
            var onclone, referenceElement;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        this.scrolledElements.forEach(restoreNodeScroll);
                        if (cloneWindow) {
                            cloneWindow.scrollTo(windowSize.left, windowSize.top);
                            if (/(iPad|iPhone|iPod)/g.test(navigator.userAgent) &&
                                (cloneWindow.scrollY !== windowSize.top || cloneWindow.scrollX !== windowSize.left)) {
                                this.context.logger.warn('Unable to restore scroll position for cloned document');
                                this.context.windowBounds = this.context.windowBounds.add(cloneWindow.scrollX - windowSize.left, cloneWindow.scrollY - windowSize.top, 0, 0);
                            }
                        }
                        onclone = this.options.onclone;
                        referenceElement = this.clonedReferenceElement;
                        if (typeof referenceElement === 'undefined') {
                            return [2 /*return*/, Promise.reject("Error finding the " + this.referenceElement.nodeName + " in the cloned document")];
                        }
                        if (!(documentClone.fonts && documentClone.fonts.ready)) return [3 /*break*/, 2];
                        return [4 /*yield*/, documentClone.fonts.ready];
                    case 1:
                        _a.sent();
                        _a.label = 2;
                    case 2:
                        if (!/(AppleWebKit)/g.test(navigator.userAgent)) return [3 /*break*/, 4];
                        return [4 /*yield*/, imagesReady(documentClone)];
                    case 3:
                        _a.sent();
                        _a.label = 4;
                    case 4:
                        if (typeof onclone === 'function') {
                            return [2 /*return*/, Promise.resolve()
                                    .then(function () { return onclone(documentClone, referenceElement); })
                                    .then(function () { return iframe; })];
                        }
                        return [2 /*return*/, iframe];
                }
            });
        }); });
        documentClone.open();
        documentClone.write(serializeDoctype(document.doctype) + "<html></html>");
        // Chrome scrolls the parent document for some reason after the write to the cloned window???
        restoreOwnerScroll(this.referenceElement.ownerDocument, scrollX, scrollY);
        documentClone.replaceChild(documentClone.adoptNode(this.documentElement), documentClone.documentElement);
        documentClone.close();
        return iframeLoad;
    };
    DocumentCloner.prototype.createElementClone = function (node) {
        if (isDebugging(node, 2 /* CLONE */)) {
            debugger;
        }
        if (isCanvasElement(node)) {
            return this.createCanvasClone(node);
        }
        if (isVideoElement(node)) {
            return this.createVideoClone(node);
        }
        if (isStyleElement(node)) {
            return this.createStyleClone(node);
        }
        var clone = node.cloneNode(false);
        if (isImageElement(clone)) {
            if (isImageElement(node) && node.currentSrc && node.currentSrc !== node.src) {
                clone.src = node.currentSrc;
                clone.srcset = '';
            }
            if (clone.loading === 'lazy') {
                clone.loading = 'eager';
            }
        }
        if (isCustomElement(clone)) {
            return this.createCustomElementClone(clone);
        }
        return clone;
    };
    DocumentCloner.prototype.createCustomElementClone = function (node) {
        var clone = document.createElement('html2canvascustomelement');
        copyCSSStyles(node.style, clone);
        return clone;
    };
    DocumentCloner.prototype.createStyleClone = function (node) {
        try {
            var sheet = node.sheet;
            if (sheet && sheet.cssRules) {
                var css = [].slice.call(sheet.cssRules, 0).reduce(function (css, rule) {
                    if (rule && typeof rule.cssText === 'string') {
                        return css + rule.cssText;
                    }
                    return css;
                }, '');
                var style = node.cloneNode(false);
                style.textContent = css;
                return style;
            }
        }
        catch (e) {
            // accessing node.sheet.cssRules throws a DOMException
            this.context.logger.error('Unable to access cssRules property', e);
            if (e.name !== 'SecurityError') {
                throw e;
            }
        }
        return node.cloneNode(false);
    };
    DocumentCloner.prototype.createCanvasClone = function (canvas) {
        var _a;
        if (this.options.inlineImages && canvas.ownerDocument) {
            var img = canvas.ownerDocument.createElement('img');
            try {
                img.src = canvas.toDataURL();
                return img;
            }
            catch (e) {
                this.context.logger.info("Unable to inline canvas contents, canvas is tainted", canvas);
            }
        }
        var clonedCanvas = canvas.cloneNode(false);
        try {
            clonedCanvas.width = canvas.width;
            clonedCanvas.height = canvas.height;
            var ctx = canvas.getContext('2d');
            var clonedCtx = clonedCanvas.getContext('2d');
            if (clonedCtx) {
                if (!this.options.allowTaint && ctx) {
                    clonedCtx.putImageData(ctx.getImageData(0, 0, canvas.width, canvas.height), 0, 0);
                }
                else {
                    var gl = (_a = canvas.getContext('webgl2')) !== null && _a !== void 0 ? _a : canvas.getContext('webgl');
                    if (gl) {
                        var attribs = gl.getContextAttributes();
                        if ((attribs === null || attribs === void 0 ? void 0 : attribs.preserveDrawingBuffer) === false) {
                            this.context.logger.warn('Unable to clone WebGL context as it has preserveDrawingBuffer=false', canvas);
                        }
                    }
                    clonedCtx.drawImage(canvas, 0, 0);
                }
            }
            return clonedCanvas;
        }
        catch (e) {
            this.context.logger.info("Unable to clone canvas as it is tainted", canvas);
        }
        return clonedCanvas;
    };
    DocumentCloner.prototype.createVideoClone = function (video) {
        var canvas = video.ownerDocument.createElement('canvas');
        canvas.width = video.offsetWidth;
        canvas.height = video.offsetHeight;
        var ctx = canvas.getContext('2d');
        try {
            if (ctx) {
                ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
                if (!this.options.allowTaint) {
                    ctx.getImageData(0, 0, canvas.width, canvas.height);
                }
            }
            return canvas;
        }
        catch (e) {
            this.context.logger.info("Unable to clone video as it is tainted", video);
        }
        var blankCanvas = video.ownerDocument.createElement('canvas');
        blankCanvas.width = video.offsetWidth;
        blankCanvas.height = video.offsetHeight;
        return blankCanvas;
    };
    DocumentCloner.prototype.appendChildNode = function (clone, child, copyStyles) {
        if (!isElementNode(child) ||
            (!isScriptElement(child) &&
                !child.hasAttribute(IGNORE_ATTRIBUTE) &&
                (typeof this.options.ignoreElements !== 'function' || !this.options.ignoreElements(child)))) {
            if (!this.options.copyStyles || !isElementNode(child) || !isStyleElement(child)) {
                clone.appendChild(this.cloneNode(child, copyStyles));
            }
        }
    };
    DocumentCloner.prototype.cloneChildNodes = function (node, clone, copyStyles) {
        var _this = this;
        for (var child = node.shadowRoot ? node.shadowRoot.firstChild : node.firstChild; child; child = child.nextSibling) {
            if (isElementNode(child) && isSlotElement(child) && typeof child.assignedNodes === 'function') {
                var assignedNodes = child.assignedNodes();
                if (assignedNodes.length) {
                    assignedNodes.forEach(function (assignedNode) { return _this.appendChildNode(clone, assignedNode, copyStyles); });
                }
            }
            else {
                this.appendChildNode(clone, child, copyStyles);
            }
        }
    };
    DocumentCloner.prototype.cloneNode = function (node, copyStyles) {
        if (isTextNode(node)) {
            return document.createTextNode(node.data);
        }
        if (!node.ownerDocument) {
            return node.cloneNode(false);
        }
        var window = node.ownerDocument.defaultView;
        if (window && isElementNode(node) && (isHTMLElementNode(node) || isSVGElementNode(node))) {
            var clone = this.createElementClone(node);
            clone.style.transitionProperty = 'none';
            var style = window.getComputedStyle(node);
            var styleBefore = window.getComputedStyle(node, ':before');
            var styleAfter = window.getComputedStyle(node, ':after');
            if (this.referenceElement === node && isHTMLElementNode(clone)) {
                this.clonedReferenceElement = clone;
            }
            if (isBodyElement(clone)) {
                createPseudoHideStyles(clone);
            }
            var counters = this.counters.parse(new CSSParsedCounterDeclaration(this.context, style));
            var before = this.resolvePseudoContent(node, clone, styleBefore, PseudoElementType.BEFORE);
            if (isCustomElement(node)) {
                copyStyles = true;
            }
            if (!isVideoElement(node)) {
                this.cloneChildNodes(node, clone, copyStyles);
            }
            if (before) {
                clone.insertBefore(before, clone.firstChild);
            }
            var after = this.resolvePseudoContent(node, clone, styleAfter, PseudoElementType.AFTER);
            if (after) {
                clone.appendChild(after);
            }
            this.counters.pop(counters);
            if ((style && (this.options.copyStyles || isSVGElementNode(node)) && !isIFrameElement(node)) ||
                copyStyles) {
                copyCSSStyles(style, clone);
            }
            if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
                this.scrolledElements.push([clone, node.scrollLeft, node.scrollTop]);
            }
            if ((isTextareaElement(node) || isSelectElement(node)) &&
                (isTextareaElement(clone) || isSelectElement(clone))) {
                clone.value = node.value;
            }
            return clone;
        }
        return node.cloneNode(false);
    };
    DocumentCloner.prototype.resolvePseudoContent = function (node, clone, style, pseudoElt) {
        var _this = this;
        if (!style) {
            return;
        }
        var value = style.content;
        var document = clone.ownerDocument;
        if (!document || !value || value === 'none' || value === '-moz-alt-content' || style.display === 'none') {
            return;
        }
        this.counters.parse(new CSSParsedCounterDeclaration(this.context, style));
        var declaration = new CSSParsedPseudoDeclaration(this.context, style);
        var anonymousReplacedElement = document.createElement('html2canvaspseudoelement');
        copyCSSStyles(style, anonymousReplacedElement);
        declaration.content.forEach(function (token) {
            if (token.type === 0 /* STRING_TOKEN */) {
                anonymousReplacedElement.appendChild(document.createTextNode(token.value));
            }
            else if (token.type === 22 /* URL_TOKEN */) {
                var img = document.createElement('img');
                img.src = token.value;
                img.style.opacity = '1';
                anonymousReplacedElement.appendChild(img);
            }
            else if (token.type === 18 /* FUNCTION */) {
                if (token.name === 'attr') {
                    var attr = token.values.filter(isIdentToken);
                    if (attr.length) {
                        anonymousReplacedElement.appendChild(document.createTextNode(node.getAttribute(attr[0].value) || ''));
                    }
                }
                else if (token.name === 'counter') {
                    var _a = token.values.filter(nonFunctionArgSeparator), counter = _a[0], counterStyle = _a[1];
                    if (counter && isIdentToken(counter)) {
                        var counterState = _this.counters.getCounterValue(counter.value);
                        var counterType = counterStyle && isIdentToken(counterStyle)
                            ? listStyleType.parse(_this.context, counterStyle.value)
                            : 3 /* DECIMAL */;
                        anonymousReplacedElement.appendChild(document.createTextNode(createCounterText(counterState, counterType, false)));
                    }
                }
                else if (token.name === 'counters') {
                    var _b = token.values.filter(nonFunctionArgSeparator), counter = _b[0], delim = _b[1], counterStyle = _b[2];
                    if (counter && isIdentToken(counter)) {
                        var counterStates = _this.counters.getCounterValues(counter.value);
                        var counterType_1 = counterStyle && isIdentToken(counterStyle)
                            ? listStyleType.parse(_this.context, counterStyle.value)
                            : 3 /* DECIMAL */;
                        var separator = delim && delim.type === 0 /* STRING_TOKEN */ ? delim.value : '';
                        var text = counterStates
                            .map(function (value) { return createCounterText(value, counterType_1, false); })
                            .join(separator);
                        anonymousReplacedElement.appendChild(document.createTextNode(text));
                    }
                }
                else ;
            }
            else if (token.type === 20 /* IDENT_TOKEN */) {
                switch (token.value) {
                    case 'open-quote':
                        anonymousReplacedElement.appendChild(document.createTextNode(getQuote(declaration.quotes, _this.quoteDepth++, true)));
                        break;
                    case 'close-quote':
                        anonymousReplacedElement.appendChild(document.createTextNode(getQuote(declaration.quotes, --_this.quoteDepth, false)));
                        break;
                    default:
                        // safari doesn't parse string tokens correctly because of lack of quotes
                        anonymousReplacedElement.appendChild(document.createTextNode(token.value));
                }
            }
        });
        anonymousReplacedElement.className = PSEUDO_HIDE_ELEMENT_CLASS_BEFORE + " " + PSEUDO_HIDE_ELEMENT_CLASS_AFTER;
        var newClassName = pseudoElt === PseudoElementType.BEFORE
            ? " " + PSEUDO_HIDE_ELEMENT_CLASS_BEFORE
            : " " + PSEUDO_HIDE_ELEMENT_CLASS_AFTER;
        if (isSVGElementNode(clone)) {
            clone.className.baseValue += newClassName;
        }
        else {
            clone.className += newClassName;
        }
        return anonymousReplacedElement;
    };
    DocumentCloner.destroy = function (container) {
        if (container.parentNode) {
            container.parentNode.removeChild(container);
            return true;
        }
        return false;
    };
    return DocumentCloner;
}());
var PseudoElementType;
(function (PseudoElementType) {
    PseudoElementType[PseudoElementType["BEFORE"] = 0] = "BEFORE";
    PseudoElementType[PseudoElementType["AFTER"] = 1] = "AFTER";
})(PseudoElementType || (PseudoElementType = {}));
var createIFrameContainer = function (ownerDocument, bounds) {
    var cloneIframeContainer = ownerDocument.createElement('iframe');
    cloneIframeContainer.className = 'html2canvas-container';
    cloneIframeContainer.style.visibility = 'hidden';
    cloneIframeContainer.style.position = 'fixed';
    cloneIframeContainer.style.left = '-10000px';
    cloneIframeContainer.style.top = '0px';
    cloneIframeContainer.style.border = '0';
    cloneIframeContainer.width = bounds.width.toString();
    cloneIframeContainer.height = bounds.height.toString();
    cloneIframeContainer.scrolling = 'no'; // ios won't scroll without it
    cloneIframeContainer.setAttribute(IGNORE_ATTRIBUTE, 'true');
    ownerDocument.body.appendChild(cloneIframeContainer);
    return cloneIframeContainer;
};
var imageReady = function (img) {
    return new Promise(function (resolve) {
        if (img.complete) {
            resolve();
            return;
        }
        if (!img.src) {
            resolve();
            return;
        }
        img.onload = resolve;
        img.onerror = resolve;
    });
};
var imagesReady = function (document) {
    return Promise.all([].slice.call(document.images, 0).map(imageReady));
};
var iframeLoader = function (iframe) {
    return new Promise(function (resolve, reject) {
        var cloneWindow = iframe.contentWindow;
        if (!cloneWindow) {
            return reject("No window assigned for iframe");
        }
        var documentClone = cloneWindow.document;
        cloneWindow.onload = iframe.onload = function () {
            cloneWindow.onload = iframe.onload = null;
            var interval = setInterval(function () {
                if (documentClone.body.childNodes.length > 0 && documentClone.readyState === 'complete') {
                    clearInterval(interval);
                    resolve(iframe);
                }
            }, 50);
        };
    });
};
var ignoredStyleProperties = [
    'all',
    'd',
    'content' // Safari shows pseudoelements if content is set
];
var copyCSSStyles = function (style, target) {
    // Edge does not provide value for cssText
    for (var i = style.length - 1; i >= 0; i--) {
        var property = style.item(i);
        if (ignoredStyleProperties.indexOf(property) === -1) {
            target.style.setProperty(property, style.getPropertyValue(property));
        }
    }
    return target;
};
var serializeDoctype = function (doctype) {
    var str = '';
    if (doctype) {
        str += '<!DOCTYPE ';
        if (doctype.name) {
            str += doctype.name;
        }
        if (doctype.internalSubset) {
            str += doctype.internalSubset;
        }
        if (doctype.publicId) {
            str += "\"" + doctype.publicId + "\"";
        }
        if (doctype.systemId) {
            str += "\"" + doctype.systemId + "\"";
        }
        str += '>';
    }
    return str;
};
var restoreOwnerScroll = function (ownerDocument, x, y) {
    if (ownerDocument &&
        ownerDocument.defaultView &&
        (x !== ownerDocument.defaultView.pageXOffset || y !== ownerDocument.defaultView.pageYOffset)) {
        ownerDocument.defaultView.scrollTo(x, y);
    }
};
var restoreNodeScroll = function (_a) {
    var element = _a[0], x = _a[1], y = _a[2];
    element.scrollLeft = x;
    element.scrollTop = y;
};
var PSEUDO_BEFORE = ':before';
var PSEUDO_AFTER = ':after';
var PSEUDO_HIDE_ELEMENT_CLASS_BEFORE = '___html2canvas___pseudoelement_before';
var PSEUDO_HIDE_ELEMENT_CLASS_AFTER = '___html2canvas___pseudoelement_after';
var PSEUDO_HIDE_ELEMENT_STYLE = "{\n    content: \"\" !important;\n    display: none !important;\n}";
var createPseudoHideStyles = function (body) {
    createStyles(body, "." + PSEUDO_HIDE_ELEMENT_CLASS_BEFORE + PSEUDO_BEFORE + PSEUDO_HIDE_ELEMENT_STYLE + "\n         ." + PSEUDO_HIDE_ELEMENT_CLASS_AFTER + PSEUDO_AFTER + PSEUDO_HIDE_ELEMENT_STYLE);
};
var createStyles = function (body, styles) {
    var document = body.ownerDocument;
    if (document) {
        var style = document.createElement('style');
        style.textContent = styles;
        body.appendChild(style);
    }
};

var CacheStorage = /** @class */ (function () {
    function CacheStorage() {
    }
    CacheStorage.getOrigin = function (url) {
        var link = CacheStorage._link;
        if (!link) {
            return 'about:blank';
        }
        link.href = url;
        link.href = link.href; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/
        return link.protocol + link.hostname + link.port;
    };
    CacheStorage.isSameOrigin = function (src) {
        return CacheStorage.getOrigin(src) === CacheStorage._origin;
    };
    CacheStorage.setContext = function (window) {
        CacheStorage._link = window.document.createElement('a');
        CacheStorage._origin = CacheStorage.getOrigin(window.location.href);
    };
    CacheStorage._origin = 'about:blank';
    return CacheStorage;
}());
var Cache = /** @class */ (function () {
    function Cache(context, _options) {
        this.context = context;
        this._options = _options;
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        this._cache = {};
    }
    Cache.prototype.addImage = function (src) {
        var result = Promise.resolve();
        if (this.has(src)) {
            return result;
        }
        if (isBlobImage(src) || isRenderable(src)) {
            (this._cache[src] = this.loadImage(src)).catch(function () {
                // prevent unhandled rejection
            });
            return result;
        }
        return result;
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    Cache.prototype.match = function (src) {
        return this._cache[src];
    };
    Cache.prototype.loadImage = function (key) {
        return __awaiter(this, void 0, void 0, function () {
            var isSameOrigin, useCORS, useProxy, src;
            var _this = this;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        isSameOrigin = CacheStorage.isSameOrigin(key);
                        useCORS = !isInlineImage(key) && this._options.useCORS === true && FEATURES.SUPPORT_CORS_IMAGES && !isSameOrigin;
                        useProxy = !isInlineImage(key) &&
                            !isSameOrigin &&
                            !isBlobImage(key) &&
                            typeof this._options.proxy === 'string' &&
                            FEATURES.SUPPORT_CORS_XHR &&
                            !useCORS;
                        if (!isSameOrigin &&
                            this._options.allowTaint === false &&
                            !isInlineImage(key) &&
                            !isBlobImage(key) &&
                            !useProxy &&
                            !useCORS) {
                            return [2 /*return*/];
                        }
                        src = key;
                        if (!useProxy) return [3 /*break*/, 2];
                        return [4 /*yield*/, this.proxy(src)];
                    case 1:
                        src = _a.sent();
                        _a.label = 2;
                    case 2:
                        this.context.logger.debug("Added image " + key.substring(0, 256));
                        return [4 /*yield*/, new Promise(function (resolve, reject) {
                                var img = new Image();
                                img.onload = function () { return resolve(img); };
                                img.onerror = reject;
                                //ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous
                                if (isInlineBase64Image(src) || useCORS) {
                                    img.crossOrigin = 'anonymous';
                                }
                                img.src = src;
                                if (img.complete === true) {
                                    // Inline XML images may fail to parse, throwing an Error later on
                                    setTimeout(function () { return resolve(img); }, 500);
                                }
                                if (_this._options.imageTimeout > 0) {
                                    setTimeout(function () { return reject("Timed out (" + _this._options.imageTimeout + "ms) loading image"); }, _this._options.imageTimeout);
                                }
                            })];
                    case 3: return [2 /*return*/, _a.sent()];
                }
            });
        });
    };
    Cache.prototype.has = function (key) {
        return typeof this._cache[key] !== 'undefined';
    };
    Cache.prototype.keys = function () {
        return Promise.resolve(Object.keys(this._cache));
    };
    Cache.prototype.proxy = function (src) {
        var _this = this;
        var proxy = this._options.proxy;
        if (!proxy) {
            throw new Error('No proxy defined');
        }
        var key = src.substring(0, 256);
        return new Promise(function (resolve, reject) {
            var responseType = FEATURES.SUPPORT_RESPONSE_TYPE ? 'blob' : 'text';
            var xhr = new XMLHttpRequest();
            xhr.onload = function () {
                if (xhr.status === 200) {
                    if (responseType === 'text') {
                        resolve(xhr.response);
                    }
                    else {
                        var reader_1 = new FileReader();
                        reader_1.addEventListener('load', function () { return resolve(reader_1.result); }, false);
                        reader_1.addEventListener('error', function (e) { return reject(e); }, false);
                        reader_1.readAsDataURL(xhr.response);
                    }
                }
                else {
                    reject("Failed to proxy resource " + key + " with status code " + xhr.status);
                }
            };
            xhr.onerror = reject;
            var queryString = proxy.indexOf('?') > -1 ? '&' : '?';
            xhr.open('GET', "" + proxy + queryString + "url=" + encodeURIComponent(src) + "&responseType=" + responseType);
            if (responseType !== 'text' && xhr instanceof XMLHttpRequest) {
                xhr.responseType = responseType;
            }
            if (_this._options.imageTimeout) {
                var timeout_1 = _this._options.imageTimeout;
                xhr.timeout = timeout_1;
                xhr.ontimeout = function () { return reject("Timed out (" + timeout_1 + "ms) proxying " + key); };
            }
            xhr.send();
        });
    };
    return Cache;
}());
var INLINE_SVG = /^data:image\/svg\+xml/i;
var INLINE_BASE64 = /^data:image\/.*;base64,/i;
var INLINE_IMG = /^data:image\/.*/i;
var isRenderable = function (src) { return FEATURES.SUPPORT_SVG_DRAWING || !isSVG$1(src); };
var isInlineImage = function (src) { return INLINE_IMG.test(src); };
var isInlineBase64Image = function (src) { return INLINE_BASE64.test(src); };
var isBlobImage = function (src) { return src.substr(0, 4) === 'blob'; };
var isSVG$1 = function (src) { return src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src); };

var Vector$1 = /** @class */ (function () {
    function Vector(x, y) {
        this.type = 0 /* VECTOR */;
        this.x = x;
        this.y = y;
    }
    Vector.prototype.add = function (deltaX, deltaY) {
        return new Vector(this.x + deltaX, this.y + deltaY);
    };
    return Vector;
}());

var lerp = function (a, b, t) {
    return new Vector$1(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
};
var BezierCurve = /** @class */ (function () {
    function BezierCurve(start, startControl, endControl, end) {
        this.type = 1 /* BEZIER_CURVE */;
        this.start = start;
        this.startControl = startControl;
        this.endControl = endControl;
        this.end = end;
    }
    BezierCurve.prototype.subdivide = function (t, firstHalf) {
        var ab = lerp(this.start, this.startControl, t);
        var bc = lerp(this.startControl, this.endControl, t);
        var cd = lerp(this.endControl, this.end, t);
        var abbc = lerp(ab, bc, t);
        var bccd = lerp(bc, cd, t);
        var dest = lerp(abbc, bccd, t);
        return firstHalf ? new BezierCurve(this.start, ab, abbc, dest) : new BezierCurve(dest, bccd, cd, this.end);
    };
    BezierCurve.prototype.add = function (deltaX, deltaY) {
        return new BezierCurve(this.start.add(deltaX, deltaY), this.startControl.add(deltaX, deltaY), this.endControl.add(deltaX, deltaY), this.end.add(deltaX, deltaY));
    };
    BezierCurve.prototype.reverse = function () {
        return new BezierCurve(this.end, this.endControl, this.startControl, this.start);
    };
    return BezierCurve;
}());
var isBezierCurve = function (path) { return path.type === 1 /* BEZIER_CURVE */; };

var BoundCurves = /** @class */ (function () {
    function BoundCurves(element) {
        var styles = element.styles;
        var bounds = element.bounds;
        var _a = getAbsoluteValueForTuple(styles.borderTopLeftRadius, bounds.width, bounds.height), tlh = _a[0], tlv = _a[1];
        var _b = getAbsoluteValueForTuple(styles.borderTopRightRadius, bounds.width, bounds.height), trh = _b[0], trv = _b[1];
        var _c = getAbsoluteValueForTuple(styles.borderBottomRightRadius, bounds.width, bounds.height), brh = _c[0], brv = _c[1];
        var _d = getAbsoluteValueForTuple(styles.borderBottomLeftRadius, bounds.width, bounds.height), blh = _d[0], blv = _d[1];
        var factors = [];
        factors.push((tlh + trh) / bounds.width);
        factors.push((blh + brh) / bounds.width);
        factors.push((tlv + blv) / bounds.height);
        factors.push((trv + brv) / bounds.height);
        var maxFactor = Math.max.apply(Math, factors);
        if (maxFactor > 1) {
            tlh /= maxFactor;
            tlv /= maxFactor;
            trh /= maxFactor;
            trv /= maxFactor;
            brh /= maxFactor;
            brv /= maxFactor;
            blh /= maxFactor;
            blv /= maxFactor;
        }
        var topWidth = bounds.width - trh;
        var rightHeight = bounds.height - brv;
        var bottomWidth = bounds.width - brh;
        var leftHeight = bounds.height - blv;
        var borderTopWidth = styles.borderTopWidth;
        var borderRightWidth = styles.borderRightWidth;
        var borderBottomWidth = styles.borderBottomWidth;
        var borderLeftWidth = styles.borderLeftWidth;
        var paddingTop = getAbsoluteValue(styles.paddingTop, element.bounds.width);
        var paddingRight = getAbsoluteValue(styles.paddingRight, element.bounds.width);
        var paddingBottom = getAbsoluteValue(styles.paddingBottom, element.bounds.width);
        var paddingLeft = getAbsoluteValue(styles.paddingLeft, element.bounds.width);
        this.topLeftBorderDoubleOuterBox =
            tlh > 0 || tlv > 0
                ? getCurvePoints(bounds.left + borderLeftWidth / 3, bounds.top + borderTopWidth / 3, tlh - borderLeftWidth / 3, tlv - borderTopWidth / 3, CORNER.TOP_LEFT)
                : new Vector$1(bounds.left + borderLeftWidth / 3, bounds.top + borderTopWidth / 3);
        this.topRightBorderDoubleOuterBox =
            tlh > 0 || tlv > 0
                ? getCurvePoints(bounds.left + topWidth, bounds.top + borderTopWidth / 3, trh - borderRightWidth / 3, trv - borderTopWidth / 3, CORNER.TOP_RIGHT)
                : new Vector$1(bounds.left + bounds.width - borderRightWidth / 3, bounds.top + borderTopWidth / 3);
        this.bottomRightBorderDoubleOuterBox =
            brh > 0 || brv > 0
                ? getCurvePoints(bounds.left + bottomWidth, bounds.top + rightHeight, brh - borderRightWidth / 3, brv - borderBottomWidth / 3, CORNER.BOTTOM_RIGHT)
                : new Vector$1(bounds.left + bounds.width - borderRightWidth / 3, bounds.top + bounds.height - borderBottomWidth / 3);
        this.bottomLeftBorderDoubleOuterBox =
            blh > 0 || blv > 0
                ? getCurvePoints(bounds.left + borderLeftWidth / 3, bounds.top + leftHeight, blh - borderLeftWidth / 3, blv - borderBottomWidth / 3, CORNER.BOTTOM_LEFT)
                : new Vector$1(bounds.left + borderLeftWidth / 3, bounds.top + bounds.height - borderBottomWidth / 3);
        this.topLeftBorderDoubleInnerBox =
            tlh > 0 || tlv > 0
                ? getCurvePoints(bounds.left + (borderLeftWidth * 2) / 3, bounds.top + (borderTopWidth * 2) / 3, tlh - (borderLeftWidth * 2) / 3, tlv - (borderTopWidth * 2) / 3, CORNER.TOP_LEFT)
                : new Vector$1(bounds.left + (borderLeftWidth * 2) / 3, bounds.top + (borderTopWidth * 2) / 3);
        this.topRightBorderDoubleInnerBox =
            tlh > 0 || tlv > 0
                ? getCurvePoints(bounds.left + topWidth, bounds.top + (borderTopWidth * 2) / 3, trh - (borderRightWidth * 2) / 3, trv - (borderTopWidth * 2) / 3, CORNER.TOP_RIGHT)
                : new Vector$1(bounds.left + bounds.width - (borderRightWidth * 2) / 3, bounds.top + (borderTopWidth * 2) / 3);
        this.bottomRightBorderDoubleInnerBox =
            brh > 0 || brv > 0
                ? getCurvePoints(bounds.left + bottomWidth, bounds.top + rightHeight, brh - (borderRightWidth * 2) / 3, brv - (borderBottomWidth * 2) / 3, CORNER.BOTTOM_RIGHT)
                : new Vector$1(bounds.left + bounds.width - (borderRightWidth * 2) / 3, bounds.top + bounds.height - (borderBottomWidth * 2) / 3);
        this.bottomLeftBorderDoubleInnerBox =
            blh > 0 || blv > 0
                ? getCurvePoints(bounds.left + (borderLeftWidth * 2) / 3, bounds.top + leftHeight, blh - (borderLeftWidth * 2) / 3, blv - (borderBottomWidth * 2) / 3, CORNER.BOTTOM_LEFT)
                : new Vector$1(bounds.left + (borderLeftWidth * 2) / 3, bounds.top + bounds.height - (borderBottomWidth * 2) / 3);
        this.topLeftBorderStroke =
            tlh > 0 || tlv > 0
                ? getCurvePoints(bounds.left + borderLeftWidth / 2, bounds.top + borderTopWidth / 2, tlh - borderLeftWidth / 2, tlv - borderTopWidth / 2, CORNER.TOP_LEFT)
                : new Vector$1(bounds.left + borderLeftWidth / 2, bounds.top + borderTopWidth / 2);
        this.topRightBorderStroke =
            tlh > 0 || tlv > 0
                ? getCurvePoints(bounds.left + topWidth, bounds.top + borderTopWidth / 2, trh - borderRightWidth / 2, trv - borderTopWidth / 2, CORNER.TOP_RIGHT)
                : new Vector$1(bounds.left + bounds.width - borderRightWidth / 2, bounds.top + borderTopWidth / 2);
        this.bottomRightBorderStroke =
            brh > 0 || brv > 0
                ? getCurvePoints(bounds.left + bottomWidth, bounds.top + rightHeight, brh - borderRightWidth / 2, brv - borderBottomWidth / 2, CORNER.BOTTOM_RIGHT)
                : new Vector$1(bounds.left + bounds.width - borderRightWidth / 2, bounds.top + bounds.height - borderBottomWidth / 2);
        this.bottomLeftBorderStroke =
            blh > 0 || blv > 0
                ? getCurvePoints(bounds.left + borderLeftWidth / 2, bounds.top + leftHeight, blh - borderLeftWidth / 2, blv - borderBottomWidth / 2, CORNER.BOTTOM_LEFT)
                : new Vector$1(bounds.left + borderLeftWidth / 2, bounds.top + bounds.height - borderBottomWidth / 2);
        this.topLeftBorderBox =
            tlh > 0 || tlv > 0
                ? getCurvePoints(bounds.left, bounds.top, tlh, tlv, CORNER.TOP_LEFT)
                : new Vector$1(bounds.left, bounds.top);
        this.topRightBorderBox =
            trh > 0 || trv > 0
                ? getCurvePoints(bounds.left + topWidth, bounds.top, trh, trv, CORNER.TOP_RIGHT)
                : new Vector$1(bounds.left + bounds.width, bounds.top);
        this.bottomRightBorderBox =
            brh > 0 || brv > 0
                ? getCurvePoints(bounds.left + bottomWidth, bounds.top + rightHeight, brh, brv, CORNER.BOTTOM_RIGHT)
                : new Vector$1(bounds.left + bounds.width, bounds.top + bounds.height);
        this.bottomLeftBorderBox =
            blh > 0 || blv > 0
                ? getCurvePoints(bounds.left, bounds.top + leftHeight, blh, blv, CORNER.BOTTOM_LEFT)
                : new Vector$1(bounds.left, bounds.top + bounds.height);
        this.topLeftPaddingBox =
            tlh > 0 || tlv > 0
                ? getCurvePoints(bounds.left + borderLeftWidth, bounds.top + borderTopWidth, Math.max(0, tlh - borderLeftWidth), Math.max(0, tlv - borderTopWidth), CORNER.TOP_LEFT)
                : new Vector$1(bounds.left + borderLeftWidth, bounds.top + borderTopWidth);
        this.topRightPaddingBox =
            trh > 0 || trv > 0
                ? getCurvePoints(bounds.left + Math.min(topWidth, bounds.width - borderRightWidth), bounds.top + borderTopWidth, topWidth > bounds.width + borderRightWidth ? 0 : Math.max(0, trh - borderRightWidth), Math.max(0, trv - borderTopWidth), CORNER.TOP_RIGHT)
                : new Vector$1(bounds.left + bounds.width - borderRightWidth, bounds.top + borderTopWidth);
        this.bottomRightPaddingBox =
            brh > 0 || brv > 0
                ? getCurvePoints(bounds.left + Math.min(bottomWidth, bounds.width - borderLeftWidth), bounds.top + Math.min(rightHeight, bounds.height - borderBottomWidth), Math.max(0, brh - borderRightWidth), Math.max(0, brv - borderBottomWidth), CORNER.BOTTOM_RIGHT)
                : new Vector$1(bounds.left + bounds.width - borderRightWidth, bounds.top + bounds.height - borderBottomWidth);
        this.bottomLeftPaddingBox =
            blh > 0 || blv > 0
                ? getCurvePoints(bounds.left + borderLeftWidth, bounds.top + Math.min(leftHeight, bounds.height - borderBottomWidth), Math.max(0, blh - borderLeftWidth), Math.max(0, blv - borderBottomWidth), CORNER.BOTTOM_LEFT)
                : new Vector$1(bounds.left + borderLeftWidth, bounds.top + bounds.height - borderBottomWidth);
        this.topLeftContentBox =
            tlh > 0 || tlv > 0
                ? getCurvePoints(bounds.left + borderLeftWidth + paddingLeft, bounds.top + borderTopWidth + paddingTop, Math.max(0, tlh - (borderLeftWidth + paddingLeft)), Math.max(0, tlv - (borderTopWidth + paddingTop)), CORNER.TOP_LEFT)
                : new Vector$1(bounds.left + borderLeftWidth + paddingLeft, bounds.top + borderTopWidth + paddingTop);
        this.topRightContentBox =
            trh > 0 || trv > 0
                ? getCurvePoints(bounds.left + Math.min(topWidth, bounds.width + borderLeftWidth + paddingLeft), bounds.top + borderTopWidth + paddingTop, topWidth > bounds.width + borderLeftWidth + paddingLeft ? 0 : trh - borderLeftWidth + paddingLeft, trv - (borderTopWidth + paddingTop), CORNER.TOP_RIGHT)
                : new Vector$1(bounds.left + bounds.width - (borderRightWidth + paddingRight), bounds.top + borderTopWidth + paddingTop);
        this.bottomRightContentBox =
            brh > 0 || brv > 0
                ? getCurvePoints(bounds.left + Math.min(bottomWidth, bounds.width - (borderLeftWidth + paddingLeft)), bounds.top + Math.min(rightHeight, bounds.height + borderTopWidth + paddingTop), Math.max(0, brh - (borderRightWidth + paddingRight)), brv - (borderBottomWidth + paddingBottom), CORNER.BOTTOM_RIGHT)
                : new Vector$1(bounds.left + bounds.width - (borderRightWidth + paddingRight), bounds.top + bounds.height - (borderBottomWidth + paddingBottom));
        this.bottomLeftContentBox =
            blh > 0 || blv > 0
                ? getCurvePoints(bounds.left + borderLeftWidth + paddingLeft, bounds.top + leftHeight, Math.max(0, blh - (borderLeftWidth + paddingLeft)), blv - (borderBottomWidth + paddingBottom), CORNER.BOTTOM_LEFT)
                : new Vector$1(bounds.left + borderLeftWidth + paddingLeft, bounds.top + bounds.height - (borderBottomWidth + paddingBottom));
    }
    return BoundCurves;
}());
var CORNER;
(function (CORNER) {
    CORNER[CORNER["TOP_LEFT"] = 0] = "TOP_LEFT";
    CORNER[CORNER["TOP_RIGHT"] = 1] = "TOP_RIGHT";
    CORNER[CORNER["BOTTOM_RIGHT"] = 2] = "BOTTOM_RIGHT";
    CORNER[CORNER["BOTTOM_LEFT"] = 3] = "BOTTOM_LEFT";
})(CORNER || (CORNER = {}));
var getCurvePoints = function (x, y, r1, r2, position) {
    var kappa = 4 * ((Math.sqrt(2) - 1) / 3);
    var ox = r1 * kappa; // control point offset horizontal
    var oy = r2 * kappa; // control point offset vertical
    var xm = x + r1; // x-middle
    var ym = y + r2; // y-middle
    switch (position) {
        case CORNER.TOP_LEFT:
            return new BezierCurve(new Vector$1(x, ym), new Vector$1(x, ym - oy), new Vector$1(xm - ox, y), new Vector$1(xm, y));
        case CORNER.TOP_RIGHT:
            return new BezierCurve(new Vector$1(x, y), new Vector$1(x + ox, y), new Vector$1(xm, ym - oy), new Vector$1(xm, ym));
        case CORNER.BOTTOM_RIGHT:
            return new BezierCurve(new Vector$1(xm, y), new Vector$1(xm, y + oy), new Vector$1(x + ox, ym), new Vector$1(x, ym));
        case CORNER.BOTTOM_LEFT:
        default:
            return new BezierCurve(new Vector$1(xm, ym), new Vector$1(xm - ox, ym), new Vector$1(x, y + oy), new Vector$1(x, y));
    }
};
var calculateBorderBoxPath = function (curves) {
    return [curves.topLeftBorderBox, curves.topRightBorderBox, curves.bottomRightBorderBox, curves.bottomLeftBorderBox];
};
var calculateContentBoxPath = function (curves) {
    return [
        curves.topLeftContentBox,
        curves.topRightContentBox,
        curves.bottomRightContentBox,
        curves.bottomLeftContentBox
    ];
};
var calculatePaddingBoxPath = function (curves) {
    return [
        curves.topLeftPaddingBox,
        curves.topRightPaddingBox,
        curves.bottomRightPaddingBox,
        curves.bottomLeftPaddingBox
    ];
};

var TransformEffect = /** @class */ (function () {
    function TransformEffect(offsetX, offsetY, matrix) {
        this.offsetX = offsetX;
        this.offsetY = offsetY;
        this.matrix = matrix;
        this.type = 0 /* TRANSFORM */;
        this.target = 2 /* BACKGROUND_BORDERS */ | 4 /* CONTENT */;
    }
    return TransformEffect;
}());
var ClipEffect = /** @class */ (function () {
    function ClipEffect(path, target) {
        this.path = path;
        this.target = target;
        this.type = 1 /* CLIP */;
    }
    return ClipEffect;
}());
var OpacityEffect = /** @class */ (function () {
    function OpacityEffect(opacity) {
        this.opacity = opacity;
        this.type = 2 /* OPACITY */;
        this.target = 2 /* BACKGROUND_BORDERS */ | 4 /* CONTENT */;
    }
    return OpacityEffect;
}());
var isTransformEffect = function (effect) {
    return effect.type === 0 /* TRANSFORM */;
};
var isClipEffect = function (effect) { return effect.type === 1 /* CLIP */; };
var isOpacityEffect = function (effect) { return effect.type === 2 /* OPACITY */; };

var equalPath = function (a, b) {
    if (a.length === b.length) {
        return a.some(function (v, i) { return v === b[i]; });
    }
    return false;
};
var transformPath = function (path, deltaX, deltaY, deltaW, deltaH) {
    return path.map(function (point, index) {
        switch (index) {
            case 0:
                return point.add(deltaX, deltaY);
            case 1:
                return point.add(deltaX + deltaW, deltaY);
            case 2:
                return point.add(deltaX + deltaW, deltaY + deltaH);
            case 3:
                return point.add(deltaX, deltaY + deltaH);
        }
        return point;
    });
};

var StackingContext = /** @class */ (function () {
    function StackingContext(container) {
        this.element = container;
        this.inlineLevel = [];
        this.nonInlineLevel = [];
        this.negativeZIndex = [];
        this.zeroOrAutoZIndexOrTransformedOrOpacity = [];
        this.positiveZIndex = [];
        this.nonPositionedFloats = [];
        this.nonPositionedInlineLevel = [];
    }
    return StackingContext;
}());
var ElementPaint = /** @class */ (function () {
    function ElementPaint(container, parent) {
        this.container = container;
        this.parent = parent;
        this.effects = [];
        this.curves = new BoundCurves(this.container);
        if (this.container.styles.opacity < 1) {
            this.effects.push(new OpacityEffect(this.container.styles.opacity));
        }
        if (this.container.styles.transform !== null) {
            var offsetX = this.container.bounds.left + this.container.styles.transformOrigin[0].number;
            var offsetY = this.container.bounds.top + this.container.styles.transformOrigin[1].number;
            var matrix = this.container.styles.transform;
            this.effects.push(new TransformEffect(offsetX, offsetY, matrix));
        }
        if (this.container.styles.overflowX !== 0 /* VISIBLE */) {
            var borderBox = calculateBorderBoxPath(this.curves);
            var paddingBox = calculatePaddingBoxPath(this.curves);
            if (equalPath(borderBox, paddingBox)) {
                this.effects.push(new ClipEffect(borderBox, 2 /* BACKGROUND_BORDERS */ | 4 /* CONTENT */));
            }
            else {
                this.effects.push(new ClipEffect(borderBox, 2 /* BACKGROUND_BORDERS */));
                this.effects.push(new ClipEffect(paddingBox, 4 /* CONTENT */));
            }
        }
    }
    ElementPaint.prototype.getEffects = function (target) {
        var inFlow = [2 /* ABSOLUTE */, 3 /* FIXED */].indexOf(this.container.styles.position) === -1;
        var parent = this.parent;
        var effects = this.effects.slice(0);
        while (parent) {
            var croplessEffects = parent.effects.filter(function (effect) { return !isClipEffect(effect); });
            if (inFlow || parent.container.styles.position !== 0 /* STATIC */ || !parent.parent) {
                effects.unshift.apply(effects, croplessEffects);
                inFlow = [2 /* ABSOLUTE */, 3 /* FIXED */].indexOf(parent.container.styles.position) === -1;
                if (parent.container.styles.overflowX !== 0 /* VISIBLE */) {
                    var borderBox = calculateBorderBoxPath(parent.curves);
                    var paddingBox = calculatePaddingBoxPath(parent.curves);
                    if (!equalPath(borderBox, paddingBox)) {
                        effects.unshift(new ClipEffect(paddingBox, 2 /* BACKGROUND_BORDERS */ | 4 /* CONTENT */));
                    }
                }
            }
            else {
                effects.unshift.apply(effects, croplessEffects);
            }
            parent = parent.parent;
        }
        return effects.filter(function (effect) { return contains(effect.target, target); });
    };
    return ElementPaint;
}());
var parseStackTree = function (parent, stackingContext, realStackingContext, listItems) {
    parent.container.elements.forEach(function (child) {
        var treatAsRealStackingContext = contains(child.flags, 4 /* CREATES_REAL_STACKING_CONTEXT */);
        var createsStackingContext = contains(child.flags, 2 /* CREATES_STACKING_CONTEXT */);
        var paintContainer = new ElementPaint(child, parent);
        if (contains(child.styles.display, 2048 /* LIST_ITEM */)) {
            listItems.push(paintContainer);
        }
        var listOwnerItems = contains(child.flags, 8 /* IS_LIST_OWNER */) ? [] : listItems;
        if (treatAsRealStackingContext || createsStackingContext) {
            var parentStack = treatAsRealStackingContext || child.styles.isPositioned() ? realStackingContext : stackingContext;
            var stack = new StackingContext(paintContainer);
            if (child.styles.isPositioned() || child.styles.opacity < 1 || child.styles.isTransformed()) {
                var order_1 = child.styles.zIndex.order;
                if (order_1 < 0) {
                    var index_1 = 0;
                    parentStack.negativeZIndex.some(function (current, i) {
                        if (order_1 > current.element.container.styles.zIndex.order) {
                            index_1 = i;
                            return false;
                        }
                        else if (index_1 > 0) {
                            return true;
                        }
                        return false;
                    });
                    parentStack.negativeZIndex.splice(index_1, 0, stack);
                }
                else if (order_1 > 0) {
                    var index_2 = 0;
                    parentStack.positiveZIndex.some(function (current, i) {
                        if (order_1 >= current.element.container.styles.zIndex.order) {
                            index_2 = i + 1;
                            return false;
                        }
                        else if (index_2 > 0) {
                            return true;
                        }
                        return false;
                    });
                    parentStack.positiveZIndex.splice(index_2, 0, stack);
                }
                else {
                    parentStack.zeroOrAutoZIndexOrTransformedOrOpacity.push(stack);
                }
            }
            else {
                if (child.styles.isFloating()) {
                    parentStack.nonPositionedFloats.push(stack);
                }
                else {
                    parentStack.nonPositionedInlineLevel.push(stack);
                }
            }
            parseStackTree(paintContainer, stack, treatAsRealStackingContext ? stack : realStackingContext, listOwnerItems);
        }
        else {
            if (child.styles.isInlineLevel()) {
                stackingContext.inlineLevel.push(paintContainer);
            }
            else {
                stackingContext.nonInlineLevel.push(paintContainer);
            }
            parseStackTree(paintContainer, stackingContext, realStackingContext, listOwnerItems);
        }
        if (contains(child.flags, 8 /* IS_LIST_OWNER */)) {
            processListItems(child, listOwnerItems);
        }
    });
};
var processListItems = function (owner, elements) {
    var numbering = owner instanceof OLElementContainer ? owner.start : 1;
    var reversed = owner instanceof OLElementContainer ? owner.reversed : false;
    for (var i = 0; i < elements.length; i++) {
        var item = elements[i];
        if (item.container instanceof LIElementContainer &&
            typeof item.container.value === 'number' &&
            item.container.value !== 0) {
            numbering = item.container.value;
        }
        item.listValue = createCounterText(numbering, item.container.styles.listStyleType, true);
        numbering += reversed ? -1 : 1;
    }
};
var parseStackingContexts = function (container) {
    var paintContainer = new ElementPaint(container, null);
    var root = new StackingContext(paintContainer);
    var listItems = [];
    parseStackTree(paintContainer, root, root, listItems);
    processListItems(paintContainer.container, listItems);
    return root;
};

var parsePathForBorder = function (curves, borderSide) {
    switch (borderSide) {
        case 0:
            return createPathFromCurves(curves.topLeftBorderBox, curves.topLeftPaddingBox, curves.topRightBorderBox, curves.topRightPaddingBox);
        case 1:
            return createPathFromCurves(curves.topRightBorderBox, curves.topRightPaddingBox, curves.bottomRightBorderBox, curves.bottomRightPaddingBox);
        case 2:
            return createPathFromCurves(curves.bottomRightBorderBox, curves.bottomRightPaddingBox, curves.bottomLeftBorderBox, curves.bottomLeftPaddingBox);
        case 3:
        default:
            return createPathFromCurves(curves.bottomLeftBorderBox, curves.bottomLeftPaddingBox, curves.topLeftBorderBox, curves.topLeftPaddingBox);
    }
};
var parsePathForBorderDoubleOuter = function (curves, borderSide) {
    switch (borderSide) {
        case 0:
            return createPathFromCurves(curves.topLeftBorderBox, curves.topLeftBorderDoubleOuterBox, curves.topRightBorderBox, curves.topRightBorderDoubleOuterBox);
        case 1:
            return createPathFromCurves(curves.topRightBorderBox, curves.topRightBorderDoubleOuterBox, curves.bottomRightBorderBox, curves.bottomRightBorderDoubleOuterBox);
        case 2:
            return createPathFromCurves(curves.bottomRightBorderBox, curves.bottomRightBorderDoubleOuterBox, curves.bottomLeftBorderBox, curves.bottomLeftBorderDoubleOuterBox);
        case 3:
        default:
            return createPathFromCurves(curves.bottomLeftBorderBox, curves.bottomLeftBorderDoubleOuterBox, curves.topLeftBorderBox, curves.topLeftBorderDoubleOuterBox);
    }
};
var parsePathForBorderDoubleInner = function (curves, borderSide) {
    switch (borderSide) {
        case 0:
            return createPathFromCurves(curves.topLeftBorderDoubleInnerBox, curves.topLeftPaddingBox, curves.topRightBorderDoubleInnerBox, curves.topRightPaddingBox);
        case 1:
            return createPathFromCurves(curves.topRightBorderDoubleInnerBox, curves.topRightPaddingBox, curves.bottomRightBorderDoubleInnerBox, curves.bottomRightPaddingBox);
        case 2:
            return createPathFromCurves(curves.bottomRightBorderDoubleInnerBox, curves.bottomRightPaddingBox, curves.bottomLeftBorderDoubleInnerBox, curves.bottomLeftPaddingBox);
        case 3:
        default:
            return createPathFromCurves(curves.bottomLeftBorderDoubleInnerBox, curves.bottomLeftPaddingBox, curves.topLeftBorderDoubleInnerBox, curves.topLeftPaddingBox);
    }
};
var parsePathForBorderStroke = function (curves, borderSide) {
    switch (borderSide) {
        case 0:
            return createStrokePathFromCurves(curves.topLeftBorderStroke, curves.topRightBorderStroke);
        case 1:
            return createStrokePathFromCurves(curves.topRightBorderStroke, curves.bottomRightBorderStroke);
        case 2:
            return createStrokePathFromCurves(curves.bottomRightBorderStroke, curves.bottomLeftBorderStroke);
        case 3:
        default:
            return createStrokePathFromCurves(curves.bottomLeftBorderStroke, curves.topLeftBorderStroke);
    }
};
var createStrokePathFromCurves = function (outer1, outer2) {
    var path = [];
    if (isBezierCurve(outer1)) {
        path.push(outer1.subdivide(0.5, false));
    }
    else {
        path.push(outer1);
    }
    if (isBezierCurve(outer2)) {
        path.push(outer2.subdivide(0.5, true));
    }
    else {
        path.push(outer2);
    }
    return path;
};
var createPathFromCurves = function (outer1, inner1, outer2, inner2) {
    var path = [];
    if (isBezierCurve(outer1)) {
        path.push(outer1.subdivide(0.5, false));
    }
    else {
        path.push(outer1);
    }
    if (isBezierCurve(outer2)) {
        path.push(outer2.subdivide(0.5, true));
    }
    else {
        path.push(outer2);
    }
    if (isBezierCurve(inner2)) {
        path.push(inner2.subdivide(0.5, true).reverse());
    }
    else {
        path.push(inner2);
    }
    if (isBezierCurve(inner1)) {
        path.push(inner1.subdivide(0.5, false).reverse());
    }
    else {
        path.push(inner1);
    }
    return path;
};

var paddingBox = function (element) {
    var bounds = element.bounds;
    var styles = element.styles;
    return bounds.add(styles.borderLeftWidth, styles.borderTopWidth, -(styles.borderRightWidth + styles.borderLeftWidth), -(styles.borderTopWidth + styles.borderBottomWidth));
};
var contentBox = function (element) {
    var styles = element.styles;
    var bounds = element.bounds;
    var paddingLeft = getAbsoluteValue(styles.paddingLeft, bounds.width);
    var paddingRight = getAbsoluteValue(styles.paddingRight, bounds.width);
    var paddingTop = getAbsoluteValue(styles.paddingTop, bounds.width);
    var paddingBottom = getAbsoluteValue(styles.paddingBottom, bounds.width);
    return bounds.add(paddingLeft + styles.borderLeftWidth, paddingTop + styles.borderTopWidth, -(styles.borderRightWidth + styles.borderLeftWidth + paddingLeft + paddingRight), -(styles.borderTopWidth + styles.borderBottomWidth + paddingTop + paddingBottom));
};

var calculateBackgroundPositioningArea = function (backgroundOrigin, element) {
    if (backgroundOrigin === 0 /* BORDER_BOX */) {
        return element.bounds;
    }
    if (backgroundOrigin === 2 /* CONTENT_BOX */) {
        return contentBox(element);
    }
    return paddingBox(element);
};
var calculateBackgroundPaintingArea = function (backgroundClip, element) {
    if (backgroundClip === 0 /* BORDER_BOX */) {
        return element.bounds;
    }
    if (backgroundClip === 2 /* CONTENT_BOX */) {
        return contentBox(element);
    }
    return paddingBox(element);
};
var calculateBackgroundRendering = function (container, index, intrinsicSize) {
    var backgroundPositioningArea = calculateBackgroundPositioningArea(getBackgroundValueForIndex(container.styles.backgroundOrigin, index), container);
    var backgroundPaintingArea = calculateBackgroundPaintingArea(getBackgroundValueForIndex(container.styles.backgroundClip, index), container);
    var backgroundImageSize = calculateBackgroundSize(getBackgroundValueForIndex(container.styles.backgroundSize, index), intrinsicSize, backgroundPositioningArea);
    var sizeWidth = backgroundImageSize[0], sizeHeight = backgroundImageSize[1];
    var position = getAbsoluteValueForTuple(getBackgroundValueForIndex(container.styles.backgroundPosition, index), backgroundPositioningArea.width - sizeWidth, backgroundPositioningArea.height - sizeHeight);
    var path = calculateBackgroundRepeatPath(getBackgroundValueForIndex(container.styles.backgroundRepeat, index), position, backgroundImageSize, backgroundPositioningArea, backgroundPaintingArea);
    var offsetX = Math.round(backgroundPositioningArea.left + position[0]);
    var offsetY = Math.round(backgroundPositioningArea.top + position[1]);
    return [path, offsetX, offsetY, sizeWidth, sizeHeight];
};
var isAuto = function (token) { return isIdentToken(token) && token.value === BACKGROUND_SIZE.AUTO; };
var hasIntrinsicValue = function (value) { return typeof value === 'number'; };
var calculateBackgroundSize = function (size, _a, bounds) {
    var intrinsicWidth = _a[0], intrinsicHeight = _a[1], intrinsicProportion = _a[2];
    var first = size[0], second = size[1];
    if (!first) {
        return [0, 0];
    }
    if (isLengthPercentage(first) && second && isLengthPercentage(second)) {
        return [getAbsoluteValue(first, bounds.width), getAbsoluteValue(second, bounds.height)];
    }
    var hasIntrinsicProportion = hasIntrinsicValue(intrinsicProportion);
    if (isIdentToken(first) && (first.value === BACKGROUND_SIZE.CONTAIN || first.value === BACKGROUND_SIZE.COVER)) {
        if (hasIntrinsicValue(intrinsicProportion)) {
            var targetRatio = bounds.width / bounds.height;
            return targetRatio < intrinsicProportion !== (first.value === BACKGROUND_SIZE.COVER)
                ? [bounds.width, bounds.width / intrinsicProportion]
                : [bounds.height * intrinsicProportion, bounds.height];
        }
        return [bounds.width, bounds.height];
    }
    var hasIntrinsicWidth = hasIntrinsicValue(intrinsicWidth);
    var hasIntrinsicHeight = hasIntrinsicValue(intrinsicHeight);
    var hasIntrinsicDimensions = hasIntrinsicWidth || hasIntrinsicHeight;
    // If the background-size is auto or auto auto:
    if (isAuto(first) && (!second || isAuto(second))) {
        // If the image has both horizontal and vertical intrinsic dimensions, it's rendered at that size.
        if (hasIntrinsicWidth && hasIntrinsicHeight) {
            return [intrinsicWidth, intrinsicHeight];
        }
        // If the image has no intrinsic dimensions and has no intrinsic proportions,
        // it's rendered at the size of the background positioning area.
        if (!hasIntrinsicProportion && !hasIntrinsicDimensions) {
            return [bounds.width, bounds.height];
        }
        // TODO If the image has no intrinsic dimensions but has intrinsic proportions, it's rendered as if contain had been specified instead.
        // If the image has only one intrinsic dimension and has intrinsic proportions, it's rendered at the size corresponding to that one dimension.
        // The other dimension is computed using the specified dimension and the intrinsic proportions.
        if (hasIntrinsicDimensions && hasIntrinsicProportion) {
            var width_1 = hasIntrinsicWidth
                ? intrinsicWidth
                : intrinsicHeight * intrinsicProportion;
            var height_1 = hasIntrinsicHeight
                ? intrinsicHeight
                : intrinsicWidth / intrinsicProportion;
            return [width_1, height_1];
        }
        // If the image has only one intrinsic dimension but has no intrinsic proportions,
        // it's rendered using the specified dimension and the other dimension of the background positioning area.
        var width_2 = hasIntrinsicWidth ? intrinsicWidth : bounds.width;
        var height_2 = hasIntrinsicHeight ? intrinsicHeight : bounds.height;
        return [width_2, height_2];
    }
    // If the image has intrinsic proportions, it's stretched to the specified dimension.
    // The unspecified dimension is computed using the specified dimension and the intrinsic proportions.
    if (hasIntrinsicProportion) {
        var width_3 = 0;
        var height_3 = 0;
        if (isLengthPercentage(first)) {
            width_3 = getAbsoluteValue(first, bounds.width);
        }
        else if (isLengthPercentage(second)) {
            height_3 = getAbsoluteValue(second, bounds.height);
        }
        if (isAuto(first)) {
            width_3 = height_3 * intrinsicProportion;
        }
        else if (!second || isAuto(second)) {
            height_3 = width_3 / intrinsicProportion;
        }
        return [width_3, height_3];
    }
    // If the image has no intrinsic proportions, it's stretched to the specified dimension.
    // The unspecified dimension is computed using the image's corresponding intrinsic dimension,
    // if there is one. If there is no such intrinsic dimension,
    // it becomes the corresponding dimension of the background positioning area.
    var width = null;
    var height = null;
    if (isLengthPercentage(first)) {
        width = getAbsoluteValue(first, bounds.width);
    }
    else if (second && isLengthPercentage(second)) {
        height = getAbsoluteValue(second, bounds.height);
    }
    if (width !== null && (!second || isAuto(second))) {
        height =
            hasIntrinsicWidth && hasIntrinsicHeight
                ? (width / intrinsicWidth) * intrinsicHeight
                : bounds.height;
    }
    if (height !== null && isAuto(first)) {
        width =
            hasIntrinsicWidth && hasIntrinsicHeight
                ? (height / intrinsicHeight) * intrinsicWidth
                : bounds.width;
    }
    if (width !== null && height !== null) {
        return [width, height];
    }
    throw new Error("Unable to calculate background-size for element");
};
var getBackgroundValueForIndex = function (values, index) {
    var value = values[index];
    if (typeof value === 'undefined') {
        return values[0];
    }
    return value;
};
var calculateBackgroundRepeatPath = function (repeat, _a, _b, backgroundPositioningArea, backgroundPaintingArea) {
    var x = _a[0], y = _a[1];
    var width = _b[0], height = _b[1];
    switch (repeat) {
        case 2 /* REPEAT_X */:
            return [
                new Vector$1(Math.round(backgroundPositioningArea.left), Math.round(backgroundPositioningArea.top + y)),
                new Vector$1(Math.round(backgroundPositioningArea.left + backgroundPositioningArea.width), Math.round(backgroundPositioningArea.top + y)),
                new Vector$1(Math.round(backgroundPositioningArea.left + backgroundPositioningArea.width), Math.round(height + backgroundPositioningArea.top + y)),
                new Vector$1(Math.round(backgroundPositioningArea.left), Math.round(height + backgroundPositioningArea.top + y))
            ];
        case 3 /* REPEAT_Y */:
            return [
                new Vector$1(Math.round(backgroundPositioningArea.left + x), Math.round(backgroundPositioningArea.top)),
                new Vector$1(Math.round(backgroundPositioningArea.left + x + width), Math.round(backgroundPositioningArea.top)),
                new Vector$1(Math.round(backgroundPositioningArea.left + x + width), Math.round(backgroundPositioningArea.height + backgroundPositioningArea.top)),
                new Vector$1(Math.round(backgroundPositioningArea.left + x), Math.round(backgroundPositioningArea.height + backgroundPositioningArea.top))
            ];
        case 1 /* NO_REPEAT */:
            return [
                new Vector$1(Math.round(backgroundPositioningArea.left + x), Math.round(backgroundPositioningArea.top + y)),
                new Vector$1(Math.round(backgroundPositioningArea.left + x + width), Math.round(backgroundPositioningArea.top + y)),
                new Vector$1(Math.round(backgroundPositioningArea.left + x + width), Math.round(backgroundPositioningArea.top + y + height)),
                new Vector$1(Math.round(backgroundPositioningArea.left + x), Math.round(backgroundPositioningArea.top + y + height))
            ];
        default:
            return [
                new Vector$1(Math.round(backgroundPaintingArea.left), Math.round(backgroundPaintingArea.top)),
                new Vector$1(Math.round(backgroundPaintingArea.left + backgroundPaintingArea.width), Math.round(backgroundPaintingArea.top)),
                new Vector$1(Math.round(backgroundPaintingArea.left + backgroundPaintingArea.width), Math.round(backgroundPaintingArea.height + backgroundPaintingArea.top)),
                new Vector$1(Math.round(backgroundPaintingArea.left), Math.round(backgroundPaintingArea.height + backgroundPaintingArea.top))
            ];
    }
};

var SMALL_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';

var SAMPLE_TEXT = 'Hidden Text';
var FontMetrics = /** @class */ (function () {
    function FontMetrics(document) {
        this._data = {};
        this._document = document;
    }
    FontMetrics.prototype.parseMetrics = function (fontFamily, fontSize) {
        var container = this._document.createElement('div');
        var img = this._document.createElement('img');
        var span = this._document.createElement('span');
        var body = this._document.body;
        container.style.visibility = 'hidden';
        container.style.fontFamily = fontFamily;
        container.style.fontSize = fontSize;
        container.style.margin = '0';
        container.style.padding = '0';
        container.style.whiteSpace = 'nowrap';
        body.appendChild(container);
        img.src = SMALL_IMAGE;
        img.width = 1;
        img.height = 1;
        img.style.margin = '0';
        img.style.padding = '0';
        img.style.verticalAlign = 'baseline';
        span.style.fontFamily = fontFamily;
        span.style.fontSize = fontSize;
        span.style.margin = '0';
        span.style.padding = '0';
        span.appendChild(this._document.createTextNode(SAMPLE_TEXT));
        container.appendChild(span);
        container.appendChild(img);
        var baseline = img.offsetTop - span.offsetTop + 2;
        container.removeChild(span);
        container.appendChild(this._document.createTextNode(SAMPLE_TEXT));
        container.style.lineHeight = 'normal';
        img.style.verticalAlign = 'super';
        var middle = img.offsetTop - container.offsetTop + 2;
        body.removeChild(container);
        return { baseline: baseline, middle: middle };
    };
    FontMetrics.prototype.getMetrics = function (fontFamily, fontSize) {
        var key = fontFamily + " " + fontSize;
        if (typeof this._data[key] === 'undefined') {
            this._data[key] = this.parseMetrics(fontFamily, fontSize);
        }
        return this._data[key];
    };
    return FontMetrics;
}());

var Renderer = /** @class */ (function () {
    function Renderer(context, options) {
        this.context = context;
        this.options = options;
    }
    return Renderer;
}());

var MASK_OFFSET = 10000;
var CanvasRenderer = /** @class */ (function (_super) {
    __extends(CanvasRenderer, _super);
    function CanvasRenderer(context, options) {
        var _this = _super.call(this, context, options) || this;
        _this._activeEffects = [];
        _this.canvas = options.canvas ? options.canvas : document.createElement('canvas');
        _this.ctx = _this.canvas.getContext('2d');
        if (!options.canvas) {
            _this.canvas.width = Math.floor(options.width * options.scale);
            _this.canvas.height = Math.floor(options.height * options.scale);
            _this.canvas.style.width = options.width + "px";
            _this.canvas.style.height = options.height + "px";
        }
        _this.fontMetrics = new FontMetrics(document);
        _this.ctx.scale(_this.options.scale, _this.options.scale);
        _this.ctx.translate(-options.x, -options.y);
        _this.ctx.textBaseline = 'bottom';
        _this._activeEffects = [];
        _this.context.logger.debug("Canvas renderer initialized (" + options.width + "x" + options.height + ") with scale " + options.scale);
        return _this;
    }
    CanvasRenderer.prototype.applyEffects = function (effects) {
        var _this = this;
        while (this._activeEffects.length) {
            this.popEffect();
        }
        effects.forEach(function (effect) { return _this.applyEffect(effect); });
    };
    CanvasRenderer.prototype.applyEffect = function (effect) {
        this.ctx.save();
        if (isOpacityEffect(effect)) {
            this.ctx.globalAlpha = effect.opacity;
        }
        if (isTransformEffect(effect)) {
            this.ctx.translate(effect.offsetX, effect.offsetY);
            this.ctx.transform(effect.matrix[0], effect.matrix[1], effect.matrix[2], effect.matrix[3], effect.matrix[4], effect.matrix[5]);
            this.ctx.translate(-effect.offsetX, -effect.offsetY);
        }
        if (isClipEffect(effect)) {
            this.path(effect.path);
            this.ctx.clip();
        }
        this._activeEffects.push(effect);
    };
    CanvasRenderer.prototype.popEffect = function () {
        this._activeEffects.pop();
        this.ctx.restore();
    };
    CanvasRenderer.prototype.renderStack = function (stack) {
        return __awaiter(this, void 0, void 0, function () {
            var styles;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        styles = stack.element.container.styles;
                        if (!styles.isVisible()) return [3 /*break*/, 2];
                        return [4 /*yield*/, this.renderStackContent(stack)];
                    case 1:
                        _a.sent();
                        _a.label = 2;
                    case 2: return [2 /*return*/];
                }
            });
        });
    };
    CanvasRenderer.prototype.renderNode = function (paint) {
        return __awaiter(this, void 0, void 0, function () {
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (contains(paint.container.flags, 16 /* DEBUG_RENDER */)) {
                            debugger;
                        }
                        if (!paint.container.styles.isVisible()) return [3 /*break*/, 3];
                        return [4 /*yield*/, this.renderNodeBackgroundAndBorders(paint)];
                    case 1:
                        _a.sent();
                        return [4 /*yield*/, this.renderNodeContent(paint)];
                    case 2:
                        _a.sent();
                        _a.label = 3;
                    case 3: return [2 /*return*/];
                }
            });
        });
    };
    CanvasRenderer.prototype.renderTextWithLetterSpacing = function (text, letterSpacing, baseline) {
        var _this = this;
        if (letterSpacing === 0) {
            this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + baseline);
        }
        else {
            var letters = segmentGraphemes(text.text);
            letters.reduce(function (left, letter) {
                _this.ctx.fillText(letter, left, text.bounds.top + baseline);
                return left + _this.ctx.measureText(letter).width;
            }, text.bounds.left);
        }
    };
    CanvasRenderer.prototype.createFontStyle = function (styles) {
        var fontVariant = styles.fontVariant
            .filter(function (variant) { return variant === 'normal' || variant === 'small-caps'; })
            .join('');
        var fontFamily = fixIOSSystemFonts(styles.fontFamily).join(', ');
        var fontSize = isDimensionToken(styles.fontSize)
            ? "" + styles.fontSize.number + styles.fontSize.unit
            : styles.fontSize.number + "px";
        return [
            [styles.fontStyle, fontVariant, styles.fontWeight, fontSize, fontFamily].join(' '),
            fontFamily,
            fontSize
        ];
    };
    CanvasRenderer.prototype.renderTextNode = function (text, styles) {
        return __awaiter(this, void 0, void 0, function () {
            var _a, font, fontFamily, fontSize, _b, baseline, middle, paintOrder;
            var _this = this;
            return __generator(this, function (_c) {
                _a = this.createFontStyle(styles), font = _a[0], fontFamily = _a[1], fontSize = _a[2];
                this.ctx.font = font;
                this.ctx.direction = styles.direction === 1 /* RTL */ ? 'rtl' : 'ltr';
                this.ctx.textAlign = 'left';
                this.ctx.textBaseline = 'alphabetic';
                _b = this.fontMetrics.getMetrics(fontFamily, fontSize), baseline = _b.baseline, middle = _b.middle;
                paintOrder = styles.paintOrder;
                text.textBounds.forEach(function (text) {
                    paintOrder.forEach(function (paintOrderLayer) {
                        switch (paintOrderLayer) {
                            case 0 /* FILL */:
                                _this.ctx.fillStyle = asString(styles.color);
                                _this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline);
                                var textShadows = styles.textShadow;
                                if (textShadows.length && text.text.trim().length) {
                                    textShadows
                                        .slice(0)
                                        .reverse()
                                        .forEach(function (textShadow) {
                                        _this.ctx.shadowColor = asString(textShadow.color);
                                        _this.ctx.shadowOffsetX = textShadow.offsetX.number * _this.options.scale;
                                        _this.ctx.shadowOffsetY = textShadow.offsetY.number * _this.options.scale;
                                        _this.ctx.shadowBlur = textShadow.blur.number;
                                        _this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline);
                                    });
                                    _this.ctx.shadowColor = '';
                                    _this.ctx.shadowOffsetX = 0;
                                    _this.ctx.shadowOffsetY = 0;
                                    _this.ctx.shadowBlur = 0;
                                }
                                if (styles.textDecorationLine.length) {
                                    _this.ctx.fillStyle = asString(styles.textDecorationColor || styles.color);
                                    styles.textDecorationLine.forEach(function (textDecorationLine) {
                                        switch (textDecorationLine) {
                                            case 1 /* UNDERLINE */:
                                                // Draws a line at the baseline of the font
                                                // TODO As some browsers display the line as more than 1px if the font-size is big,
                                                // need to take that into account both in position and size
                                                _this.ctx.fillRect(text.bounds.left, Math.round(text.bounds.top + baseline), text.bounds.width, 1);
                                                break;
                                            case 2 /* OVERLINE */:
                                                _this.ctx.fillRect(text.bounds.left, Math.round(text.bounds.top), text.bounds.width, 1);
                                                break;
                                            case 3 /* LINE_THROUGH */:
                                                // TODO try and find exact position for line-through
                                                _this.ctx.fillRect(text.bounds.left, Math.ceil(text.bounds.top + middle), text.bounds.width, 1);
                                                break;
                                        }
                                    });
                                }
                                break;
                            case 1 /* STROKE */:
                                if (styles.webkitTextStrokeWidth && text.text.trim().length) {
                                    _this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
                                    _this.ctx.lineWidth = styles.webkitTextStrokeWidth;
                                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
                                    _this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
                                    _this.ctx.strokeText(text.text, text.bounds.left, text.bounds.top + baseline);
                                }
                                _this.ctx.strokeStyle = '';
                                _this.ctx.lineWidth = 0;
                                _this.ctx.lineJoin = 'miter';
                                break;
                        }
                    });
                });
                return [2 /*return*/];
            });
        });
    };
    CanvasRenderer.prototype.renderReplacedElement = function (container, curves, image) {
        if (image && container.intrinsicWidth > 0 && container.intrinsicHeight > 0) {
            var box = contentBox(container);
            var path = calculatePaddingBoxPath(curves);
            this.path(path);
            this.ctx.save();
            this.ctx.clip();
            this.ctx.drawImage(image, 0, 0, container.intrinsicWidth, container.intrinsicHeight, box.left, box.top, box.width, box.height);
            this.ctx.restore();
        }
    };
    CanvasRenderer.prototype.renderNodeContent = function (paint) {
        return __awaiter(this, void 0, void 0, function () {
            var container, curves, styles, _i, _a, child, image, image, iframeRenderer, canvas, size, _b, fontFamily, fontSize, baseline, bounds, x, textBounds, img, image, url, fontFamily, bounds;
            return __generator(this, function (_c) {
                switch (_c.label) {
                    case 0:
                        this.applyEffects(paint.getEffects(4 /* CONTENT */));
                        container = paint.container;
                        curves = paint.curves;
                        styles = container.styles;
                        _i = 0, _a = container.textNodes;
                        _c.label = 1;
                    case 1:
                        if (!(_i < _a.length)) return [3 /*break*/, 4];
                        child = _a[_i];
                        return [4 /*yield*/, this.renderTextNode(child, styles)];
                    case 2:
                        _c.sent();
                        _c.label = 3;
                    case 3:
                        _i++;
                        return [3 /*break*/, 1];
                    case 4:
                        if (!(container instanceof ImageElementContainer)) return [3 /*break*/, 8];
                        _c.label = 5;
                    case 5:
                        _c.trys.push([5, 7, , 8]);
                        return [4 /*yield*/, this.context.cache.match(container.src)];
                    case 6:
                        image = _c.sent();
                        this.renderReplacedElement(container, curves, image);
                        return [3 /*break*/, 8];
                    case 7:
                        _c.sent();
                        this.context.logger.error("Error loading image " + container.src);
                        return [3 /*break*/, 8];
                    case 8:
                        if (container instanceof CanvasElementContainer) {
                            this.renderReplacedElement(container, curves, container.canvas);
                        }
                        if (!(container instanceof SVGElementContainer)) return [3 /*break*/, 12];
                        _c.label = 9;
                    case 9:
                        _c.trys.push([9, 11, , 12]);
                        return [4 /*yield*/, this.context.cache.match(container.svg)];
                    case 10:
                        image = _c.sent();
                        this.renderReplacedElement(container, curves, image);
                        return [3 /*break*/, 12];
                    case 11:
                        _c.sent();
                        this.context.logger.error("Error loading svg " + container.svg.substring(0, 255));
                        return [3 /*break*/, 12];
                    case 12:
                        if (!(container instanceof IFrameElementContainer && container.tree)) return [3 /*break*/, 14];
                        iframeRenderer = new CanvasRenderer(this.context, {
                            scale: this.options.scale,
                            backgroundColor: container.backgroundColor,
                            x: 0,
                            y: 0,
                            width: container.width,
                            height: container.height
                        });
                        return [4 /*yield*/, iframeRenderer.render(container.tree)];
                    case 13:
                        canvas = _c.sent();
                        if (container.width && container.height) {
                            this.ctx.drawImage(canvas, 0, 0, container.width, container.height, container.bounds.left, container.bounds.top, container.bounds.width, container.bounds.height);
                        }
                        _c.label = 14;
                    case 14:
                        if (container instanceof InputElementContainer) {
                            size = Math.min(container.bounds.width, container.bounds.height);
                            if (container.type === CHECKBOX) {
                                if (container.checked) {
                                    this.ctx.save();
                                    this.path([
                                        new Vector$1(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79),
                                        new Vector$1(container.bounds.left + size * 0.16, container.bounds.top + size * 0.5549),
                                        new Vector$1(container.bounds.left + size * 0.27347, container.bounds.top + size * 0.44071),
                                        new Vector$1(container.bounds.left + size * 0.39694, container.bounds.top + size * 0.5649),
                                        new Vector$1(container.bounds.left + size * 0.72983, container.bounds.top + size * 0.23),
                                        new Vector$1(container.bounds.left + size * 0.84, container.bounds.top + size * 0.34085),
                                        new Vector$1(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79)
                                    ]);
                                    this.ctx.fillStyle = asString(INPUT_COLOR);
                                    this.ctx.fill();
                                    this.ctx.restore();
                                }
                            }
                            else if (container.type === RADIO) {
                                if (container.checked) {
                                    this.ctx.save();
                                    this.ctx.beginPath();
                                    this.ctx.arc(container.bounds.left + size / 2, container.bounds.top + size / 2, size / 4, 0, Math.PI * 2, true);
                                    this.ctx.fillStyle = asString(INPUT_COLOR);
                                    this.ctx.fill();
                                    this.ctx.restore();
                                }
                            }
                        }
                        if (isTextInputElement(container) && container.value.length) {
                            _b = this.createFontStyle(styles), fontFamily = _b[0], fontSize = _b[1];
                            baseline = this.fontMetrics.getMetrics(fontFamily, fontSize).baseline;
                            this.ctx.font = fontFamily;
                            this.ctx.fillStyle = asString(styles.color);
                            this.ctx.textBaseline = 'alphabetic';
                            this.ctx.textAlign = canvasTextAlign(container.styles.textAlign);
                            bounds = contentBox(container);
                            x = 0;
                            switch (container.styles.textAlign) {
                                case 1 /* CENTER */:
                                    x += bounds.width / 2;
                                    break;
                                case 2 /* RIGHT */:
                                    x += bounds.width;
                                    break;
                            }
                            textBounds = bounds.add(x, 0, 0, -bounds.height / 2 + 1);
                            this.ctx.save();
                            this.path([
                                new Vector$1(bounds.left, bounds.top),
                                new Vector$1(bounds.left + bounds.width, bounds.top),
                                new Vector$1(bounds.left + bounds.width, bounds.top + bounds.height),
                                new Vector$1(bounds.left, bounds.top + bounds.height)
                            ]);
                            this.ctx.clip();
                            this.renderTextWithLetterSpacing(new TextBounds(container.value, textBounds), styles.letterSpacing, baseline);
                            this.ctx.restore();
                            this.ctx.textBaseline = 'alphabetic';
                            this.ctx.textAlign = 'left';
                        }
                        if (!contains(container.styles.display, 2048 /* LIST_ITEM */)) return [3 /*break*/, 20];
                        if (!(container.styles.listStyleImage !== null)) return [3 /*break*/, 19];
                        img = container.styles.listStyleImage;
                        if (!(img.type === 0 /* URL */)) return [3 /*break*/, 18];
                        image = void 0;
                        url = img.url;
                        _c.label = 15;
                    case 15:
                        _c.trys.push([15, 17, , 18]);
                        return [4 /*yield*/, this.context.cache.match(url)];
                    case 16:
                        image = _c.sent();
                        this.ctx.drawImage(image, container.bounds.left - (image.width + 10), container.bounds.top);
                        return [3 /*break*/, 18];
                    case 17:
                        _c.sent();
                        this.context.logger.error("Error loading list-style-image " + url);
                        return [3 /*break*/, 18];
                    case 18: return [3 /*break*/, 20];
                    case 19:
                        if (paint.listValue && container.styles.listStyleType !== -1 /* NONE */) {
                            fontFamily = this.createFontStyle(styles)[0];
                            this.ctx.font = fontFamily;
                            this.ctx.fillStyle = asString(styles.color);
                            this.ctx.textBaseline = 'middle';
                            this.ctx.textAlign = 'right';
                            bounds = new Bounds(container.bounds.left, container.bounds.top + getAbsoluteValue(container.styles.paddingTop, container.bounds.width), container.bounds.width, computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 1);
                            this.renderTextWithLetterSpacing(new TextBounds(paint.listValue, bounds), styles.letterSpacing, computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 2);
                            this.ctx.textBaseline = 'bottom';
                            this.ctx.textAlign = 'left';
                        }
                        _c.label = 20;
                    case 20: return [2 /*return*/];
                }
            });
        });
    };
    CanvasRenderer.prototype.renderStackContent = function (stack) {
        return __awaiter(this, void 0, void 0, function () {
            var _i, _a, child, _b, _c, child, _d, _e, child, _f, _g, child, _h, _j, child, _k, _l, child, _m, _o, child;
            return __generator(this, function (_p) {
                switch (_p.label) {
                    case 0:
                        if (contains(stack.element.container.flags, 16 /* DEBUG_RENDER */)) {
                            debugger;
                        }
                        // https://www.w3.org/TR/css-position-3/#painting-order
                        // 1. the background and borders of the element forming the stacking context.
                        return [4 /*yield*/, this.renderNodeBackgroundAndBorders(stack.element)];
                    case 1:
                        // https://www.w3.org/TR/css-position-3/#painting-order
                        // 1. the background and borders of the element forming the stacking context.
                        _p.sent();
                        _i = 0, _a = stack.negativeZIndex;
                        _p.label = 2;
                    case 2:
                        if (!(_i < _a.length)) return [3 /*break*/, 5];
                        child = _a[_i];
                        return [4 /*yield*/, this.renderStack(child)];
                    case 3:
                        _p.sent();
                        _p.label = 4;
                    case 4:
                        _i++;
                        return [3 /*break*/, 2];
                    case 5: 
                    // 3. For all its in-flow, non-positioned, block-level descendants in tree order:
                    return [4 /*yield*/, this.renderNodeContent(stack.element)];
                    case 6:
                        // 3. For all its in-flow, non-positioned, block-level descendants in tree order:
                        _p.sent();
                        _b = 0, _c = stack.nonInlineLevel;
                        _p.label = 7;
                    case 7:
                        if (!(_b < _c.length)) return [3 /*break*/, 10];
                        child = _c[_b];
                        return [4 /*yield*/, this.renderNode(child)];
                    case 8:
                        _p.sent();
                        _p.label = 9;
                    case 9:
                        _b++;
                        return [3 /*break*/, 7];
                    case 10:
                        _d = 0, _e = stack.nonPositionedFloats;
                        _p.label = 11;
                    case 11:
                        if (!(_d < _e.length)) return [3 /*break*/, 14];
                        child = _e[_d];
                        return [4 /*yield*/, this.renderStack(child)];
                    case 12:
                        _p.sent();
                        _p.label = 13;
                    case 13:
                        _d++;
                        return [3 /*break*/, 11];
                    case 14:
                        _f = 0, _g = stack.nonPositionedInlineLevel;
                        _p.label = 15;
                    case 15:
                        if (!(_f < _g.length)) return [3 /*break*/, 18];
                        child = _g[_f];
                        return [4 /*yield*/, this.renderStack(child)];
                    case 16:
                        _p.sent();
                        _p.label = 17;
                    case 17:
                        _f++;
                        return [3 /*break*/, 15];
                    case 18:
                        _h = 0, _j = stack.inlineLevel;
                        _p.label = 19;
                    case 19:
                        if (!(_h < _j.length)) return [3 /*break*/, 22];
                        child = _j[_h];
                        return [4 /*yield*/, this.renderNode(child)];
                    case 20:
                        _p.sent();
                        _p.label = 21;
                    case 21:
                        _h++;
                        return [3 /*break*/, 19];
                    case 22:
                        _k = 0, _l = stack.zeroOrAutoZIndexOrTransformedOrOpacity;
                        _p.label = 23;
                    case 23:
                        if (!(_k < _l.length)) return [3 /*break*/, 26];
                        child = _l[_k];
                        return [4 /*yield*/, this.renderStack(child)];
                    case 24:
                        _p.sent();
                        _p.label = 25;
                    case 25:
                        _k++;
                        return [3 /*break*/, 23];
                    case 26:
                        _m = 0, _o = stack.positiveZIndex;
                        _p.label = 27;
                    case 27:
                        if (!(_m < _o.length)) return [3 /*break*/, 30];
                        child = _o[_m];
                        return [4 /*yield*/, this.renderStack(child)];
                    case 28:
                        _p.sent();
                        _p.label = 29;
                    case 29:
                        _m++;
                        return [3 /*break*/, 27];
                    case 30: return [2 /*return*/];
                }
            });
        });
    };
    CanvasRenderer.prototype.mask = function (paths) {
        this.ctx.beginPath();
        this.ctx.moveTo(0, 0);
        this.ctx.lineTo(this.canvas.width, 0);
        this.ctx.lineTo(this.canvas.width, this.canvas.height);
        this.ctx.lineTo(0, this.canvas.height);
        this.ctx.lineTo(0, 0);
        this.formatPath(paths.slice(0).reverse());
        this.ctx.closePath();
    };
    CanvasRenderer.prototype.path = function (paths) {
        this.ctx.beginPath();
        this.formatPath(paths);
        this.ctx.closePath();
    };
    CanvasRenderer.prototype.formatPath = function (paths) {
        var _this = this;
        paths.forEach(function (point, index) {
            var start = isBezierCurve(point) ? point.start : point;
            if (index === 0) {
                _this.ctx.moveTo(start.x, start.y);
            }
            else {
                _this.ctx.lineTo(start.x, start.y);
            }
            if (isBezierCurve(point)) {
                _this.ctx.bezierCurveTo(point.startControl.x, point.startControl.y, point.endControl.x, point.endControl.y, point.end.x, point.end.y);
            }
        });
    };
    CanvasRenderer.prototype.renderRepeat = function (path, pattern, offsetX, offsetY) {
        this.path(path);
        this.ctx.fillStyle = pattern;
        this.ctx.translate(offsetX, offsetY);
        this.ctx.fill();
        this.ctx.translate(-offsetX, -offsetY);
    };
    CanvasRenderer.prototype.resizeImage = function (image, width, height) {
        var _a;
        if (image.width === width && image.height === height) {
            return image;
        }
        var ownerDocument = (_a = this.canvas.ownerDocument) !== null && _a !== void 0 ? _a : document;
        var canvas = ownerDocument.createElement('canvas');
        canvas.width = Math.max(1, width);
        canvas.height = Math.max(1, height);
        var ctx = canvas.getContext('2d');
        ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height);
        return canvas;
    };
    CanvasRenderer.prototype.renderBackgroundImage = function (container) {
        return __awaiter(this, void 0, void 0, function () {
            var index, _loop_1, this_1, _i, _a, backgroundImage;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        index = container.styles.backgroundImage.length - 1;
                        _loop_1 = function (backgroundImage) {
                            var image, url, _c, path, x, y, width, height, pattern, _d, path, x, y, width, height, _e, lineLength, x0, x1, y0, y1, canvas, ctx, gradient_1, pattern, _f, path, left, top_1, width, height, position, x, y, _g, rx, ry, radialGradient_1, midX, midY, f, invF;
                            return __generator(this, function (_h) {
                                switch (_h.label) {
                                    case 0:
                                        if (!(backgroundImage.type === 0 /* URL */)) return [3 /*break*/, 5];
                                        image = void 0;
                                        url = backgroundImage.url;
                                        _h.label = 1;
                                    case 1:
                                        _h.trys.push([1, 3, , 4]);
                                        return [4 /*yield*/, this_1.context.cache.match(url)];
                                    case 2:
                                        image = _h.sent();
                                        return [3 /*break*/, 4];
                                    case 3:
                                        _h.sent();
                                        this_1.context.logger.error("Error loading background-image " + url);
                                        return [3 /*break*/, 4];
                                    case 4:
                                        if (image) {
                                            _c = calculateBackgroundRendering(container, index, [
                                                image.width,
                                                image.height,
                                                image.width / image.height
                                            ]), path = _c[0], x = _c[1], y = _c[2], width = _c[3], height = _c[4];
                                            pattern = this_1.ctx.createPattern(this_1.resizeImage(image, width, height), 'repeat');
                                            this_1.renderRepeat(path, pattern, x, y);
                                        }
                                        return [3 /*break*/, 6];
                                    case 5:
                                        if (isLinearGradient(backgroundImage)) {
                                            _d = calculateBackgroundRendering(container, index, [null, null, null]), path = _d[0], x = _d[1], y = _d[2], width = _d[3], height = _d[4];
                                            _e = calculateGradientDirection(backgroundImage.angle, width, height), lineLength = _e[0], x0 = _e[1], x1 = _e[2], y0 = _e[3], y1 = _e[4];
                                            canvas = document.createElement('canvas');
                                            canvas.width = width;
                                            canvas.height = height;
                                            ctx = canvas.getContext('2d');
                                            gradient_1 = ctx.createLinearGradient(x0, y0, x1, y1);
                                            processColorStops(backgroundImage.stops, lineLength).forEach(function (colorStop) {
                                                return gradient_1.addColorStop(colorStop.stop, asString(colorStop.color));
                                            });
                                            ctx.fillStyle = gradient_1;
                                            ctx.fillRect(0, 0, width, height);
                                            if (width > 0 && height > 0) {
                                                pattern = this_1.ctx.createPattern(canvas, 'repeat');
                                                this_1.renderRepeat(path, pattern, x, y);
                                            }
                                        }
                                        else if (isRadialGradient(backgroundImage)) {
                                            _f = calculateBackgroundRendering(container, index, [
                                                null,
                                                null,
                                                null
                                            ]), path = _f[0], left = _f[1], top_1 = _f[2], width = _f[3], height = _f[4];
                                            position = backgroundImage.position.length === 0 ? [FIFTY_PERCENT] : backgroundImage.position;
                                            x = getAbsoluteValue(position[0], width);
                                            y = getAbsoluteValue(position[position.length - 1], height);
                                            _g = calculateRadius(backgroundImage, x, y, width, height), rx = _g[0], ry = _g[1];
                                            if (rx > 0 && ry > 0) {
                                                radialGradient_1 = this_1.ctx.createRadialGradient(left + x, top_1 + y, 0, left + x, top_1 + y, rx);
                                                processColorStops(backgroundImage.stops, rx * 2).forEach(function (colorStop) {
                                                    return radialGradient_1.addColorStop(colorStop.stop, asString(colorStop.color));
                                                });
                                                this_1.path(path);
                                                this_1.ctx.fillStyle = radialGradient_1;
                                                if (rx !== ry) {
                                                    midX = container.bounds.left + 0.5 * container.bounds.width;
                                                    midY = container.bounds.top + 0.5 * container.bounds.height;
                                                    f = ry / rx;
                                                    invF = 1 / f;
                                                    this_1.ctx.save();
                                                    this_1.ctx.translate(midX, midY);
                                                    this_1.ctx.transform(1, 0, 0, f, 0, 0);
                                                    this_1.ctx.translate(-midX, -midY);
                                                    this_1.ctx.fillRect(left, invF * (top_1 - midY) + midY, width, height * invF);
                                                    this_1.ctx.restore();
                                                }
                                                else {
                                                    this_1.ctx.fill();
                                                }
                                            }
                                        }
                                        _h.label = 6;
                                    case 6:
                                        index--;
                                        return [2 /*return*/];
                                }
                            });
                        };
                        this_1 = this;
                        _i = 0, _a = container.styles.backgroundImage.slice(0).reverse();
                        _b.label = 1;
                    case 1:
                        if (!(_i < _a.length)) return [3 /*break*/, 4];
                        backgroundImage = _a[_i];
                        return [5 /*yield**/, _loop_1(backgroundImage)];
                    case 2:
                        _b.sent();
                        _b.label = 3;
                    case 3:
                        _i++;
                        return [3 /*break*/, 1];
                    case 4: return [2 /*return*/];
                }
            });
        });
    };
    CanvasRenderer.prototype.renderSolidBorder = function (color, side, curvePoints) {
        return __awaiter(this, void 0, void 0, function () {
            return __generator(this, function (_a) {
                this.path(parsePathForBorder(curvePoints, side));
                this.ctx.fillStyle = asString(color);
                this.ctx.fill();
                return [2 /*return*/];
            });
        });
    };
    CanvasRenderer.prototype.renderDoubleBorder = function (color, width, side, curvePoints) {
        return __awaiter(this, void 0, void 0, function () {
            var outerPaths, innerPaths;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (!(width < 3)) return [3 /*break*/, 2];
                        return [4 /*yield*/, this.renderSolidBorder(color, side, curvePoints)];
                    case 1:
                        _a.sent();
                        return [2 /*return*/];
                    case 2:
                        outerPaths = parsePathForBorderDoubleOuter(curvePoints, side);
                        this.path(outerPaths);
                        this.ctx.fillStyle = asString(color);
                        this.ctx.fill();
                        innerPaths = parsePathForBorderDoubleInner(curvePoints, side);
                        this.path(innerPaths);
                        this.ctx.fill();
                        return [2 /*return*/];
                }
            });
        });
    };
    CanvasRenderer.prototype.renderNodeBackgroundAndBorders = function (paint) {
        return __awaiter(this, void 0, void 0, function () {
            var styles, hasBackground, borders, backgroundPaintingArea, side, _i, borders_1, border;
            var _this = this;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        this.applyEffects(paint.getEffects(2 /* BACKGROUND_BORDERS */));
                        styles = paint.container.styles;
                        hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
                        borders = [
                            { style: styles.borderTopStyle, color: styles.borderTopColor, width: styles.borderTopWidth },
                            { style: styles.borderRightStyle, color: styles.borderRightColor, width: styles.borderRightWidth },
                            { style: styles.borderBottomStyle, color: styles.borderBottomColor, width: styles.borderBottomWidth },
                            { style: styles.borderLeftStyle, color: styles.borderLeftColor, width: styles.borderLeftWidth }
                        ];
                        backgroundPaintingArea = calculateBackgroundCurvedPaintingArea(getBackgroundValueForIndex(styles.backgroundClip, 0), paint.curves);
                        if (!(hasBackground || styles.boxShadow.length)) return [3 /*break*/, 2];
                        this.ctx.save();
                        this.path(backgroundPaintingArea);
                        this.ctx.clip();
                        if (!isTransparent(styles.backgroundColor)) {
                            this.ctx.fillStyle = asString(styles.backgroundColor);
                            this.ctx.fill();
                        }
                        return [4 /*yield*/, this.renderBackgroundImage(paint.container)];
                    case 1:
                        _a.sent();
                        this.ctx.restore();
                        styles.boxShadow
                            .slice(0)
                            .reverse()
                            .forEach(function (shadow) {
                            _this.ctx.save();
                            var borderBoxArea = calculateBorderBoxPath(paint.curves);
                            var maskOffset = shadow.inset ? 0 : MASK_OFFSET;
                            var shadowPaintingArea = transformPath(borderBoxArea, -maskOffset + (shadow.inset ? 1 : -1) * shadow.spread.number, (shadow.inset ? 1 : -1) * shadow.spread.number, shadow.spread.number * (shadow.inset ? -2 : 2), shadow.spread.number * (shadow.inset ? -2 : 2));
                            if (shadow.inset) {
                                _this.path(borderBoxArea);
                                _this.ctx.clip();
                                _this.mask(shadowPaintingArea);
                            }
                            else {
                                _this.mask(borderBoxArea);
                                _this.ctx.clip();
                                _this.path(shadowPaintingArea);
                            }
                            _this.ctx.shadowOffsetX = shadow.offsetX.number + maskOffset;
                            _this.ctx.shadowOffsetY = shadow.offsetY.number;
                            _this.ctx.shadowColor = asString(shadow.color);
                            _this.ctx.shadowBlur = shadow.blur.number;
                            _this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : 'rgba(0,0,0,1)';
                            _this.ctx.fill();
                            _this.ctx.restore();
                        });
                        _a.label = 2;
                    case 2:
                        side = 0;
                        _i = 0, borders_1 = borders;
                        _a.label = 3;
                    case 3:
                        if (!(_i < borders_1.length)) return [3 /*break*/, 13];
                        border = borders_1[_i];
                        if (!(border.style !== 0 /* NONE */ && !isTransparent(border.color) && border.width > 0)) return [3 /*break*/, 11];
                        if (!(border.style === 2 /* DASHED */)) return [3 /*break*/, 5];
                        return [4 /*yield*/, this.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 2 /* DASHED */)];
                    case 4:
                        _a.sent();
                        return [3 /*break*/, 11];
                    case 5:
                        if (!(border.style === 3 /* DOTTED */)) return [3 /*break*/, 7];
                        return [4 /*yield*/, this.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 3 /* DOTTED */)];
                    case 6:
                        _a.sent();
                        return [3 /*break*/, 11];
                    case 7:
                        if (!(border.style === 4 /* DOUBLE */)) return [3 /*break*/, 9];
                        return [4 /*yield*/, this.renderDoubleBorder(border.color, border.width, side, paint.curves)];
                    case 8:
                        _a.sent();
                        return [3 /*break*/, 11];
                    case 9: return [4 /*yield*/, this.renderSolidBorder(border.color, side, paint.curves)];
                    case 10:
                        _a.sent();
                        _a.label = 11;
                    case 11:
                        side++;
                        _a.label = 12;
                    case 12:
                        _i++;
                        return [3 /*break*/, 3];
                    case 13: return [2 /*return*/];
                }
            });
        });
    };
    CanvasRenderer.prototype.renderDashedDottedBorder = function (color, width, side, curvePoints, style) {
        return __awaiter(this, void 0, void 0, function () {
            var strokePaths, boxPaths, startX, startY, endX, endY, length, dashLength, spaceLength, useLineDash, multiplier, numberOfDashes, minSpace, maxSpace, path1, path2, path1, path2;
            return __generator(this, function (_a) {
                this.ctx.save();
                strokePaths = parsePathForBorderStroke(curvePoints, side);
                boxPaths = parsePathForBorder(curvePoints, side);
                if (style === 2 /* DASHED */) {
                    this.path(boxPaths);
                    this.ctx.clip();
                }
                if (isBezierCurve(boxPaths[0])) {
                    startX = boxPaths[0].start.x;
                    startY = boxPaths[0].start.y;
                }
                else {
                    startX = boxPaths[0].x;
                    startY = boxPaths[0].y;
                }
                if (isBezierCurve(boxPaths[1])) {
                    endX = boxPaths[1].end.x;
                    endY = boxPaths[1].end.y;
                }
                else {
                    endX = boxPaths[1].x;
                    endY = boxPaths[1].y;
                }
                if (side === 0 || side === 2) {
                    length = Math.abs(startX - endX);
                }
                else {
                    length = Math.abs(startY - endY);
                }
                this.ctx.beginPath();
                if (style === 3 /* DOTTED */) {
                    this.formatPath(strokePaths);
                }
                else {
                    this.formatPath(boxPaths.slice(0, 2));
                }
                dashLength = width < 3 ? width * 3 : width * 2;
                spaceLength = width < 3 ? width * 2 : width;
                if (style === 3 /* DOTTED */) {
                    dashLength = width;
                    spaceLength = width;
                }
                useLineDash = true;
                if (length <= dashLength * 2) {
                    useLineDash = false;
                }
                else if (length <= dashLength * 2 + spaceLength) {
                    multiplier = length / (2 * dashLength + spaceLength);
                    dashLength *= multiplier;
                    spaceLength *= multiplier;
                }
                else {
                    numberOfDashes = Math.floor((length + spaceLength) / (dashLength + spaceLength));
                    minSpace = (length - numberOfDashes * dashLength) / (numberOfDashes - 1);
                    maxSpace = (length - (numberOfDashes + 1) * dashLength) / numberOfDashes;
                    spaceLength =
                        maxSpace <= 0 || Math.abs(spaceLength - minSpace) < Math.abs(spaceLength - maxSpace)
                            ? minSpace
                            : maxSpace;
                }
                if (useLineDash) {
                    if (style === 3 /* DOTTED */) {
                        this.ctx.setLineDash([0, dashLength + spaceLength]);
                    }
                    else {
                        this.ctx.setLineDash([dashLength, spaceLength]);
                    }
                }
                if (style === 3 /* DOTTED */) {
                    this.ctx.lineCap = 'round';
                    this.ctx.lineWidth = width;
                }
                else {
                    this.ctx.lineWidth = width * 2 + 1.1;
                }
                this.ctx.strokeStyle = asString(color);
                this.ctx.stroke();
                this.ctx.setLineDash([]);
                // dashed round edge gap
                if (style === 2 /* DASHED */) {
                    if (isBezierCurve(boxPaths[0])) {
                        path1 = boxPaths[3];
                        path2 = boxPaths[0];
                        this.ctx.beginPath();
                        this.formatPath([new Vector$1(path1.end.x, path1.end.y), new Vector$1(path2.start.x, path2.start.y)]);
                        this.ctx.stroke();
                    }
                    if (isBezierCurve(boxPaths[1])) {
                        path1 = boxPaths[1];
                        path2 = boxPaths[2];
                        this.ctx.beginPath();
                        this.formatPath([new Vector$1(path1.end.x, path1.end.y), new Vector$1(path2.start.x, path2.start.y)]);
                        this.ctx.stroke();
                    }
                }
                this.ctx.restore();
                return [2 /*return*/];
            });
        });
    };
    CanvasRenderer.prototype.render = function (element) {
        return __awaiter(this, void 0, void 0, function () {
            var stack;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (this.options.backgroundColor) {
                            this.ctx.fillStyle = asString(this.options.backgroundColor);
                            this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
                        }
                        stack = parseStackingContexts(element);
                        return [4 /*yield*/, this.renderStack(stack)];
                    case 1:
                        _a.sent();
                        this.applyEffects([]);
                        return [2 /*return*/, this.canvas];
                }
            });
        });
    };
    return CanvasRenderer;
}(Renderer));
var isTextInputElement = function (container) {
    if (container instanceof TextareaElementContainer) {
        return true;
    }
    else if (container instanceof SelectElementContainer) {
        return true;
    }
    else if (container instanceof InputElementContainer && container.type !== RADIO && container.type !== CHECKBOX) {
        return true;
    }
    return false;
};
var calculateBackgroundCurvedPaintingArea = function (clip, curves) {
    switch (clip) {
        case 0 /* BORDER_BOX */:
            return calculateBorderBoxPath(curves);
        case 2 /* CONTENT_BOX */:
            return calculateContentBoxPath(curves);
        case 1 /* PADDING_BOX */:
        default:
            return calculatePaddingBoxPath(curves);
    }
};
var canvasTextAlign = function (textAlign) {
    switch (textAlign) {
        case 1 /* CENTER */:
            return 'center';
        case 2 /* RIGHT */:
            return 'right';
        case 0 /* LEFT */:
        default:
            return 'left';
    }
};
// see https://github.com/niklasvh/html2canvas/pull/2645
var iOSBrokenFonts = ['-apple-system', 'system-ui'];
var fixIOSSystemFonts = function (fontFamilies) {
    return /iPhone OS 15_(0|1)/.test(window.navigator.userAgent)
        ? fontFamilies.filter(function (fontFamily) { return iOSBrokenFonts.indexOf(fontFamily) === -1; })
        : fontFamilies;
};

var ForeignObjectRenderer = /** @class */ (function (_super) {
    __extends(ForeignObjectRenderer, _super);
    function ForeignObjectRenderer(context, options) {
        var _this = _super.call(this, context, options) || this;
        _this.canvas = options.canvas ? options.canvas : document.createElement('canvas');
        _this.ctx = _this.canvas.getContext('2d');
        _this.options = options;
        _this.canvas.width = Math.floor(options.width * options.scale);
        _this.canvas.height = Math.floor(options.height * options.scale);
        _this.canvas.style.width = options.width + "px";
        _this.canvas.style.height = options.height + "px";
        _this.ctx.scale(_this.options.scale, _this.options.scale);
        _this.ctx.translate(-options.x, -options.y);
        _this.context.logger.debug("EXPERIMENTAL ForeignObject renderer initialized (" + options.width + "x" + options.height + " at " + options.x + "," + options.y + ") with scale " + options.scale);
        return _this;
    }
    ForeignObjectRenderer.prototype.render = function (element) {
        return __awaiter(this, void 0, void 0, function () {
            var svg, img;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        svg = createForeignObjectSVG(this.options.width * this.options.scale, this.options.height * this.options.scale, this.options.scale, this.options.scale, element);
                        return [4 /*yield*/, loadSerializedSVG(svg)];
                    case 1:
                        img = _a.sent();
                        if (this.options.backgroundColor) {
                            this.ctx.fillStyle = asString(this.options.backgroundColor);
                            this.ctx.fillRect(0, 0, this.options.width * this.options.scale, this.options.height * this.options.scale);
                        }
                        this.ctx.drawImage(img, -this.options.x * this.options.scale, -this.options.y * this.options.scale);
                        return [2 /*return*/, this.canvas];
                }
            });
        });
    };
    return ForeignObjectRenderer;
}(Renderer));
var loadSerializedSVG = function (svg) {
    return new Promise(function (resolve, reject) {
        var img = new Image();
        img.onload = function () {
            resolve(img);
        };
        img.onerror = reject;
        img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(new XMLSerializer().serializeToString(svg));
    });
};

var Logger = /** @class */ (function () {
    function Logger(_a) {
        var id = _a.id, enabled = _a.enabled;
        this.id = id;
        this.enabled = enabled;
        this.start = Date.now();
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    Logger.prototype.debug = function () {
        var args = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            args[_i] = arguments[_i];
        }
        if (this.enabled) {
            // eslint-disable-next-line no-console
            if (typeof window !== 'undefined' && window.console && typeof console.debug === 'function') {
                // eslint-disable-next-line no-console
                console.debug.apply(console, __spreadArray([this.id, this.getTime() + "ms"], args));
            }
            else {
                this.info.apply(this, args);
            }
        }
    };
    Logger.prototype.getTime = function () {
        return Date.now() - this.start;
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    Logger.prototype.info = function () {
        var args = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            args[_i] = arguments[_i];
        }
        if (this.enabled) {
            // eslint-disable-next-line no-console
            if (typeof window !== 'undefined' && window.console && typeof console.info === 'function') {
                // eslint-disable-next-line no-console
                console.info.apply(console, __spreadArray([this.id, this.getTime() + "ms"], args));
            }
        }
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    Logger.prototype.warn = function () {
        var args = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            args[_i] = arguments[_i];
        }
        if (this.enabled) {
            // eslint-disable-next-line no-console
            if (typeof window !== 'undefined' && window.console && typeof console.warn === 'function') {
                // eslint-disable-next-line no-console
                console.warn.apply(console, __spreadArray([this.id, this.getTime() + "ms"], args));
            }
            else {
                this.info.apply(this, args);
            }
        }
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    Logger.prototype.error = function () {
        var args = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            args[_i] = arguments[_i];
        }
        if (this.enabled) {
            // eslint-disable-next-line no-console
            if (typeof window !== 'undefined' && window.console && typeof console.error === 'function') {
                // eslint-disable-next-line no-console
                console.error.apply(console, __spreadArray([this.id, this.getTime() + "ms"], args));
            }
            else {
                this.info.apply(this, args);
            }
        }
    };
    Logger.instances = {};
    return Logger;
}());

var Context = /** @class */ (function () {
    function Context(options, windowBounds) {
        var _a;
        this.windowBounds = windowBounds;
        this.instanceName = "#" + Context.instanceCount++;
        this.logger = new Logger({ id: this.instanceName, enabled: options.logging });
        this.cache = (_a = options.cache) !== null && _a !== void 0 ? _a : new Cache(this, options);
    }
    Context.instanceCount = 1;
    return Context;
}());

var html2canvas = function (element, options) {
    if (options === void 0) { options = {}; }
    return renderElement(element, options);
};
if (typeof window !== 'undefined') {
    CacheStorage.setContext(window);
}
var renderElement = function (element, opts) { return __awaiter(void 0, void 0, void 0, function () {
    var ownerDocument, defaultView, resourceOptions, contextOptions, windowOptions, windowBounds, context, foreignObjectRendering, cloneOptions, documentCloner, clonedElement, container, _a, width, height, left, top, backgroundColor, renderOptions, canvas, renderer, root, renderer;
    var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
    return __generator(this, function (_u) {
        switch (_u.label) {
            case 0:
                if (!element || typeof element !== 'object') {
                    return [2 /*return*/, Promise.reject('Invalid element provided as first argument')];
                }
                ownerDocument = element.ownerDocument;
                if (!ownerDocument) {
                    throw new Error("Element is not attached to a Document");
                }
                defaultView = ownerDocument.defaultView;
                if (!defaultView) {
                    throw new Error("Document is not attached to a Window");
                }
                resourceOptions = {
                    allowTaint: (_b = opts.allowTaint) !== null && _b !== void 0 ? _b : false,
                    imageTimeout: (_c = opts.imageTimeout) !== null && _c !== void 0 ? _c : 15000,
                    proxy: opts.proxy,
                    useCORS: (_d = opts.useCORS) !== null && _d !== void 0 ? _d : false
                };
                contextOptions = __assign({ logging: (_e = opts.logging) !== null && _e !== void 0 ? _e : true, cache: opts.cache }, resourceOptions);
                windowOptions = {
                    windowWidth: (_f = opts.windowWidth) !== null && _f !== void 0 ? _f : defaultView.innerWidth,
                    windowHeight: (_g = opts.windowHeight) !== null && _g !== void 0 ? _g : defaultView.innerHeight,
                    scrollX: (_h = opts.scrollX) !== null && _h !== void 0 ? _h : defaultView.pageXOffset,
                    scrollY: (_j = opts.scrollY) !== null && _j !== void 0 ? _j : defaultView.pageYOffset
                };
                windowBounds = new Bounds(windowOptions.scrollX, windowOptions.scrollY, windowOptions.windowWidth, windowOptions.windowHeight);
                context = new Context(contextOptions, windowBounds);
                foreignObjectRendering = (_k = opts.foreignObjectRendering) !== null && _k !== void 0 ? _k : false;
                cloneOptions = {
                    allowTaint: (_l = opts.allowTaint) !== null && _l !== void 0 ? _l : false,
                    onclone: opts.onclone,
                    ignoreElements: opts.ignoreElements,
                    inlineImages: foreignObjectRendering,
                    copyStyles: foreignObjectRendering
                };
                context.logger.debug("Starting document clone with size " + windowBounds.width + "x" + windowBounds.height + " scrolled to " + -windowBounds.left + "," + -windowBounds.top);
                documentCloner = new DocumentCloner(context, element, cloneOptions);
                clonedElement = documentCloner.clonedReferenceElement;
                if (!clonedElement) {
                    return [2 /*return*/, Promise.reject("Unable to find element in cloned iframe")];
                }
                return [4 /*yield*/, documentCloner.toIFrame(ownerDocument, windowBounds)];
            case 1:
                container = _u.sent();
                _a = isBodyElement(clonedElement) || isHTMLElement(clonedElement)
                    ? parseDocumentSize(clonedElement.ownerDocument)
                    : parseBounds(context, clonedElement), width = _a.width, height = _a.height, left = _a.left, top = _a.top;
                backgroundColor = parseBackgroundColor(context, clonedElement, opts.backgroundColor);
                renderOptions = {
                    canvas: opts.canvas,
                    backgroundColor: backgroundColor,
                    scale: (_o = (_m = opts.scale) !== null && _m !== void 0 ? _m : defaultView.devicePixelRatio) !== null && _o !== void 0 ? _o : 1,
                    x: ((_p = opts.x) !== null && _p !== void 0 ? _p : 0) + left,
                    y: ((_q = opts.y) !== null && _q !== void 0 ? _q : 0) + top,
                    width: (_r = opts.width) !== null && _r !== void 0 ? _r : Math.ceil(width),
                    height: (_s = opts.height) !== null && _s !== void 0 ? _s : Math.ceil(height)
                };
                if (!foreignObjectRendering) return [3 /*break*/, 3];
                context.logger.debug("Document cloned, using foreign object rendering");
                renderer = new ForeignObjectRenderer(context, renderOptions);
                return [4 /*yield*/, renderer.render(clonedElement)];
            case 2:
                canvas = _u.sent();
                return [3 /*break*/, 5];
            case 3:
                context.logger.debug("Document cloned, element located at " + left + "," + top + " with size " + width + "x" + height + " using computed rendering");
                context.logger.debug("Starting DOM parsing");
                root = parseTree(context, clonedElement);
                if (backgroundColor === root.styles.backgroundColor) {
                    root.styles.backgroundColor = COLORS.TRANSPARENT;
                }
                context.logger.debug("Starting renderer for element at " + renderOptions.x + "," + renderOptions.y + " with size " + renderOptions.width + "x" + renderOptions.height);
                renderer = new CanvasRenderer(context, renderOptions);
                return [4 /*yield*/, renderer.render(root)];
            case 4:
                canvas = _u.sent();
                _u.label = 5;
            case 5:
                if ((_t = opts.removeContainer) !== null && _t !== void 0 ? _t : true) {
                    if (!DocumentCloner.destroy(container)) {
                        context.logger.error("Cannot detach cloned iframe as it is not in the DOM anymore");
                    }
                }
                context.logger.debug("Finished rendering");
                return [2 /*return*/, canvas];
        }
    });
}); };
var parseBackgroundColor = function (context, element, backgroundColorOverride) {
    var ownerDocument = element.ownerDocument;
    // http://www.w3.org/TR/css3-background/#special-backgrounds
    var documentBackgroundColor = ownerDocument.documentElement
        ? parseColor(context, getComputedStyle(ownerDocument.documentElement).backgroundColor)
        : COLORS.TRANSPARENT;
    var bodyBackgroundColor = ownerDocument.body
        ? parseColor(context, getComputedStyle(ownerDocument.body).backgroundColor)
        : COLORS.TRANSPARENT;
    var defaultBackgroundColor = typeof backgroundColorOverride === 'string'
        ? parseColor(context, backgroundColorOverride)
        : backgroundColorOverride === null
            ? COLORS.TRANSPARENT
            : 0xffffffff;
    return element === ownerDocument.documentElement
        ? isTransparent(documentBackgroundColor)
            ? isTransparent(bodyBackgroundColor)
                ? defaultBackgroundColor
                : bodyBackgroundColor
            : documentBackgroundColor
        : defaultBackgroundColor;
};

//failCallback will be called when the press is not long enough to considered a long press
function addContextMenuListener(elem, callback, failCallback = () => {}) {
    if (!elem || !callback) return;

    let timeout = null;
    const longPressTimer = 500;
    const MOVE_THRESHOLD = 3;
    let startX, startY;

    const touchStartHandler = (event) => {
        event.preventDefault();
        
        if (timeout) {
            clearTimeout(timeout);
            timeout = null;
        }
        // If more than one finger touches the screen, cancel the timeout
        if (event.touches.length > 1) return;

        const touch = event.touches[0];
        startX = touch.clientX;
        startY = touch.clientY;

        timeout = setTimeout(() => {
            event.clientX = touch.clientX;
            event.clientY = touch.clientY;
            callback(event);
            clearTimeout(timeout);
            timeout = null;
        }, longPressTimer);
    };

    const globalTouchStartHandler = (event) => {
        // Cancel timeout if multiple touches are detected anywhere on the screen
        if (event.touches.length > 1 && timeout) {
            clearTimeout(timeout);
            timeout = null;
        }
    };

    const touchMoveHandler = (event) => {
        if (!timeout) return;
        const touch = event.touches[0];
        const deltaX = Math.abs(touch.clientX - startX);
        const deltaY = Math.abs(touch.clientY - startY);

        if (deltaX > MOVE_THRESHOLD || deltaY > MOVE_THRESHOLD) {
            clearTimeout(timeout);
            timeout = null;
        }
    };

    const touchEndHandler = (event) => {
        event.preventDefault();
        if (timeout) {
            failCallback(event);
            clearTimeout(timeout);
            timeout = null;
        }
    };

    const contextMenuHandler = (event) => {
        callback(event);
        event.preventDefault();
        event.stopPropagation();
    };
    
    if (os.isIphoneSafari()) {
        elem.addEventListener('touchstart', touchStartHandler);
        elem.addEventListener('touchmove', touchMoveHandler);
        elem.addEventListener('touchend', touchEndHandler);
        window.addEventListener('touchstart', globalTouchStartHandler);
    } else {
        elem.addEventListener('contextmenu', contextMenuHandler);
    }

    return function removeContextMenuListener() {
        if (os.isIphoneSafari()) {
            elem.removeEventListener('touchstart', touchStartHandler);
            elem.removeEventListener('touchmove', touchMoveHandler);
            elem.removeEventListener('touchend', touchEndHandler);
            window.removeEventListener('touchstart', globalTouchStartHandler);
        } else {
            elem.removeEventListener('contextmenu', contextMenuHandler);
        }
    };
}

/** @private */
class Dot {

    constructor(parentElement, cfg = {}) {

        this._highlightClass = "viewer-ruler-dot-highlighted";

        this._x = 0;
        this._y = 0;

        this._dot = document.createElement('div');
        this._dot.className += this._dot.className ? ' viewer-ruler-dot' : 'viewer-ruler-dot';

        this._dotClickable = document.createElement('div');
        this._dotClickable.className += this._dotClickable.className ? ' viewer-ruler-dot-clickable' : 'viewer-ruler-dot-clickable';

        this._visible = cfg.visible !== false;
        this._culled = false;

        var dot = this._dot;
        var dotStyle = dot.style;
        dotStyle["border-radius"] = 25 + "px";
        dotStyle.border = "solid 2px white";
        dotStyle.background = "lightgreen";
        dotStyle.position = "absolute";
        dotStyle["z-index"] = cfg.zIndex === undefined ? "40000005" : cfg.zIndex ;
        dotStyle.width = 8 + "px";
        dotStyle.height = 8 + "px";
        dotStyle.visibility = this._visible ? "visible" : "hidden";
        dotStyle.top = 0 + "px";
        dotStyle.left = 0 + "px";
        dotStyle["box-shadow"] = "0 2px 5px 0 #182A3D;";
        dotStyle["opacity"] = 1.0;
        dotStyle["pointer-events"] = "none";
        if (cfg.onContextMenu) ;
        parentElement.appendChild(dot);

        var dotClickable = this._dotClickable;
        var dotClickableStyle = dotClickable.style;
        dotClickableStyle["border-radius"] = 35 + "px";
        dotClickableStyle.border = "solid 10px white";
        dotClickableStyle.position = "absolute";
        dotClickableStyle["z-index"] = cfg.zIndex === undefined ? "40000007" : (cfg.zIndex + 1);
        dotClickableStyle.width = 8 + "px";
        dotClickableStyle.height = 8 + "px";
        dotClickableStyle.visibility = "visible";
        dotClickableStyle.top = 0 + "px";
        dotClickableStyle.left = 0 + "px";
        dotClickableStyle["opacity"] = 0.0;
        dotClickableStyle["pointer-events"] = "none";
        if (cfg.onContextMenu) ;
        parentElement.appendChild(dotClickable);

        dotClickable.addEventListener('click', (event) => {
            parentElement.dispatchEvent(new MouseEvent('mouseover', event));
        });

        if (cfg.onMouseOver) {
            dotClickable.addEventListener('mouseover', (event) => {
                cfg.onMouseOver(event, this);
                parentElement.dispatchEvent(new MouseEvent('mouseover', event));
            });
        }

        if (cfg.onMouseLeave) {
            dotClickable.addEventListener('mouseleave', (event) => {
                cfg.onMouseLeave(event, this);
            });
        }

        if (cfg.onMouseWheel) {
            dotClickable.addEventListener('wheel', (event) => {
                cfg.onMouseWheel(event, this);
            });
        }

        if (cfg.onMouseDown) {
            dotClickable.addEventListener('mousedown', (event) => {
                cfg.onMouseDown(event, this);
            });
        }

        if (cfg.onMouseUp) {
            dotClickable.addEventListener('mouseup', (event) => {
                cfg.onMouseUp(event, this);
            });
        }

        if (cfg.onMouseMove) {
            dotClickable.addEventListener('mousemove', (event) => {
                cfg.onMouseMove(event, this);
            });
        }

        if (cfg.onTouchstart) {
            dotClickable.addEventListener('touchstart', (event) => {
                cfg.onTouchstart(event, this);
            });
        }

        if (cfg.onTouchmove) {
            dotClickable.addEventListener('touchmove', (event) => {
                cfg.onTouchmove(event, this);
            });
        }

        if (cfg.onTouchend) {
            dotClickable.addEventListener('touchend', (event) => {
                cfg.onTouchend(event, this);
            });
        }

        if (cfg.onContextMenu) {
            const contextMenuCallback = (event) => {
                cfg.onContextMenu(event, this);
            };
            addContextMenuListener(dotClickable, contextMenuCallback);
            
        }

        this.setPos(cfg.x || 0, cfg.y || 0);
        this.setFillColor(cfg.fillColor);
        this.setBorderColor(cfg.borderColor);
    }

    setPos(x, y) {
        this._x = x;
        this._y = y;
        var dotStyle = this._dot.style;
        dotStyle["left"] = (Math.round(x) - 6) + 'px';
        dotStyle["top"] = (Math.round(y) - 6) + 'px';

        var dotClickableStyle = this._dotClickable.style;
        dotClickableStyle["left"] = (Math.round(x) - 14) + 'px';
        dotClickableStyle["top"] = (Math.round(y) - 14) + 'px';
    }

    setFillColor(color) {
        this._dot.style.background = color || "lightgreen";
    }

    setBorderColor(color) {
        this._dot.style.border = "solid 2px" + (color || "black");
    }

    setOpacity(opacity) {
        this._dot.style.opacity = opacity;
    }

    _updateVisibility() {
        this._dot.style.visibility = this._dotClickable.style.visibility = this._visible && !this._culled ? "visible" : "hidden";
    }

    setVisible(visible) {
        if (this._visible === visible) {
            return;
        }
        this._visible = !!visible;
        this._updateVisibility();
    }

    setCulled(culled) {
        if (this._culled === culled) {
            return;
        }
        this._culled = !!culled;
        this._updateVisibility();
    }

    setClickable(clickable) {
        this._dotClickable.style["pointer-events"] = (clickable) ? "all" : "none";
    }

    setHighlighted(highlighted) {
        if (this._highlighted === highlighted) {
            return;
        }
        this._highlighted = !!highlighted;
        if (this._highlighted) {
            this._dot.classList.add(this._highlightClass);
        } else {
            this._dot.classList.remove(this._highlightClass);
        }
    }
    
    destroy() {
        this.setVisible(false);
        if (this._dot.parentElement) {
            this._dot.parentElement.removeChild(this._dot);
        }
        if (this._dotClickable.parentElement) {
            this._dotClickable.parentElement.removeChild(this._dotClickable);
        }
    }
}

/** @private */
class Label {

    constructor(parentElement, cfg = {}) {

        this._highlightClass = "viewer-ruler-label-highlighted";

        this._prefix = cfg.prefix || "";
        this._x = 0;
        this._y = 0;
        this._visible = true;
        this._culled = false;

        this._label = document.createElement('div');
        this._label.className += this._label.className ? ' viewer-ruler-label' : 'viewer-ruler-label';
        this._timeout = null;

        var label = this._label;
        var style = label.style;

        style["border-radius"] = 5 + "px";
        style.color = "white";
        style.padding = "4px";
        style.border = "solid 1px";
        style.background = "lightgreen";
        style.position = "absolute";
        style["z-index"] = cfg.zIndex === undefined ? "5000005" : cfg.zIndex;
        style.width = "auto";
        style.height = "auto";
        style.visibility = "visible";
        style.top = 0 + "px";
        style.left = 0 + "px";
        style["pointer-events"] = "all";
        style["opacity"] = 1.0;
        if (cfg.onContextMenu) ;
        label.innerText = "";

        parentElement.appendChild(label);

        this.setPos(cfg.x || 0, cfg.y || 0);
        this.setFillColor(cfg.fillColor);
        this.setBorderColor(cfg.fillColor);
        this.setText(cfg.text);

        if (cfg.onMouseOver) {
            label.addEventListener('mouseover', (event) => {
                cfg.onMouseOver(event, this);
                event.preventDefault();
            });
        }

        if (cfg.onMouseLeave) {
            label.addEventListener('mouseleave', (event) => {
                cfg.onMouseLeave(event, this);
                event.preventDefault();
            });
        }

        if (cfg.onMouseWheel) {
            label.addEventListener('wheel', (event) => {
                cfg.onMouseWheel(event, this);
            });
        }

        if (cfg.onMouseDown) {
            label.addEventListener('mousedown', (event) => {
                cfg.onMouseDown(event, this);
                event.stopPropagation();
            });
        }

        if (cfg.onMouseUp) {
            label.addEventListener('mouseup', (event) => {
                cfg.onMouseUp(event, this);
                event.stopPropagation();
            });
        }

        if (cfg.onMouseMove) {
            label.addEventListener('mousemove', (event) => {
                cfg.onMouseMove(event, this);
            });
        }

        if (cfg.onContextMenu) {
            const contextMenuCallback = (event) => {
                cfg.onContextMenu(event, this);
            };
            addContextMenuListener(label, contextMenuCallback);
            
        }
    }

    setPos(x, y) {
        this._x = x;
        this._y = y;
        var style = this._label.style;
        style["left"] = (Math.round(x) - 20) + 'px';
        style["top"] = (Math.round(y) - 12) + 'px';
    }

    setPosOnWire(x1, y1, x2, y2) {
        var x = x1 + ((x2 - x1) * 0.5);
        var y = y1 + ((y2 - y1) * 0.5);
        var style = this._label.style;
        style["left"] = (Math.round(x) - 20) + 'px';
        style["top"] = (Math.round(y) - 12) + 'px';
    }

    setPosBetweenWires(x1, y1, x2, y2, x3, y3) {
        var x = (x1 + x2 + x3) / 3;
        var y = (y1 + y2 + y3) / 3;
        var style = this._label.style;
        style["left"] = (Math.round(x) - 20) + 'px';
        style["top"] = (Math.round(y) - 12) + 'px';
    }

    setText(text) {
        this._label.innerHTML = this._prefix + (text || "");
    }

    setFillColor(color) {
        this._fillColor = color || "lightgreen";
        this._label.style.background =this._fillColor;
    }

    setBorderColor(color) {
        this._borderColor = color || "black";
        this._label.style.border = "solid 1px " + this._borderColor;
    }

    setOpacity(opacity) {
        this._label.style.opacity = opacity;
    }

    _updateVisibility() {
        this._label.style.visibility = this._visible && !this._culled ? "visible" : "hidden";
    }

    setVisible(visible) {
        if (this._visible === visible) {
            return;
        }
        this._visible = !!visible;
        this._updateVisibility();
    }

    setCulled(culled) {
        if (this._culled === culled) {
            return;
        }
        this._culled = !!culled;
        this._updateVisibility();
    }

    setHighlighted(highlighted) {
        if (this._highlighted === highlighted) {
            return;
        }
        this._highlighted = !!highlighted;
        if (this._highlighted) {
            this._label.classList.add(this._highlightClass);
        } else {
            this._label.classList.remove(this._highlightClass);
        }
    }

    setClickable(clickable) {
        this._label.style["pointer-events"] = (clickable) ? "all" : "none";
    }

    setPrefix(prefix) {
        if(this._prefix === prefix){
            return;
        }
        this._prefix = prefix;
    }

    destroy() {
        if (this._label.parentElement) {
            this._label.parentElement.removeChild(this._label);
        }
    }

    
}

/** @private */
class Wire {

    constructor(parentElement, cfg = {}) {

        this._color = cfg.color || "black";
        this._highlightClass = "viewer-ruler-wire-highlighted";

        this._wire = document.createElement('div');
        this._wire.className += this._wire.className ? ' viewer-ruler-wire' : 'viewer-ruler-wire';

        this._wireClickable = document.createElement('div');
        this._wireClickable.className += this._wireClickable.className ? ' viewer-ruler-wire-clickable' : 'viewer-ruler-wire-clickable';

        this._thickness = cfg.thickness || 1.0;
        this._thicknessClickable = cfg.thicknessClickable || 6.0;

        this._visible = true;
        this._culled = false;

        var wire = this._wire;
        var wireStyle = wire.style;

        wireStyle.border = "solid " + this._thickness + "px " + this._color;
        wireStyle.position = "absolute";
        wireStyle["z-index"] = cfg.zIndex === undefined ? "2000001" : cfg.zIndex;
        wireStyle.width = 0 + "px";
        wireStyle.height = 0 + "px";
        wireStyle.visibility = "visible";
        wireStyle.top = 0 + "px";
        wireStyle.left = 0 + "px";
        wireStyle['-webkit-transform-origin'] = "0 0";
        wireStyle['-moz-transform-origin'] = "0 0";
        wireStyle['-ms-transform-origin'] = "0 0";
        wireStyle['-o-transform-origin'] = "0 0";
        wireStyle['transform-origin'] = "0 0";
        wireStyle['-webkit-transform'] = 'rotate(0deg)';
        wireStyle['-moz-transform'] = 'rotate(0deg)';
        wireStyle['-ms-transform'] = 'rotate(0deg)';
        wireStyle['-o-transform'] = 'rotate(0deg)';
        wireStyle['transform'] = 'rotate(0deg)';
        wireStyle["opacity"] = 1.0;
        wireStyle["pointer-events"] = "none";
        if (cfg.onContextMenu) ;

        parentElement.appendChild(wire);

        var wireClickable = this._wireClickable;
        var wireClickableStyle = wireClickable.style;

        wireClickableStyle.border = "solid " + this._thicknessClickable + "px " + this._color;
        wireClickableStyle.position = "absolute";
        wireClickableStyle["z-index"] = cfg.zIndex === undefined ? "2000002" : (cfg.zIndex + 1);
        wireClickableStyle.width = 0 + "px";
        wireClickableStyle.height = 0 + "px";
        wireClickableStyle.visibility = "visible";
        wireClickableStyle.top = 0 + "px";
        wireClickableStyle.left = 0 + "px";
        // wireClickableStyle["pointer-events"] = "none";
        wireClickableStyle['-webkit-transform-origin'] = "0 0";
        wireClickableStyle['-moz-transform-origin'] = "0 0";
        wireClickableStyle['-ms-transform-origin'] = "0 0";
        wireClickableStyle['-o-transform-origin'] = "0 0";
        wireClickableStyle['transform-origin'] = "0 0";
        wireClickableStyle['-webkit-transform'] = 'rotate(0deg)';
        wireClickableStyle['-moz-transform'] = 'rotate(0deg)';
        wireClickableStyle['-ms-transform'] = 'rotate(0deg)';
        wireClickableStyle['-o-transform'] = 'rotate(0deg)';
        wireClickableStyle['transform'] = 'rotate(0deg)';
        wireClickableStyle["opacity"] = 0.0;
        wireClickableStyle["pointer-events"] = "none";
        if (cfg.onContextMenu) ;

        parentElement.appendChild(wireClickable);

        if (cfg.onMouseOver) {
            wireClickable.addEventListener('mouseover', (event) => {
                cfg.onMouseOver(event, this);
            });
        }

        if (cfg.onMouseLeave) {
            wireClickable.addEventListener('mouseleave', (event) => {
                cfg.onMouseLeave(event, this);
            });
        }

        if (cfg.onMouseWheel) {
            wireClickable.addEventListener('wheel', (event) => {
                cfg.onMouseWheel(event, this);
            });
        }

        if (cfg.onMouseDown) {
            wireClickable.addEventListener('mousedown', (event) => {
                cfg.onMouseDown(event, this);
            });
        }

        if (cfg.onMouseUp) {
            wireClickable.addEventListener('mouseup', (event) => {
                cfg.onMouseUp(event, this);
            });
        }

        if (cfg.onMouseMove) {
            wireClickable.addEventListener('mousemove', (event) => {
                cfg.onMouseMove(event, this);
            });
        }

        if (cfg.onContextMenu) {
            const contextMenuCallback = (event) => {
                cfg.onContextMenu(event, this);
            };
            addContextMenuListener(wireClickable, contextMenuCallback);
            
        }

        this._x1 = 0;
        this._y1 = 0;
        this._x2 = 0;
        this._y2 = 0;

        this._update();
    }

    get visible() {
        return this._wire.style.visibility === "visible";
    }

    _update() {

        var length = Math.abs(Math.sqrt((this._x1 - this._x2) * (this._x1 - this._x2) + (this._y1 - this._y2) * (this._y1 - this._y2)));
        var angle = Math.atan2(this._y2 - this._y1, this._x2 - this._x1) * 180.0 / Math.PI;

        var wireStyle = this._wire.style;
        wireStyle["width"] = Math.round(length) + 'px';
        wireStyle["left"] = Math.round(this._x1) + 'px';
        wireStyle["top"] = Math.round(this._y1) + 'px';
        wireStyle['-webkit-transform'] =
            wireStyle['-moz-transform'] =
            wireStyle['-ms-transform'] =
            wireStyle['-o-transform'] =
            wireStyle['transform'] = 'rotate(' + angle + 'deg) translate(-' + this._thickness + 'px, -' + this._thickness + 'px)';

        var wireClickableStyle = this._wireClickable.style;
        wireClickableStyle["width"] = Math.round(length) + 'px';
        wireClickableStyle["left"] = Math.round(this._x1) + 'px';
        wireClickableStyle["top"] = Math.round(this._y1) + 'px';
        wireClickableStyle['-webkit-transform'] =
            wireClickableStyle['-moz-transform'] =
            wireClickableStyle['-ms-transform'] =
            wireClickableStyle['-o-transform'] =
            wireClickableStyle['transform'] = 'rotate(' + angle + 'deg) translate(-' + this._thicknessClickable + 'px, -' + this._thicknessClickable + 'px)';
    }

    setStartAndEnd(x1, y1, x2, y2) {
        this._x1 = x1;
        this._y1 = y1;
        this._x2 = x2;
        this._y2 = y2;
        this._update();
    }

    setColor(color) {
        this._color = color || "black";
        this._wire.style.border = "solid " + this._thickness + "px " + this._color;
    }

    setOpacity(opacity) {
        this._wire.style.opacity = opacity;
    }

    _updateVisibility() {
        this._wire.style.visibility = this._wireClickable.style.visibility = this._visible && !this._culled ? "visible" : "hidden";
    }

    setVisible(visible) {
        if (this._visible === visible) {
            return;
        }
        this._visible = !!visible;
        this._updateVisibility();
    }

    setCulled(culled) {
        if (this._culled === culled) {
            return;
        }
        this._culled = !!culled;
        this._updateVisibility();
    }

    setClickable(clickable) {
        this._wireClickable.style["pointer-events"] = (clickable) ? "all" : "none";
    }

    setHighlighted(highlighted) {
        if (this._highlighted === highlighted) {
            return;
        }
        this._highlighted = !!highlighted;
        if (this._highlighted) {
            this._wire.classList.add(this._highlightClass);
        } else {
            this._wire.classList.remove(this._highlightClass);
        }
    }

    destroy(visible) {
        if (this._wire.parentElement) {
            this._wire.parentElement.removeChild(this._wire);
        }
        if (this._wireClickable.parentElement) {
            this._wireClickable.parentElement.removeChild(this._wireClickable);
        }
    }
}

const tmpVec2a = math.vec2();
const tmpVec2b = math.vec2();
const tmpVec2c = math.vec2();
const tmpVec2d = math.vec2();
const tmpVec3a$2 = math.vec3();
const tmpVec3b$2 = math.vec3();
const tmpVec4a = math.vec4();
const tmpVec4b = math.vec4();
const tmpVec4c = math.vec4();
const tmpVec4d = math.vec4();
const tmpMat4  = math.mat4();
({
    origin:    math.vec3(),
    direction: math.vec3()
});

function transformToNode(from, to, vec) {
    const fromRec = from.getBoundingClientRect();
    const toRec = to.getBoundingClientRect();
    vec[0] += fromRec.left - toRec.left;
    vec[1] += fromRec.top  - toRec.top;
}
const toClipSpace = (camera, worldPos, p) => {
    // The reason the projMatrix is explicitely fetched as below is a complex callback chain
    // leading from a boundary update to a viewMatrix and projMatrix updates, which change their values
    // in the mid of the toClipSpace, leading to incorrect values.
    // The fetch ensures that matrix values are stabilized before used to transform worldPos to p.
    // Better solution would be to ensure that no mutually-triggered callbacks
    // (like boundary => view/projMatrix in this situation) ever happen, so user code can rely
    // on a callback running in a stable context.
    tmpMat4.set(camera.viewMatrix);
    tmpMat4.set(camera.projMatrix);
    // to homogeneous coords
    p.set(worldPos);
    p[3] = 1;

    // to clip space
    math.mulMat4v4(camera.viewMatrix, p, p);
    math.mulMat4v4(camera.projMatrix, p, p);
};

const toCanvasSpace = (canvas, parentElement, ndc, p) => {
    p[0] = (1 + ndc[0]) * 0.5 * canvas.offsetWidth;
    p[1] = (1 - ndc[1]) * 0.5 * canvas.offsetHeight;
    transformToNode(canvas, parentElement, p);
};

const clipSegment = (scene, parentElement, start, end, canvasStart, canvasEnd) => {
    const camera = scene.camera;
    const canvas = scene.canvas.canvas;

    if (math.distVec3(start, end) < 0.001) {
        return false;
    }

    const delta = math.subVec3(end, start, tmpVec3a$2);
    let s_min = 0.0;
    let s_max = 1.0;

    for (let plane of scene._sectionPlanesState.sectionPlanes) {
        const endDot   = math.dotVec3(plane.dir, math.subVec3(plane.pos, end,   tmpVec3b$2));
        const startDot = math.dotVec3(plane.dir, math.subVec3(plane.pos, start, tmpVec3b$2));

        if ((startDot > 0) && (endDot > 0)) {
            return false;
        } else if ((startDot > 0) || (endDot > 0)) {
            const denom = math.dotVec3(plane.dir, delta);
            if (Math.abs(denom) >= 1e-6) {
                const ratio = math.dotVec3(plane.dir, tmpVec3b$2) / denom;
                if (startDot > 0) {
                    s_min = Math.max(s_min, ratio);
                } else {
                    s_max = Math.min(s_max, ratio);
                }
            }
        }
    }

    const p0 = tmpVec4a;
    const p1 = tmpVec4b;
    toClipSpace(camera, (s_min > 0) ? math.addVec3(start, math.mulVec3Scalar(delta, s_min, tmpVec3b$2), tmpVec3b$2) : start, p0);
    toClipSpace(camera, (s_max < 1) ? math.addVec3(start, math.mulVec3Scalar(delta, s_max, tmpVec3b$2), tmpVec3b$2) : end,   p1);

    const p0Behind = ((p0[2] / p0[3]) < -1) || (p0[3] < 0);
    const p1Behind = ((p1[2] / p1[3]) < -1) || (p1[3] < 0);
    if (p0Behind && p1Behind) {
        return false;
    }

    const t = (p0[3] + p0[2]) / ((p0[3] + p0[2]) - (p1[3] + p1[2]));

    if ((t > 0) && (t < 1)) { //p0Behind || p1Behind) {
        // Find the intersection of a segment with the near plane in clip space, if it exists."""
        // Calculate the interpolation factor t where the line segment crosses the near plane
        const delta = math.subVec4(p1, p0, tmpVec4c);
        math.mulVec4Scalar(delta, t, delta);
        math.addVec4(p0, delta, p0Behind ? p0 : p1);
    }

    // normalize clip space coords
    math.mulVec4Scalar(p0, 1.0 / p0[3]);
    math.mulVec4Scalar(p1, 1.0 / p1[3]);

    let t_min = 0.0;
    let t_max = 1.0;

    // If either point is outside the view frustum, clip the line segment

    for (let i = 0; i < 2; ++i) {
        const denom = p1[i] - p0[i];

        const l = (-p0[3] - p0[i]) / denom;
        const r = ( p0[3] - p0[i]) / denom;

        if (denom > 0) {
            t_min = Math.max(t_min, l);
            t_max = Math.min(t_max, r);
        } else {
            t_min = Math.max(t_min, r);
            t_max = Math.min(t_max, l);
        }
    }

    if (t_min >= t_max) {
        return false;
    }

    // Calculate the clipped start and end points
    const ndcDelta = math.subVec4(p1, p0, tmpVec4c);
    math.addVec4(p0, math.mulVec4Scalar(ndcDelta, t_max, tmpVec4d), p1);
    math.addVec4(p0, math.mulVec4Scalar(ndcDelta, t_min, tmpVec4d), p0);

    math.mulVec4Scalar(p0, 1 / p0[3]);
    math.mulVec4Scalar(p1, 1 / p1[3]);

    toCanvasSpace(canvas, parentElement, p0, canvasStart);
    toCanvasSpace(canvas, parentElement, p1, canvasEnd);

    return true;
};

class Dot3D extends Marker {
    constructor(scene, markerCfg, parentElement, cfg = {}) {
        const camera = scene.camera;

        super(scene, markerCfg);

        this.__visible = true;  // "__" to not interfere with Marker::_visible

        const handler = (cfgEvent, componentEvent) => {
            return event => {
                if (cfgEvent) {
                    cfgEvent(event);
                }
                this.fire(componentEvent, event, true);
            };
        };
        this._dot = new Dot(parentElement, {
            borderColor: cfg.borderColor,
            fillColor: cfg.fillColor,
            zIndex: cfg.zIndex,
            onMouseOver:   handler(cfg.onMouseOver,   "mouseover"),
            onMouseLeave:  handler(cfg.onMouseLeave,  "mouseleave"),
            onMouseWheel:  handler(cfg.onMouseWheel,  "wheel"),
            onMouseDown:   handler(cfg.onMouseDown,   "mousedown"),
            onMouseUp:     handler(cfg.onMouseUp,     "mouseup"),
            onMouseMove:   handler(cfg.onMouseMove,   "mousemove"),
            onTouchstart:  handler(cfg.onTouchstart,  "touchstart"),
            onTouchmove:   handler(cfg.onTouchmove,   "touchmove"),
            onTouchend:    handler(cfg.onTouchend,    "touchend"),
            onContextMenu: handler(cfg.onContextMenu, "contextmenu")
        });

        const toClipSpace = (worldPos, p) => {
            // to homogeneous coords
            p.set(worldPos);
            p[3] = 1;

            // to clip space
            math.mulMat4v4(camera.viewMatrix, p, p);
            math.mulMat4v4(camera.projMatrix, p, p);
        };

        const toCanvasSpace = ndc => {
            const canvas = scene.canvas.canvas;
            ndc[0] = (1 + ndc[0]) * 0.5 * canvas.offsetWidth;
            ndc[1] = (1 - ndc[1]) * 0.5 * canvas.offsetHeight;
            transformToNode(canvas, parentElement, ndc);
        };

        const updateDotPos = () => {
            if (! this.__visible) {
                return;
            }
            const p0 = tmpVec4c;
            toClipSpace(this.worldPos, p0);
            math.mulVec3Scalar(p0, 1.0 / p0[3]);

            const outsideFrustum = ((p0[3] < 0)
                                    ||
                                    (p0[0] < -1) || (p0[0] > 1)
                                    ||
                                    (p0[1] < -1) || (p0[1] > 1)
                                    ||
                                    (p0[2] < -1) || (p0[2] > 1));
            const culled = outsideFrustum || scene._sectionPlanesState.sectionPlanes.some(
                plane => (math.dotVec3(plane.dir, math.subVec3(plane.pos, this.worldPos, tmpVec3a$2)) > 0));

            this._dot.setCulled(culled);
            if (!culled) {
                toCanvasSpace(p0);
                this._dot.setPos(p0[0], p0[1]);
            }
        };

        this.on("worldPos", updateDotPos);

        const onViewMatrix = camera.on("viewMatrix", updateDotPos);
        const onProjMatrix = camera.on("projMatrix", updateDotPos);
        const onCanvasBnd  = scene.canvas.on("boundary", updateDotPos);
        const planesUpdate = scene.on("sectionPlaneUpdated", updateDotPos);

        this._updatePosition = updateDotPos;

        this._cleanup = () => {
            camera.off(onViewMatrix);
            camera.off(onProjMatrix);
            scene.canvas.off(onCanvasBnd);
            scene.off(planesUpdate);
            this._dot.destroy();
        };
    }

    setClickable(value) {
        this._dot.setClickable(value);
    }

    setFillColor(value) {
        this._dot.setFillColor(value);
    }

    setBorderColor(value) {
        this._dot.setBorderColor(value);
    }

    setHighlighted(value) {
        this._dot.setHighlighted(value);
    }

    setOpacity(value) {
        this._dot.setOpacity(value);
    }

    setVisible(value) {
        if (this.__visible != value) {
            this.__visible = value;
            this._updatePosition();
            this._dot.setVisible(value);
        }
    }

    destroy() {
        this._cleanup();
        super.destroy();
    }

}

class Label3D {
    constructor(scene, parentElement, cfg) {
        const camera = scene.camera;

        this._label = new Label(parentElement, cfg);
        this._start = math.vec3();
        this._mid   = math.vec3();
        this._end   = math.vec3();
        this._yOff  = 0;
        this.__visible = true;

        this._updatePos = () => {
            toClipSpace(camera, this._start, tmpVec4a);
            math.mulVec4Scalar(tmpVec4a, 1.0 / tmpVec4a[3]);
            toCanvasSpace(scene.canvas.canvas, parentElement, tmpVec4a, tmpVec2a);
            this._label.setPos(tmpVec2a[0], tmpVec2a[1]);
        };

        const setPosOnWire = (p0, p1, yOff) => {
            p0[0] += p1[0];
            p0[1] += p1[1];
            math.mulVec2Scalar(p0, .5);
            this._label.setPos(p0[0], p0[1] + yOff);
        };

        this._updatePosBetween = () => {
            const visibleA = clipSegment(scene, parentElement, this._start, this._mid, tmpVec2a, tmpVec2b);
            const visibleB = clipSegment(scene, parentElement, this._end,   this._mid, tmpVec2c, tmpVec2d);
            this._label.setCulled(! (visibleA || visibleB));
            if (visibleA && visibleB) {
                tmpVec2b[0] += tmpVec2d[0];
                tmpVec2b[1] += tmpVec2d[1];
                math.mulVec2Scalar(tmpVec2b, .5);

                tmpVec2b[0] += tmpVec2a[0] + tmpVec2c[0];
                tmpVec2b[1] += tmpVec2a[1] + tmpVec2c[1];
                math.mulVec2Scalar(tmpVec2b, 1/3);
                this._label.setPos(tmpVec2b[0], tmpVec2b[1]);
            } else if (visibleA) {
                setPosOnWire(tmpVec2a, tmpVec2b, 0);
            } else if (visibleB) {
                setPosOnWire(tmpVec2c, tmpVec2d, 0);
            }
        };

        this._updatePosOnWire = () => {
            const visible = (clipSegment(scene, parentElement, this._start, this._end, tmpVec2a, tmpVec2b)
                             &&
                             (math.distVec2(tmpVec2a, tmpVec2b) >= this._labelMinAxisLength));
            this._label.setCulled(!visible);
            if (visible) {
                setPosOnWire(tmpVec2a, tmpVec2b, this._yOff);
            }
        };

        let posUpdate = () => { };
        this._setUpdatePositions = (_posUpdate) => {
            posUpdate = _posUpdate;
            this._updatePositions();
        };
        this._updatePositions = () => {
            if (this.__visible) {
                posUpdate();
            }
        };

        const onViewMatrix = camera.on("viewMatrix", this._updatePositions);
        const onProjMatrix = camera.on("projMatrix", this._updatePositions);
        const onCanvasBnd  = scene.canvas.on("boundary", this._updatePositions);
        const planesUpdate = scene.on("sectionPlaneUpdated", this._updatePositions);

        this._cleanup = () => {
            camera.off(onViewMatrix);
            camera.off(onProjMatrix);
            scene.canvas.off(onCanvasBnd);
            scene.off(planesUpdate);
            this._label.destroy();
        };
    }

    setPos(p0) {
        this._start.set(p0);
        this._setUpdatePositions(this._updatePos);
    }

    setPosOnWire(p0, p1, yOff, labelMinAxisLength) {
        this._start.set(p0);
        this._end.set(p1);
        this._yOff = yOff;
        this._labelMinAxisLength = labelMinAxisLength;
        this._setUpdatePositions(this._updatePosOnWire);
    }

    setPosBetween(p0, p1, p2) {
        this._start.set(p0);
        this._mid.set(p1);
        this._end.set(p2);
        this._setUpdatePositions(this._updatePosBetween);
    }

    setFillColor(value) {
        this._label.setFillColor(value);
    }

    setHighlighted(value) {
        this._label.setHighlighted(value);
    }

    setText(value) {
        this._label.setText(value);
    }

    setClickable(value) {
        this._label.setClickable(value);
    }

    setVisible(value) {
        if (this.__visible != value) {
            this.__visible = value;
            this._updatePositions();
            this._label.setVisible(value);
        }
    }

    destroy() {
        this._cleanup();
    }

}

class Wire3D {
    constructor(scene, parentElement, cfg) {
        const camera = scene.camera;

        this._wire  = new Wire(parentElement, cfg);
        this._start = math.vec3();
        this._end   = math.vec3();
        this.__visible = true;

        this._updatePositions = () => {
            if (! this.__visible) {
                return;
            }
            const visible = clipSegment(scene, parentElement, this._start, this._end, tmpVec2a, tmpVec2b);
            this._wire.setCulled(! visible);
            if (visible) {
                this._wire.setStartAndEnd(tmpVec2a[0], tmpVec2a[1], tmpVec2b[0], tmpVec2b[1]);
            }
        };

        const onViewMatrix = camera.on("viewMatrix", this._updatePositions);
        const onProjMatrix = camera.on("projMatrix", this._updatePositions);
        const onCanvasBnd  = scene.canvas.on("boundary", this._updatePositions);
        const planesUpdate = scene.on("sectionPlaneUpdated", this._updatePositions);

        this._cleanup = () => {
            camera.off(onViewMatrix);
            camera.off(onProjMatrix);
            scene.canvas.off(onCanvasBnd);
            scene.off(planesUpdate);
            this._wire.destroy();
        };
    }

    setEnds(start, end) {
        this._start.set(start);
        this._end.set(end);
        this._updatePositions();
    }

    setClickable(value) {
        this._wire.setClickable(value);
    }

    setColor(value) {
        this._wire.setColor(value);
    }

    setHighlighted(value) {
        this._wire.setHighlighted(value);
    }

    setOpacity(value) {
        this._wire.setOpacity(value);
    }

    setVisible(value) {
        if (this.__visible != value) {
            this.__visible = value;
            this._updatePositions();
            this._wire.setVisible(value);
        }
    }

    destroy() {
        this._cleanup();
    }

}

/**
 * The 3D Viewer at the heart of the xeokit SDK.
 *
 * * A Viewer wraps a single {@link Scene}
 * * Add {@link Plugin}s to a Viewer to extend its functionality.
 * * {@link Viewer#metaScene} holds metadata about models in the
 * Viewer's {@link MetaScene}.
 * * Use {@link Viewer#cameraFlight} to fly or jump the {@link Scene}'s
 * {@link Camera} to target positions, boundaries or {@link Entity}s.
 *
 * @public
 */
class Viewer {

    /**
     * @constructor
     * @param {Object} cfg Viewer configuration.
     * @param {String} [cfg.id] Optional ID for this Viewer, defaults to the ID of {@link Viewer#scene}, which xeokit automatically generates.
     * @param {String} [cfg.canvasId]  ID of an existing HTML canvas for the {@link Viewer#scene} - either this or canvasElement is mandatory. When both values are given, the element reference is always preferred to the ID.
     * @param {HTMLCanvasElement} [cfg.canvasElement] Reference of an existing HTML canvas for the {@link Viewer#scene} - either this or canvasId is mandatory. When both values are given, the element reference is always preferred to the ID.
     * @param {HTMLElement} [cfg.keyboardEventsElement] Optional reference to HTML element on which key events should be handled. Defaults to the HTML Document.
     * @param {String} [cfg.spinnerElementId]  ID of existing HTML element to show the {@link Spinner} - internally creates a default element automatically if this is omitted.
     * @param {Number} [cfg.passes=1] The number of times the {@link Viewer#scene} renders per frame.
     * @param {Boolean} [cfg.clearEachPass=false] When doing multiple passes per frame, specifies if to clear the canvas before each pass (true) or just before the first pass (false).
     * @param {Boolean} [cfg.preserveDrawingBuffer=true]  Whether or not to preserve the WebGL drawing buffer. This needs to be ````true```` for {@link Viewer#getSnapshot} to work.
     * @param {Boolean} [cfg.transparent=true]  Whether or not the canvas is transparent.
     * @param {Boolean} [cfg.premultipliedAlpha=false]  Whether or not you want alpha composition with premultiplied alpha. Highlighting and selection works best when this is ````false````.
     * @param {Boolean} [cfg.gammaInput=true]  When true, expects that all textures and colors are premultiplied gamma.
     * @param {Boolean} [cfg.gammaOutput=false]  Whether or not to render with pre-multiplied gama.
     * @param {Number} [cfg.gammaFactor=2.2] The gamma factor to use when rendering with pre-multiplied gamma.
     * @param {Number[]} [cfg.backgroundColor=[1,1,1]] Sets the canvas background color to use when ````transparent```` is false.
     * @param {Boolean} [cfg.backgroundColorFromAmbientLight=true] When ````transparent```` is false, set this ````true````
     * to derive the canvas background color from {@link AmbientLight#color}, or ````false```` to set the canvas background to ````backgroundColor````.
     * @param {String} [cfg.units="meters"] The measurement unit type. Accepted values are ````"meters"````, ````"metres"````, , ````"centimeters"````, ````"centimetres"````, ````"millimeters"````,  ````"millimetres"````, ````"yards"````, ````"feet"```` and ````"inches"````.
     * @param {Number} [cfg.scale=1] The number of Real-space units in each World-space coordinate system unit.
     * @param {Number[]} [cfg.origin=[0,0,0]] The Real-space 3D origin, in current measurement units, at which the World-space coordinate origin ````[0,0,0]```` sits.
     * @param {Boolean} [cfg.saoEnabled=false] Whether to enable Scalable Ambient Obscurance (SAO) effect. See {@link SAO} for more info.
     * @param {Boolean} [cfg.antialias=true] Whether to enable anti-aliasing.
     * @throws {String} Throws an exception when both canvasId or canvasElement are missing or they aren't pointing to a valid HTMLCanvasElement.
     * @param {Boolean} [cfg.alphaDepthMask=true] Whether writing into the depth buffer is enabled or disabled when rendering transparent objects.
     * @param {Boolean} [cfg.entityOffsetsEnabled=false] Whether to enable {@link Entity#offset}. For best performance, only set this ````true```` when you need to use {@link Entity#offset}.
     * @param {Boolean} [cfg.readableGeometryEnabled=false] Whether to enable full-precision accuracy when surface picking with {@link Scene#pick}. Note that when ````true````, this configuration will increase the amount of browser memory used by the Viewer. The ````pickSurfacePrecision```` option for ````Scene#pick```` only works if this is set ````true````.
     * @param {Boolean} [cfg.logarithmicDepthBufferEnabled=false] Whether to enable logarithmic depth buffer. When this is true,
     * you can set huge values for {@link Perspective#far} and {@link Ortho#far}, to push the far clipping plane back so
     * that it does not clip huge models.
     * @param {Boolean} [cfg.colorTextureEnabled=true] Whether to enable base color texture rendering.
     * @param {Boolean} [cfg.pbrEnabled=false] Whether to enable physically-based rendering.
     * @param {LocaleService} [cfg.localeService=null] Optional locale-based translation service.
     * @param {Boolean} [cfg.dtxEnabled=false] Whether to enable data texture-based (DTX) scene representation within the Viewer. When this is true, the Viewer will use data textures to
     * store geometry on the GPU for triangle meshes that don't have textures. This gives a much lower memory footprint for these types of model element. This mode may not perform well on low-end GPUs that are optimized
     * to use textures to hold geometry data. Works great on most medium/high-end GPUs found in desktop computers, including the nVIDIA and Intel HD chipsets. Set this false to use the default vertex buffer object (VBO)
     * mode for storing geometry, which is the standard technique used in most graphics engines, and will work adequately on most low-end GPUs.
     * @param {Number} [cfg.markerZOffset=-0.001] The Z value of offset for Marker's OcclusionTester. The closest the value is to 0.000 the more precise OcclusionTester will be, but at the same time the less precise it will behave for Markers that are located exactly on the Surface.
     * @param {number} [cfg.numCachedSectionPlanes=0] Enhances the efficiency of SectionPlane creation by proactively allocating Viewer resources for a specified quantity
     * of SectionPlanes. Introducing this parameter streamlines the initial creation speed of SectionPlanes, particularly up to the designated quantity. This parameter internally
     * configures renderer logic for the specified number of SectionPlanes, eliminating the need for setting up logic with each SectionPlane creation and thereby enhancing
     * responsiveness. It is important to consider that each SectionPlane impacts rendering performance, so it is recommended to set this value to a quantity that aligns with
     * your expected usage.
     */
    constructor(cfg) {

        /**
         * The Viewer's current language setting.
         * @property language
         * @deprecated
         * @type {String}
         */
        this.language = "en";

        /**
         * The viewer's locale service.
         *
         * This is configured via the Viewer's constructor.
         *
         * By default, this service will be an instance of {@link LocaleService}, which will just return
         * null translations for all given strings and phrases.
         *
         * @property localeService
         * @type {LocaleService}
         * @since 2.0
         */
        this.localeService = cfg.localeService || new LocaleService();

        /**
         * The Viewer's {@link Scene}.
         * @property scene
         * @type {Scene}
         */
        this.scene = new Scene(this, {
            canvasId: cfg.canvasId,
            canvasElement: cfg.canvasElement,
            keyboardEventsElement: cfg.keyboardEventsElement,
            contextAttr: {
                preserveDrawingBuffer: cfg.preserveDrawingBuffer !== false,
                premultipliedAlpha: (!!cfg.premultipliedAlpha),
                antialias: (cfg.antialias !== false)
            },
            spinnerElementId: cfg.spinnerElementId,
            transparent: (cfg.transparent !== false),
            gammaInput: true,
            gammaOutput: false,
            backgroundColor: cfg.backgroundColor,
            backgroundColorFromAmbientLight: cfg.backgroundColorFromAmbientLight,
            ticksPerRender: 1,
            ticksPerOcclusionTest: 20,
            units: cfg.units,
            scale: cfg.scale,
            origin: cfg.origin,
            saoEnabled: cfg.saoEnabled,
            alphaDepthMask: (cfg.alphaDepthMask !== false),
            entityOffsetsEnabled: (!!cfg.entityOffsetsEnabled),
            readableGeometryEnabled: (!!cfg.readableGeometryEnabled),
            logarithmicDepthBufferEnabled: (!!cfg.logarithmicDepthBufferEnabled),
            pbrEnabled: (!!cfg.pbrEnabled),
            colorTextureEnabled: (cfg.colorTextureEnabled !== false),
            dtxEnabled: (!!cfg.dtxEnabled),
            markerZOffset: cfg.markerZOffset,
            numCachedSectionPlanes: cfg.numCachedSectionPlanes
        });

        /**
         * Metadata about the {@link Scene} and the models and objects within it.
         * @property metaScene
         * @type {MetaScene}
         * @readonly
         */
        this.metaScene = new MetaScene(this, this.scene);

        /**
         * The Viewer's ID.
         * @property id
         *
         * @type {String|Number}
         */
        this.id = cfg.id || this.scene.id;

        /**
         * The Viewer's {@link Camera}. This is also found on {@link Scene#camera}.
         * @property camera
         * @type {Camera}
         */
        this.camera = this.scene.camera;

        /**
         * The Viewer's {@link CameraFlightAnimation}, which
         * is used to fly the {@link Scene}'s {@link Camera} to given targets.
         * @property cameraFlight
         * @type {CameraFlightAnimation}
         */
        this.cameraFlight = new CameraFlightAnimation(this.scene, {
            duration: 0.5
        });

        /**
         * The Viewer's {@link CameraControl}, which
         * controls the {@link Scene}'s {@link Camera} with mouse,  touch and keyboard input.
         * @property cameraControl
         * @type {CameraControl}
         */
        this.cameraControl = new CameraControl(this.scene, {
            // panToPointer: true,
            doublePickFlyTo: true
        });

        this._plugins = [];

        /**
         * Subscriptions to events sent with {@link fire}.
         * @private
         */
        this._eventSubs = {};
    }

    /**
     * Returns the capabilities of this Viewer.
     *
     * @returns {{astcSupported: boolean, etc1Supported: boolean, pvrtcSupported: boolean, etc2Supported: boolean, dxtSupported: boolean, bptcSupported: boolean}}
     */
    get capabilities() {
        return this.scene.capabilities;
    }

    /**
     * Subscribes to an event fired at this Viewer.
     *
     * @param {String} event The event
     * @param {Function} callback Callback fired on the event
     */
    on(event, callback) {
        let subs = this._eventSubs[event];
        if (!subs) {
            subs = [];
            this._eventSubs[event] = subs;
        }
        subs.push(callback);
    }

    /**
     * Fires an event at this Viewer.
     *
     * @param {String} event Event name
     * @param {Object} value Event parameters
     */
    fire(event, value) {
        const subs = this._eventSubs[event];
        if (subs) {
            for (let i = 0, len = subs.length; i < len; i++) {
                subs[i](value);
            }
        }
    }

    /**
     * Unsubscribes from an event fired at this Viewer.
     * @param event
     */
    off(event) { // TODO

    }

    /**
     * Logs a message to the JavaScript developer console, prefixed with the ID of this Viewer.
     *
     * @param {String} msg The message
     */
    log(msg) {
        console.log(`[xeokit viewer ${this.id}]: ${msg}`);
    }

    /**
     * Logs an error message to the JavaScript developer console, prefixed with the ID of this Viewer.
     *
     * @param {String} msg The error message
     */
    error(msg) {
        console.error(`[xeokit viewer ${this.id}]: ${msg}`);
    }

    /**
     * Installs a Plugin.
     *
     * @private
     */
    addPlugin(plugin) {
        this._plugins.push(plugin);
    }

    /**
     * Uninstalls a Plugin, clearing content from it first.
     *
     * @private
     */
    removePlugin(plugin) {
        for (let i = 0, len = this._plugins.length; i < len; i++) {
            const p = this._plugins[i];
            if (p === plugin) {
                if (p.clear) {
                    p.clear();
                }
                this._plugins.splice(i, 1);
                return;
            }
        }
    }

    /**
     * Sends a message to installed Plugins.
     *
     * The message can optionally be accompanied by a value.
     * @private
     */
    sendToPlugins(name, value) {
        for (let i = 0, len = this._plugins.length; i < len; i++) {
            const p = this._plugins[i];
            if (p.send) {
                p.send(name, value);
            }
        }
    }

    /**
     * @private
     * @deprecated
     */
    clear() {
        throw "Viewer#clear() no longer implemented - use '#sendToPlugins(\"clear\") instead";
    }

    /**
     * @private
     * @deprecated
     */
    resetView() {
        throw "Viewer#resetView() no longer implemented - use CameraMemento & ObjectsMemento classes instead";
    }

    /**
     * Enter snapshot mode.
     *
     * Switches rendering to a hidden snapshot canvas.
     *
     * Exit snapshot mode using {@link Viewer#endSnapshot}.
     */
    beginSnapshot() {
        if (this._snapshotBegun) {
            return;
        }
        this.scene._renderer.beginSnapshot();
        this._snapshotBegun = true;
    }

    /**
     * Gets a snapshot of this Viewer's {@link Scene} as a Base64-encoded image.
     *
     * #### Usage:
     *
     * ````javascript
     * const imageData = viewer.getSnapshot({
     *    width: 500,
     *    height: 500,
     *    format: "png"
     * });
     * ````
     * @param {*} [params] Capture options.
     * @param {Number} [params.width] Desired width of result in pixels - defaults to width of canvas.
     * @param {Number} [params.height] Desired height of result in pixels - defaults to height of canvas.
     * @param {String} [params.format="jpeg"] Desired format; "jpeg", "png" or "bmp".
     * @param {Boolean} [params.includeGizmos=false] When true, will include gizmos like {@link SectionPlane} in the snapshot.
     * @returns {String} String-encoded image data URI.
     */
    getSnapshot(params = {}) {

        const needFinishSnapshot = (!this._snapshotBegun);
        const resize = (params.width !== undefined && params.height !== undefined);
        const canvas = this.scene.canvas.canvas;
        const saveWidth = canvas.clientWidth;
        const saveHeight = canvas.clientHeight;
        const width = params.width ? Math.floor(params.width) : canvas.width;
        const height = params.height ? Math.floor(params.height) : canvas.height;

        if (resize) {
            canvas.width = width;
            canvas.height = height;
        }

        if (!this._snapshotBegun) {
            this.beginSnapshot({
                width,
                height
            });
        }

        if (!params.includeGizmos) {
            this.sendToPlugins("snapshotStarting"); // Tells plugins to hide things that shouldn't be in snapshot
        }

        // firing "rendering" is necessary to trigger DTX{Lines,Triangles}Layer::_uploadDeferredFlags
        this.scene.fire("rendering", { }, true);

        this.scene._renderer.renderSnapshot();

        const imageDataURI = this.scene._renderer.readSnapshot(params);

        if (resize) {
            canvas.width = saveWidth;
            canvas.height = saveHeight;

            this.scene.glRedraw();
        }

        if (!params.includeGizmos) {
            this.sendToPlugins("snapshotFinished");
        }

        if (needFinishSnapshot) {
            this.endSnapshot();
        }

        return imageDataURI;
    }

    /**
     * Gets a snapshot of this Viewer's {@link Scene} as a Base64-encoded image which includes
     * the HTML elements created by various plugins.
     *
     * The snapshot image is composed of an image of the viewer canvas, overlaid with an image
     * of the HTML container element belonging to each installed Viewer plugin. Each container
     * element is only rendered once, so it's OK for plugins to share the same container.
     *
     * #### Usage:
     *
     * ````javascript
     * viewer.getSnapshotWithPlugins({
     *    width: 500,
     *    height: 500,
     *    format: "png"
     * }).then((imageData)=>{
     *
     * });
     * ````
     * @param {*} [params] Capture options.
     * @param {Number} [params.width] Desired width of result in pixels - defaults to width of canvas.
     * @param {Number} [params.height] Desired height of result in pixels - defaults to height of canvas.
     * @param {String} [params.format="jpeg"] Desired format; "jpeg", "png" or "bmp".
     * @param {Boolean} [params.includeGizmos=false] When true, will include gizmos like {@link SectionPlane} in the snapshot.
     * @returns {Promise} Promise which returns a string-encoded image data URI.
     */
    async getSnapshotWithPlugins(params = {}) {

        // We use gl.readPixels to get the WebGL canvas snapshot in a new
        // HTMLCanvas element, scaled to the target snapshot size, then
        // use html2canvas to render each plugin's container element into
        // that HTMLCanvas. Finally, we save the HTMLCanvas to a bitmap.

        // We don't rely on html2canvas to up-scale our WebGL canvas
        // when we want a higher-resolution snapshot, which would cause
        // blurring. Instead, we manage the scale and redraw of the WebGL
        // canvas ourselves, in order to allow the Viewer to render the
        // right amount of pixels, for a sharper image.


        const needFinishSnapshot = (!this._snapshotBegun);
        const resize = (params.width !== undefined && params.height !== undefined);
        const canvas = this.scene.canvas.canvas;
        const saveWidth = canvas.clientWidth;
        const saveHeight = canvas.clientHeight;
        const snapshotWidth = params.width ? Math.floor(params.width) : canvas.width;
        const snapshotHeight = params.height ? Math.floor(params.height) : canvas.height;

        if (resize) {
            canvas.width = snapshotWidth;
            canvas.height = snapshotHeight;
        }

        if (!this._snapshotBegun) {
            this.beginSnapshot();
        }

        if (!params.includeGizmos) {
            this.sendToPlugins("snapshotStarting"); // Tells plugins to hide things that shouldn't be in snapshot
        }

        this.scene._renderer.renderSnapshot();

        const snapshotCanvas = this.scene._renderer.readSnapshotAsCanvas();

        if (resize) {
            canvas.width = saveWidth;
            canvas.height = saveHeight;
            this.scene.glRedraw();
        }

        const pluginToCapture = {};
        const pluginContainerElements = [];

        for (let i = 0, len = this._plugins.length; i < len; i++) { // Find plugin container elements
            const plugin = this._plugins[i];
            if (plugin.getContainerElement) {
                const containerElement = plugin.getContainerElement();
                if (containerElement !== document.body) {
                    if (!pluginToCapture[containerElement.id]) {
                        pluginToCapture[containerElement.id] = true;
                        pluginContainerElements.push(containerElement);
                    }
                }
            }
        }

        // Added to fix label's text offset in an html2canvas capture (See XEOK-151)
        // based on https://github.com/niklasvh/html2canvas/issues/2775#issuecomment-1316356991
        const style = document.createElement('style');
        document.head.appendChild(style);
        style.sheet?.insertRule('body > div:last-child img { display: inline-block; }');

        for (let i = 0, len = pluginContainerElements.length; i < len; i++) {
            const containerElement = pluginContainerElements[i];
            await html2canvas(containerElement, {
                canvas: snapshotCanvas,
                backgroundColor: null,
                scale: snapshotCanvas.width / containerElement.clientWidth
            });
            // Reverts translation and scaling applied to the snapshotCanvas's context
            // by the html2canvas call (inside the ForeignObjectRenderer's constructor)
            // (implemented to compensate XCD-153 issue)
            snapshotCanvas.getContext("2d").resetTransform();
        }

        style.remove();

        if (!params.includeGizmos) {
            this.sendToPlugins("snapshotFinished");
        }
        if (needFinishSnapshot) {
            this.endSnapshot();
        }
        let format = params.format || "png";
        if (format !== "jpeg" && format !== "png" && format !== "bmp") {
            console.error("Unsupported image format: '" + format + "' - supported types are 'jpeg', 'bmp' and 'png' - defaulting to 'png'");
            format = "png";
        }
        if (!params.includeGizmos) {
            this.sendToPlugins("snapshotFinished");
        }
        if (needFinishSnapshot) {
            this.endSnapshot();
        }
        return snapshotCanvas.toDataURL(`image/${format}`);
    }

    /**
     * Exits snapshot mode.
     *
     * Switches rendering back to the main canvas.
     *
     */
    endSnapshot() {
        if (!this._snapshotBegun) {
            return;
        }
        this.scene._renderer.endSnapshot();
        this.scene._renderer.render({force: true});
        this._snapshotBegun = false;
    }

    /** Destroys this Viewer.
     */
    destroy() {
        const plugins = this._plugins.slice(); // Array will modify as we delete plugins
        for (let i = 0, len = plugins.length; i < len; i++) {
            const plugin = plugins[i];
            plugin.destroy();
        }
        this.scene.destroy();
    }
}

const tmpVec3a$1 = math.vec3();
const tmpVec3b$1 = math.vec3();

/**
 * @desc Measures the angle indicated by three 3D points.
 *
 * See {@link AngleMeasurementsPlugin} for more info.
 */
class AngleMeasurement extends Component {

    /**
     * @private
     */
    constructor(plugin, cfg = {}) {

        const scene = plugin.viewer.scene;

        super(scene, cfg);

        /**
         * The {@link AngleMeasurementsPlugin} that owns this AngleMeasurement.
         * @type {AngleMeasurementsPlugin}
         */
        this.plugin = plugin;

        const container = cfg.container;
        if (!container) {
            throw "config missing: container";
        }

        this._color = cfg.color || plugin.defaultColor;

        const channel = function(v) {
            const listeners = [ ];
            let value = v !== false;
            return {
                reg: (l) => listeners.push(l),
                get: () => value,
                set: (v) => {
                    value = v !== false;
                    listeners.forEach(l => l(value));
                }
            };
        };

        this._visible           = channel(cfg.visible);
        this._originVisible     = channel(cfg.originVisible);
        this._cornerVisible     = channel(cfg.cornerVisible);
        this._targetVisible     = channel(cfg.targetVisible);
        this._originWireVisible = channel(cfg.originWireVisible);
        this._targetWireVisible = channel(cfg.targetWireVisible);
        this._angleVisible      = channel(cfg.angleVisible);
        this._labelsVisible     = channel();
        this.labelsVisible      = cfg.labelsVisible;
        this._clickable         = channel(false);

        this.approximate = cfg.approximate;


        const canvas = scene.canvas.canvas;

        const onMouseOver = cfg.onMouseOver ? (event) => {
            cfg.onMouseOver(event, this);
            canvas.dispatchEvent(new MouseEvent('mouseover', event));
        } : null;

        const onMouseLeave = cfg.onMouseLeave ? (event) => {
            cfg.onMouseLeave(event, this);
            canvas.dispatchEvent(new MouseEvent('mouseleave', event));
        } : null;

        const onContextMenu = cfg.onContextMenu ? (event) => {
            cfg.onContextMenu(event, this);
        } : null;

        const onMouseDown  = (event) => canvas.dispatchEvent(new MouseEvent('mousedown', event));
        const onMouseUp    = (event) => canvas.dispatchEvent(new MouseEvent('mouseup', event));
        const onMouseMove  = (event) => canvas.dispatchEvent(new MouseEvent('mousemove', event));
        const onMouseWheel = (event) => canvas.dispatchEvent(new WheelEvent('wheel', event));


        this._cleanups = [ ];
        this._drawables = [ ];

        const registerDrawable = (drawable, visibilityChannels) => {
            const updateVisibility = () => drawable.setVisible(visibilityChannels.every(ch => ch.get()));
            visibilityChannels.forEach(ch => ch.reg(updateVisibility));
            this._drawables.push(drawable);
            this._cleanups.push(() => drawable.destroy());
        };

        const makeWire = (color, thickness, visibilityChannels) => {
            const wire = new Wire3D(scene, container, {
                color: color,
                thickness: thickness,
                thicknessClickable: 6,
                zIndex: plugin.zIndex !== undefined ? plugin.zIndex + 1 : undefined,
                onMouseOver,
                onMouseLeave,
                onMouseWheel,
                onMouseDown,
                onMouseUp,
                onMouseMove,
                onContextMenu
            });
            registerDrawable(wire, visibilityChannels);
            return {
                setEnds: (p0, p1) => wire.setEnds(p0, p1),
                setColor: value => wire.setColor(value)
            };
        };
        this._originWire = makeWire(this._color || "blue", 1, [ this._visible, this._originWireVisible ]);
        this._targetWire = makeWire(this._color || "red",  1, [ this._visible, this._targetWireVisible ]);

        const makeLabel = (color, zIndexOffset, visibilityChannels) => {
            const label = new Label3D(scene, container, {
                fillColor: color,
                zIndex: plugin.zIndex + zIndexOffset,
                onMouseOver,
                onMouseLeave,
                onMouseWheel,
                onMouseDown,
                onMouseUp,
                onMouseMove,
                onContextMenu
            });
            registerDrawable(label, visibilityChannels);
            return {
                setFillColor:  value => label.setFillColor(value),
                setPosOnWire:  (p0, p1, offset) => label.setPosOnWire(p0, p1, offset),
                setPosBetween: (p0, p1, p2) => label.setPosBetween(p0, p1, p2),
                setText:       str => label.setText(str)
            };
        };
        this._angleLabel = makeLabel(this._color || "#00BBFF", 2, [ this._visible, this._angleVisible, this._labelsVisible ]);

        const makeDot = (cfg, visibilityChannels) => {
            const dot = new Dot3D(scene, cfg, container, {
                fillColor: this._color,
                zIndex: plugin.zIndex !== undefined ? plugin.zIndex + 2 : undefined,
                onMouseOver,
                onMouseLeave,
                onMouseWheel,
                onMouseDown,
                onMouseUp,
                onMouseMove,
                onContextMenu
            });
            dot.on("worldPos", () => this._update());
            registerDrawable(dot, visibilityChannels);
            return dot;
        };
        this._originDot = makeDot(cfg.origin, [ this._visible, this._originVisible ]);
        this._cornerDot = makeDot(cfg.corner, [ this._visible, this._cornerVisible ]);
        this._targetDot = makeDot(cfg.target, [ this._visible, this._targetVisible ]);

        this._update();
    }

    _update() {
        if (! this._targetDot) {
            return;
        }

        const p0 = this._originDot.worldPos;
        const p1 = this._cornerDot.worldPos;
        const p2 = this._targetDot.worldPos;

        this._originWire.setEnds(p0, p1);
        this._targetWire.setEnds(p1, p2);
        this._angleLabel.setPosBetween(p0, p1, p2);

        math.subVec3(p0, p1, tmpVec3a$1);
        math.subVec3(p2, p1, tmpVec3b$1);

        if ((math.lenVec3(tmpVec3a$1) > 0) && (math.lenVec3(tmpVec3b$1) > 0)) {
            math.normalizeVec3(tmpVec3a$1);
            math.normalizeVec3(tmpVec3b$1);
            this._angle = Math.abs(math.angleVec3(tmpVec3a$1, tmpVec3b$1)) * math.RADTODEG;
            this._angleLabel.setText((this._approximate ? " ~ " : " = ") + this._angle.toFixed(2) + "°");
        } else {
            this._angle = undefined;
            this._angleLabel.setText("");
        }
    }

    /**
     * Sets whether this AngleMeasurement indicates that its measurement is approximate.
     *
     * This is ````true```` by default.
     *
     * @type {Boolean}
     */
    set approximate(approximate) {
        approximate = approximate !== false;
        if (this._approximate === approximate) {
            return;
        }
        this._approximate = approximate;
        this._update();
    }

    /**
     * Gets whether this AngleMeasurement indicates that its measurement is approximate.
     *
     * This is ````true```` by default.
     *
     * @type {Boolean}
     */
    get approximate() {
        return this._approximate;
    }

    /**
     * Gets the origin {@link Dot3D}.
     *
     * @type {Dot3D}
     */
    get origin() {
        return this._originDot;
    }

    /**
     * Gets the corner {@link Dot3D}.
     *
     * @type {Dot3D}
     */
    get corner() {
        return this._cornerDot;
    }

    /**
     * Gets the target {@link Dot3D}.
     *
     * @type {Dot3D}
     */
    get target() {
        return this._targetDot;
    }

    /**
     * Gets the angle between two connected 3D line segments, given
     * as three positions on the surface(s) of one or more {@link Entity}s.
     *
     * @type {Number}
     */
    get angle() {
        return this._angle;
    }

    /**
     * Gets the color of the angle measurement.
     *
     * The color is an HTML string representation, eg. "#00BBFF" and "blue".
     *
     * @type {String}
     */
    get color() {
        return this._color;
    }

    /** Sets the color of the angle measurement.
     *
     * The color is given as an HTML string representation, eg. "#00BBFF" and "blue".
     *
     * @type {String}
     */
    set color(value) {
        this._color = value;
        this._originDot.setFillColor(value);
        this._cornerDot.setFillColor(value);
        this._targetDot.setFillColor(value);
        this._originWire.setColor(value || "blue");
        this._targetWire.setColor(value || "red");
        this._angleLabel.setFillColor(value || "#00BBFF");
    }

    /**
     * Sets whether this AngleMeasurement is visible or not.
     *
     * @type {Boolean}
     */
    set visible(value) {
        this._visible.set(value);
    }

    /**
     * Gets whether this AngleMeasurement is visible or not.
     *
     * @type {Boolean}
     */
    get visible() {
        return this._visible.get();
    }

    /**
     * Sets if the origin {@link Dot3D} is visible.
     *
     * @type {Boolean}
     */
    set originVisible(value) {
        this._originVisible.set(value);
    }

    /**
     * Gets if the origin {@link Dot3D} is visible.
     *
     * @type {Boolean}
     */
    get originVisible() {
        return this._originVisible.get();
    }

    /**
     * Sets if the corner {@link Dot3D} is visible.
     *
     * @type {Boolean}
     */
    set cornerVisible(value) {
        this._cornerVisible.set(value);
    }

    /**
     * Gets if the corner {@link Dot3D} is visible.
     *
     * @type {Boolean}
     */
    get cornerVisible() {
        return this._cornerVisible.get();
    }

    /**
     * Sets if the target {@link Dot3D} is visible.
     *
     * @type {Boolean}
     */
    set targetVisible(value) {
        this._targetVisible.set(value);
    }

    /**
     * Gets if the target {@link Dot3D} is visible.
     *
     * @type {Boolean}
     */
    get targetVisible() {
        return this._targetVisible.get();
    }

    /**
     * Sets if the wire between the origin and the corner is visible.
     *
     * @type {Boolean}
     */
    set originWireVisible(value) {
        this._originWireVisible.set(value);
    }

    /**
     * Gets if the wire between the origin and the corner is visible.
     *
     * @type {Boolean}
     */
    get originWireVisible() {
        return this._originWireVisible.get();
    }

    /**
     * Sets if the wire between the target and the corner is visible.
     *
     * @type {Boolean}
     */
    set targetWireVisible(value) {
        this._targetWireVisible.set(value);
    }

    /**
     * Gets if the wire between the target and the corner is visible.
     *
     * @type {Boolean}
     */
    get targetWireVisible() {
        return this._targetWireVisible.get();
    }

    /**
     * Sets if the angle label is visible.
     *
     * @type {Boolean}
     */
    set angleVisible(value) {
        this._angleVisible.set(value);
    }

    /**
     * Gets if the angle label is visible.
     *
     * @type {Boolean}
     */
    get angleVisible() {
        return this._angleVisible.get();
    }

    /**
     * Sets if the labels are visible.
     *
     * @type {Boolean}
     */
    set labelsVisible(value) {
        this._labelsVisible.set(value !== undefined ? Boolean(value) : this.plugin.defaultLabelsVisible);
    }

    /**
     * Gets if the labels are visible.
     *
     * @type {Boolean}
     */
    get labelsVisible() {
        return this._labelsVisible.get();
    }

    /**
     * Sets if this DistanceMeasurement appears highlighted.
     * @param highlighted
     */
    setHighlighted(highlighted) {
        this._drawables.forEach(d => d.setHighlighted(highlighted));
    }

    /**
     * Sets if the wires, dots ad labels will fire "mouseOver" "mouseLeave" and "contextMenu" events.
     *
     * @type {Boolean}
     */
    set clickable(value) {
        this._clickable.set(!!value);
        this._drawables.forEach(d => d.setClickable(this._clickable.get()));
    }

    /**
     * Gets if the wires, dots ad labels will fire "mouseOver" "mouseLeave" and "contextMenu" events.
     *
     * @type {Boolean}
     */
    get clickable() {
        return this._clickable.get();
    }

    /**
     * @private
     */
    destroy() {
        this._cleanups.forEach(cleanup => cleanup());
        super.destroy();
    }
}

/**
 * Creates {@link AngleMeasurement}s in an {@link AngleMeasurementsPlugin} from user input.
 *
 * @interface
 * @abstract
 */
class AngleMeasurementsControl extends Component {

    /**
     * Gets if this AngleMeasurementsControl is currently active, where it is responding to input.
     *
     * @returns {boolean} True if this AngleMeassurementsControl is active.
     * @abstract
     */
    get active() {
    }

    /**
     * Sets whether snap-to-vertex and snap-to-edge are enabled for this AngleMeasurementsControl.
     *
     * This is `true` by default.
     *
     * Internally, this deactivates then activates the AngleMeasurementsControl when changed, which means that
     * it will destroy any AngleMeasurements currently under construction, and incurs some overhead, since it unbinds
     * and rebinds various input handlers.
     *
     * @param {boolean} snapping Whether to enable snap-to-vertex and snap-edge for this AngleMeasurementsControl.
     */
    set snapping(snapping) {
    }

    /**
     * Gets whether snap-to-vertex and snap-to-edge are enabled for this AngleMeasurementsControl.
     *
     * This is `true` by default.
     *
     * @returns {boolean} Whether snap-to-vertex and snap-to-edge are enabled for this AngleMeasurementsControl.
     */
    get snapping() {
        return true;
    }

    /**
     * Activates this AngleMeasurementsMouseControl, ready to respond to input.
     *
     * @abstract
     */
    activate() {
    }

    /**
     * Deactivates this AngleMeasurementsControl, making it unresponsive to input.
     *
     * Destroys any {@link AngleMeasurement} under construction by this AngleMeasurementsControl.
     *
     * @abstract
     */
    deactivate() {
    }

    /**
     * Resets this AngleMeasurementsControl.
     *
     * Destroys any {@link AngleMeasurement} under construction by this AngleMeasurementsControl.
     *
     * Does nothing if the AngleMeasurementsControl is not active.
     *
     * @abstract
     */
    reset() {
    }

    /**
     * Gets the {@link AngleMeasurement} under construction by this AngleMeasurementsControl, if any.
     *
     * @returns {null|AngleMeasurement}
     * @abstract
     */
    get currentMeasurement() {
        return null;
    }

    /**
     * Destroys this AngleMeasurementsMouseControl.
     *
     * Destroys any {@link AngleMeasurement} under construction by this AngleMeasurementsControl.
     *
     * @abstract
     */
    destroy() {
    }
}

const MOUSE_FINDING_ORIGIN = 0;
const MOUSE_FINDING_CORNER = 1;
const MOUSE_FINDING_TARGET = 2;

/**
 * Creates {@link AngleMeasurement}s in an {@link AngleMeasurementsPlugin} from mouse input.
 *
 * ## Usage
 *
 * [[Run example](/examples/measurement/#angle_createWithMouse_snapping)]
 *
 * ````javascript
 * import {Viewer, XKTLoaderPlugin, AngleMeasurementsPlugin, AngleMeasurementsMouseControl, PointerLens} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 * });
 *
 * viewer.camera.eye = [-3.93, 2.85, 27.01];
 * viewer.camera.look = [4.40, 3.72, 8.89];
 * viewer.camera.up = [-0.01, 0.99, 0.039];
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const sceneModel = xktLoader.load({
 *     id: "myModel",
 *     src: "Duplex.xkt"
 * });
 *
 * const angleMeasurements = new AngleMeasurementsPlugin(viewer);
 *
 * const angleMeasurementsMouseControl  = new AngleMeasurementsMouseControl(angleMeasurements, {
 *     pointerLens : new PointerLens(viewer)
 * })
 *
 * angleMeasurementsMouseControl.snapping = true;
 *
 * angleMeasurementsMouseControl.activate();
 * ````
 */
class AngleMeasurementsMouseControl extends AngleMeasurementsControl {

    /**
     * Creates a AngleMeasurementsMouseControl bound to the given AngleMeasurementsPlugin.
     *
     * @param {AngleMeasurementsPlugin} angleMeasurementsPlugin The AngleMeasurementsPlugin to control.
     * @param {*} [cfg] Configuration
     * @param {function} [cfg.canvasToPagePos] Optional function to map canvas-space coordinates to page coordinates.
     * @param {PointerLens} [cfg.pointerLens] A PointerLens to use to provide a magnified view of the cursor when snapping is enabled.
     * @param {boolean} [cfg.snapping=true] Whether to initially enable snap-to-vertex and snap-to-edge for this AngleMeasurementsMouseControl.
     */
    constructor(angleMeasurementsPlugin, cfg = {}) {

        super(angleMeasurementsPlugin.viewer.scene);

        this._canvasToPagePos = cfg.canvasToPagePos;

        this.pointerLens = cfg.pointerLens;

        this._active = false;

        this._currentAngleMeasurement = null;

        this._initMarkerDiv();

        this._snapping = cfg.snapping !== false;
        this._mouseState = MOUSE_FINDING_ORIGIN;

        this._attachPlugin(angleMeasurementsPlugin, cfg);
    }

    _initMarkerDiv() {
        const markerDiv = document.createElement('div');
        const canvas = this.scene.canvas.canvas;
        canvas.parentNode.insertBefore(markerDiv, canvas);

        markerDiv.style.background = "black";
        markerDiv.style.border = "2px solid blue";
        markerDiv.style.borderRadius = "10px";
        markerDiv.style.width = "5px";
        markerDiv.style.height = "5px";
        markerDiv.style.top = "-200px";
        markerDiv.style.left = "-200px";
        markerDiv.style.margin = "0 0";
        markerDiv.style.zIndex = "100";
        markerDiv.style.position = "absolute";
        markerDiv.style.pointerEvents = "none";

        this.markerDiv = markerDiv;
    }

    _destroyMarkerDiv() {
        if (this.markerDiv) {
            this.markerDiv.parentNode.removeChild(this.markerDiv);
            this.markerDiv = null;
        }
    }

    _attachPlugin(angleMeasurementsPlugin, cfg = {}) {

        /**
         * The {@link AngleMeasurementsPlugin} that owns this AngleMeasurementsMouseControl.
         *
         * @type {AngleMeasurementsPlugin}
         */
        this.angleMeasurementsPlugin = angleMeasurementsPlugin;

        /**
         * The {@link AngleMeasurementsPlugin} that owns this AngleMeasurementsMouseControl.
         *
         * @type {AngleMeasurementsPlugin}
         */
        this.plugin = angleMeasurementsPlugin;
    }

    /**
     * Gets if this AngleMeasurementsMouseControl is currently active, where it is responding to input.
     *
     * @returns {boolean}
     */
    get active() {
        return this._active;
    }

    /**
     * Sets whether snap-to-vertex and snap-to-edge are enabled for this AngleMeasurementsMouseControl.
     *
     * This is `true` by default.
     *
     * @param {boolean} snapping Whether to enable snap-to-vertex and snap-edge for this AngleMeasurementsMouseControl.
     */
    set snapping(snapping) {
        this._snapping = snapping;
    }

    /**
     * Gets whether snap-to-vertex and snap-to-edge are enabled for this AngleMeasurementsMouseControl.
     *
     * This is `true` by default.
     *
     * @returns {boolean} Whether snap-to-vertex and snap-to-edge are enabled for this AngleMeasurementsMouseControl.
     */
    get snapping() {
        return this._snapping;
    }

    /**
     * Activates this AngleMeasurementsMouseControl, ready to respond to input.
     */
    activate() {
        if (this._active) {
            return;
        }
        if (!this.markerDiv) {
            this._initMarkerDiv(); // if the marker is destroyed after deactivation, we recreate it
        }
        this.angleMeasurementsPlugin;
        const scene = this.scene;
        scene.input;
        const canvas = scene.canvas.canvas;
        const clickTolerance = 20;
        const cameraControl = this.angleMeasurementsPlugin.viewer.cameraControl;
        const pointerLens = this.pointerLens;
        let mouseHovering = false;
        let hoveredEntity = null;
        let lastMouseCanvasX = 0;
        let lastMouseCanvasY = 0;
        const mouseWorldPos = math.vec3();
        const mouseHoverCanvasPos = math.vec2();
        this._currentAngleMeasurement = null;

        const pagePos = math.vec2();

        const hoverOn = event => {
                if (event.snappedToVertex || event.snappedToEdge) {
                    if (pointerLens) {
                        pointerLens.visible = true;
                        pointerLens.canvasPos = event.canvasPos;
                        pointerLens.snappedCanvasPos = event.snappedCanvasPos || event.canvasPos;
                        pointerLens.snapped = true;
                    }
                    this.markerDiv.style.background = "greenyellow";
                    this.markerDiv.style.border = "2px solid green";
                } else {
                    if (pointerLens) {
                        pointerLens.visible = true;
                        pointerLens.canvasPos =  event.canvasPos;
                        pointerLens.snappedCanvasPos = event.canvasPos;
                        pointerLens.snapped = false;
                    }
                    this.markerDiv.style.background = "pink";
                    this.markerDiv.style.border = "2px solid red";
                }
                const canvasPos = event.snappedCanvasPos || event.canvasPos;
                mouseHovering = true;
                hoveredEntity = event.entity;
                mouseWorldPos.set(event.worldPos);
                mouseHoverCanvasPos.set(canvasPos);
                switch (this._mouseState) {
                    case MOUSE_FINDING_ORIGIN:
                        if (this._canvasToPagePos) {
                            this._canvasToPagePos(canvas, canvasPos, pagePos);
                            this.markerDiv.style.left = `${pagePos[0] - 5}px`;
                            this.markerDiv.style.top = `${pagePos[1] - 5}px`;
                        } else {
                            const markerPos = math.vec2(canvasPos);
                            transformToNode(canvas, this.markerDiv.parentNode, markerPos);
                            this.markerDiv.style.left = `${markerPos[0] - 5}px`;
                            this.markerDiv.style.top = `${markerPos[1] - 5}px`;
                        }
                        break;
                    case MOUSE_FINDING_CORNER:
                        if (this._currentAngleMeasurement) {
                            this._currentAngleMeasurement.originWireVisible = true;
                            this._currentAngleMeasurement.targetWireVisible = false;
                            this._currentAngleMeasurement.cornerVisible = true;
                            this._currentAngleMeasurement.angleVisible = false;
                            this._currentAngleMeasurement.corner.worldPos = event.worldPos;
                            this._currentAngleMeasurement.corner.entity = event.entity;
                        }
                        this.markerDiv.style.left = `-10000px`;
                        this.markerDiv.style.top = `-10000px`;
                        canvas.style.cursor = "pointer";
                        break;
                    case MOUSE_FINDING_TARGET:
                        if (this._currentAngleMeasurement) {
                            this._currentAngleMeasurement.targetWireVisible = true;
                            this._currentAngleMeasurement.targetVisible = true;
                            this._currentAngleMeasurement.angleVisible = true;
                            this._currentAngleMeasurement.target.worldPos = event.worldPos;
                            this._currentAngleMeasurement.target.entity = event.entity;
                        }
                        this.markerDiv.style.left = `-10000px`;
                        this.markerDiv.style.top = `-10000px`;
                        canvas.style.cursor = "pointer";
                        break;
                }
        };
        this._onHoverSnapOrSurface = cameraControl.on("hoverSnapOrSurface", e => { if (this._snapping)   hoverOn(e); });
        this._onHoverSurface       = cameraControl.on("hoverSurface",       e => { if (! this._snapping) hoverOn(e); });

        canvas.addEventListener('mousedown', this._onMouseDown = (e) => {
            if (e.which !== 1) {
                return;
            }
            lastMouseCanvasX = e.clientX;
            lastMouseCanvasY = e.clientY;
        });
        canvas.addEventListener("mouseup", this._onMouseUp =(e) => {
            if (e.which !== 1) {
                return;
            }
            if (e.clientX > lastMouseCanvasX + clickTolerance ||
                e.clientX < lastMouseCanvasX - clickTolerance ||
                e.clientY > lastMouseCanvasY + clickTolerance ||
                e.clientY < lastMouseCanvasY - clickTolerance) {
                return;
            }
            switch (this._mouseState) {
                case MOUSE_FINDING_ORIGIN:
                    if (mouseHovering) {
                        this._currentAngleMeasurement = this.angleMeasurementsPlugin.createMeasurement({
                            id: math.createUUID(),
                            origin: {
                                worldPos: mouseWorldPos,
                                entity: hoveredEntity
                            },
                            corner: {
                                worldPos: mouseWorldPos,
                                entity: hoveredEntity
                            },
                            target: {
                                worldPos: mouseWorldPos,
                                entity: hoveredEntity
                            },
                            approximate: true
                        });
                        this._currentAngleMeasurement.clickable = false;
                        this._currentAngleMeasurement.originVisible = true;
                        this._currentAngleMeasurement.originWireVisible = true;
                        this._currentAngleMeasurement.cornerVisible = false;
                        this._currentAngleMeasurement.targetWireVisible = false;
                        this._currentAngleMeasurement.targetVisible = false;
                        this._currentAngleMeasurement.angleVisible = false;
                        this._currentAngleMeasurement.origin.entity = hoveredEntity;
                        this._mouseState = MOUSE_FINDING_CORNER;
                        this.angleMeasurementsPlugin.fire("measurementStart", this._currentAngleMeasurement);
                    }
                    break;
                case MOUSE_FINDING_CORNER:
                    if (mouseHovering) {
                        this._currentAngleMeasurement.targetWireVisible = false;
                        this._currentAngleMeasurement.targetVisible = true;
                        this._currentAngleMeasurement.angleVisible = true;
                        this._currentAngleMeasurement.corner.entity = hoveredEntity;
                        this._mouseState = MOUSE_FINDING_TARGET;
                    } else {
                        if (this._currentAngleMeasurement) {
                            this._currentAngleMeasurement.destroy();
                            this._currentAngleMeasurement = null;
                            hoveredEntity = null;
                            this._mouseState = MOUSE_FINDING_ORIGIN;
                            this.angleMeasurementsPlugin.fire("measurementCancel", this._currentAngleMeasurement);
                        }
                    }
                    break;
                case MOUSE_FINDING_TARGET:
                    if (mouseHovering) {
                        this._currentAngleMeasurement.targetVisible = true;
                        this._currentAngleMeasurement.angleVisible = true;
                        this._currentAngleMeasurement.target.entity = hoveredEntity;
                        this._currentAngleMeasurement.clickable = true;
                        hoveredEntity = null;
                        this.angleMeasurementsPlugin.fire("measurementEnd", this._currentAngleMeasurement);
                        this._currentAngleMeasurement = null;
                        this._mouseState = MOUSE_FINDING_ORIGIN;
                    } else {
                        if (this._currentAngleMeasurement) {
                            this._currentAngleMeasurement.destroy();
                            this._currentAngleMeasurement = null;
                            hoveredEntity = null;
                            this._mouseState = MOUSE_FINDING_ORIGIN;
                            this.angleMeasurementsPlugin.fire("measurementCancel", this._currentAngleMeasurement);
                        }
                    }
                    break;
            }
        });

        const hoverOff = event => {
                mouseHovering = false;
                if (pointerLens) {
                    pointerLens.visible = true;
                    pointerLens.canvasPos = event.canvasPos;
                    pointerLens.snappedCanvasPos = event.snappedCanvasPos || event.canvasPos;
                    pointerLens.snapped = false;
                }
                this.markerDiv.style.left = `-100px`;
                this.markerDiv.style.top = `-100px`;
                if (this._currentAngleMeasurement) {
                    switch (this._mouseState) {
                        case MOUSE_FINDING_ORIGIN:
                            this._currentAngleMeasurement.originVisible = false;
                            break;
                        case MOUSE_FINDING_CORNER:
                            this._currentAngleMeasurement.cornerVisible = false;
                            this._currentAngleMeasurement.originWireVisible = false;
                            this._currentAngleMeasurement.targetVisible = false;
                            this._currentAngleMeasurement.targetWireVisible = false;
                            this._currentAngleMeasurement.angleVisible = false;
                            break;
                        case MOUSE_FINDING_TARGET:
                            this._currentAngleMeasurement.targetVisible = false;
                            this._currentAngleMeasurement.targetWireVisible = false;
                            this._currentAngleMeasurement.angleVisible = false;
                            break;
                    }
                    canvas.style.cursor = "default";
                }
        };
        this._onHoverSnapOrSurfaceOff = cameraControl.on("hoverSnapOrSurfaceOff", e => { if (this._snapping)   hoverOff(e); });
        this._onHoverOff              = cameraControl.on("hoverOff",              e => { if (! this._snapping) hoverOff(e); });

        this._active = true;
    }

    /**
     * Deactivates this AngleMeasurementsMouseControl, making it unresponsive to input.
     *
     * Destroys any {@link AngleMeasurement} under construction by this AngleMeasurementsMouseControl.
     */
    deactivate() {
        if (!this._active) {
            return;
        }

        if (this.pointerLens) {
            this.pointerLens.visible = false;
        }

        this.reset();

        const canvas = this.scene.canvas.canvas;
        canvas.removeEventListener("mousedown", this._onMouseDown);
        canvas.removeEventListener("mouseup", this._onMouseUp);
        const cameraControl = this.angleMeasurementsPlugin.viewer.cameraControl;
        cameraControl.off(this._onHoverSnapOrSurface);
        cameraControl.off(this._onHoverSurface);
        cameraControl.off(this._onHoverSnapOrSurfaceOff);
        cameraControl.off(this._onHoverOff);

        this._active = false;
    }

    /**
     * Resets this AngleMeasurementsMouseControl.
     *
     * Destroys any {@link AngleMeasurement} under construction by this AngleMeasurementsMouseControl.
     *
     * Does nothing if the AngleMeasurementsMouseControl is not active.
     */
    reset() {
        if (!this._active) {
            return;
        }

        this._destroyMarkerDiv();
        this._initMarkerDiv();

        if (this._currentAngleMeasurement) {
            this._currentAngleMeasurement.destroy();
            this._currentAngleMeasurement = null;
        }

        this._mouseState = MOUSE_FINDING_ORIGIN;
    }

    /**
     * Gets the {@link AngleMeasurement} under construction by this AngleMeasurementsMouseControl, if any.
     *
     * @returns {null|AngleMeasurement}
     */
    get currentMeasurement() {
        return this._currentAngleMeasurement;
    }

    /**
     * Destroys this AngleMeasurementsMouseControl.
     */
    destroy() {
        this.deactivate();
        super.destroy();
    }
}

/**
 * {@link Viewer} plugin for measuring angles.
 *
 * [<img src="https://user-images.githubusercontent.com/83100/63641903-61488180-c6b6-11e9-8e00-895b9d16dc4b.gif">](https://xeokit.github.io/xeokit-sdk/examples/index.html#measurements_angle_createWithMouse)
 *
 * * [[Example 1: Model with angle measurements](https://xeokit.github.io/xeokit-sdk/examples/index.html#measurements_angle_modelWithMeasurements)]
 * * [[Example 2: Create angle measurements with mouse](https://xeokit.github.io/xeokit-sdk/examples/measurement/#angle_createWithMouse_snapping)]
 *
 * ## Overview
 *
 * * An {@link AngleMeasurement} shows the angle between two connected 3D line segments, given
 * as three positions on the surface(s) of one or more {@link Entity}s.
 * * As shown on the screen capture above, a AngleMeasurement has two wires that show the line segments, with a label that shows the angle between them.
 * * Create AngleMeasurements programmatically with {@link AngleMeasurementsPlugin#createMeasurement}.
 * * Create AngleMeasurements interactively using a {@link AngleMeasurementsControl}.
 * * Existing AngleMeasurements are registered by ID in {@link AngleMeasurementsPlugin#measurements}.
 * * Destroy AngleMeasurements using {@link AngleMeasurementsPlugin#destroyMeasurement}.
 * * Configure global measurement units and scale via {@link Metrics}, located at {@link Scene#metrics}
 *
 * ## Example 1: Creating AngleMeasurements Programmatically
 *
 * In our first example, we'll use an {@link XKTLoaderPlugin} to load a model, and then use a AngleMeasurementsPlugin to programmatically create two {@link AngleMeasurement}s.
 *
 * Note how each AngleMeasurement has ````origin````, ````corner```` and  ````target````, which each indicate a 3D World-space
 * position on the surface of an {@link Entity}. These can be aon the same Entity, or on different Entitys.
 *
 * [[Run example](https://xeokit.github.io/xeokit-sdk/examples/index.html#measurements_angle_modelWithMeasurements)]
 *
 * ````JavaScript
 * import {Viewer, XKTLoaderPlugin, AngleMeasurementsPlugin} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.scene.camera.eye = [-2.37, 18.97, -26.12];
 * viewer.scene.camera.look = [10.97, 5.82, -11.22];
 * viewer.scene.camera.up = [0.36, 0.83, 0.40];
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const angleMeasurements = new AngleMeasurementsPlugin(viewer);
 *
 * const model = xktLoader.load({
 *      src: "./models/xkt/duplex/duplex.xkt"
 * });
 *
 * model.on("loaded", () => {
 *
 *      const myMeasurement1 = angleMeasurements.createMeasurement({
 *          id: "myAngleMeasurement1",
 *          origin: {
 *              entity: viewer.scene.objects["2O2Fr$t4X7Zf8NOew3FLOH"],
 *              worldPos: [0.044, 5.998, 17.767]
 *          },
 *          corner: {
 *              entity: viewer.scene.objects["2O2Fr$t4X7Zf8NOew3FLOH"],
 *              worldPos: [0.044, 5.998, 17.767]
 *          },
 *          target: {
 *              entity: viewer.scene.objects["2O2Fr$t4X7Zf8NOew3FLOH"],
 *              worldPos: [4.738, 3.172, 17.768]
 *          },
 *          visible: true
 *      });
 *
 *      const myMeasurement2 = angleMeasurements.createMeasurement({
 *          id: "myAngleMeasurement2",
 *          origin: {
 *              entity: viewer.scene.objects["2O2Fr$t4X7Zf8NOew3FNr2"],
 *              worldPos: [0.457, 2.532, 17.766]
 *          },
 *          corner: {
 *              entity: viewer.scene.objects["2O2Fr$t4X7Zf8NOew3FNr2"],
 *              worldPos: [0.457, 2.532, 17.766]
 *          },
 *          target: {
 *              entity: viewer.scene.objects["1CZILmCaHETO8tf3SgGEXu"],
 *              worldPos: [0.436, 0.001, 22.135]
 *          },
 *          visible: true
 *      });
 * });
 * ````
 *
 * ## Example 2: Creating AngleMeasurements with Mouse Input
 *
 * In our second example, we'll use an {@link XKTLoaderPlugin} to load a model, then we'll use the AngleMeasurementsPlugin's {@link AngleMeasurementsTouchControl} to interactively create {@link AngleMeasurement}s with mouse or touch input.
 *
 * After we've activated the AngleMeasurementsControl, the first click on any {@link Entity} begins constructing a AngleMeasurement, fixing its
 * origin to that Entity. The next click on any Entity will fix the AngleMeasurement's corner, and the next click after
 * that will fix its target and complete the AngleMeasurement.
 *
 * The AngleMeasurementControl will then wait for the next click on any Entity, to begin constructing
 * another AngleMeasurement, and so on, until deactivated again.
 *
 * [[Run example](https://xeokit.github.io/xeokit-sdk/examples/measurement/#angle_createWithMouse_snapping)]
 *
 * ````JavaScript
 * import {Viewer, XKTLoaderPlugin, AngleMeasurementsPlugin, AngleMeasurementsMouseControl, PointerLens} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.scene.camera.eye = [-2.37, 18.97, -26.12];
 * viewer.scene.camera.look = [10.97, 5.82, -11.22];
 * viewer.scene.camera.up = [0.36, 0.83, 0.40];
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * cconst angleMeasurementsMouseControl  = new AngleMeasurementsMouseControl(angleMeasurements, {
 *     pointerLens : new PointerLens(viewer)
 * })
 *
 * angleMeasurementsMouseControl.snapToVertex = true;
 * angleMeasurementsMouseControl.snapToEdge = true;
 *
 * angleMeasurementsMouseControl.activate();
 * ````
 *
 * ## Example 4: Attaching Mouse Handlers
 *
 * In our fourth example, we'll attach even handlers to our plugin, to catch when the user
 * hovers or right-clicks over our measurements.
 *
 * [[Run example](/examples/measurement/#angle_modelWithMeasurements)]
 *
 * ````javascript
 * import {Viewer, XKTLoaderPlugin, AngleMeasurementsPlugin} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.scene.camera.eye = [-2.37, 18.97, -26.12];
 * viewer.scene.camera.look = [10.97, 5.82, -11.22];
 * viewer.scene.camera.up = [0.36, 0.83, 0.40];
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const angleMeasurements = new AngleMeasurementsPlugin(viewer);
 *
 * angleMeasurements.on("mouseOver", (e) => {
 *     e.measurement.setHighlighted(true);
 * });
 *
 * angleMeasurements.on("mouseLeave", (e) => {
 *     e.measurement.setHighlighted(false);
 * });
 *
 * angleMeasurements.on("contextMenu", (e) => {
 *     // Show context menu
 *     e.event.preventDefault();
 * });
 *
 * const model = xktLoader.load({
 *      src: "./models/xkt/duplex/duplex.xkt"
 * });
 *
 * model.on("loaded", () => {
 *
 *       angleMeasurementsPlugin.createMeasurement({
 *             id: "angleMeasurement1",
 *             origin: {
 *                 entity: viewer.scene.objects["1CZILmCaHETO8tf3SgGEXu"],
 *                 worldPos: [0.4158603637281142, 2.5193106917110457, 17.79972838299403]
 *             },
 *             corner: {
 *                 entity: viewer.scene.objects["1CZILmCaHETO8tf3SgGEXu"],
 *                 worldPos: [0.41857741956197625,0.0987169929481646,17.799763071093395]
 *             },
 *             target: {
 *                 entity: viewer.scene.objects["1CZILmCaHETO8tf3SgGEXu"],
 *                 worldPos: [5.235526066859247, 0.11580773869801986, 17.824891550941565]
 *             },
 *             visible: true
 *         });
 *
 *         angleMeasurementsPlugin.createMeasurement({
 *             id: "angleMeasurement2",
 *             origin: {
 *                 entity: viewer.scene.objects["2O2Fr$t4X7Zf8NOew3FNr2"],
 *                 worldPos: [-0.00003814187850181838, 5.9996748076205115,17.79996871551525]
 *             },
 *             corner: {
 *                 entity: viewer.scene.objects["2O2Fr$t4X7Zf8NOew3FNqI"],
 *                 worldPos: [-0.0005214119318139865, 3.1010044228517595, 17.787656604483363]
 *
 *             },
 *             target: {
 *                 entity: viewer.scene.objects["1s1jVhK8z0pgKYcr9jt7AB"],
 *                 worldPos: [ 8.380657312957396, 3.1055697628459553, 17.799220108187185]
 *             },
 *             visible: true
 *         });
 * });
 * ````
 *
 * ## Example 5: Creating AngleMeasurements with Touch Input
 *
 * In our fifth example, we'll show how to create angle measurements with touch input, with snapping
 * to the nearest vertex or edge. While creating the measurements, a long-touch when setting the
 * start, corner or end point will cause the point to snap to the nearest vertex or edge. A quick
 * touch-release will immediately set the point at the tapped position on the object surface.
 *
 * [[Run example](https://xeokit.github.io/xeokit-sdk/examples/measurement/#angle_createWithTouch_snapping)]
 *
 * ````javascript
 * import {Viewer, XKTLoaderPlugin, AngleMeasurementsPlugin, AngleMeasurementsTouchControl} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.scene.camera.eye = [-2.37, 18.97, -26.12];
 * viewer.scene.camera.look = [10.97, 5.82, -11.22];
 * viewer.scene.camera.up = [0.36, 0.83, 0.40];
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const angleMeasurements = new AngleMeasurementsPlugin(viewer);
 *
 * const model = xktLoader.load({
 *      src: "./models/xkt/duplex/duplex.xkt"
 * });
 *
 * const angleMeasurements = new AngleMeasurementsPlugin(viewer);
 *
 * const angleMeasurementsTouchControl  = new AngleMeasurementsTouchControl(angleMeasurements, {
 *     pointerLens : new PointerLens(viewer),
 *     snapToVertex: true,
 *     snapToEdge: true
 * })
 *
 * angleMeasurementsTouchControl.activate();
 * ````
 */
class AngleMeasurementsPlugin extends Plugin {

    /**
     * @constructor
     * @param {Viewer} viewer The Viewer.
     * @param {Object} [cfg]  Plugin configuration.
     * @param {String} [cfg.id="AngleMeasurements"] Optional ID for this plugin, so that we can find it within {@link Viewer#plugins}.
     * @param {HTMLElement} [cfg.container] Container DOM element for markers and labels. Defaults to ````document.body````.
     * @param {string} [cfg.defaultColor=null] The default color of the dots, wire and label.
     * @param {boolean} [cfg.defaultLabelsVisible=true] The default value of {@link AngleMeasurement.labelsVisible}.
     * @param {number} [cfg.zIndex] If set, the wires, dots and labels will have this zIndex (+1 for dots and +2 for labels).
     * @param {PointerCircle} [cfg.pointerLens] A PointerLens to help the user position the pointer. This can be shared with other plugins.
     */
    constructor(viewer, cfg = {}) {

        super("AngleMeasurements", viewer);

        this._container = cfg.container || document.body;

        this._defaultControl = null;

        this._measurements = {};

        this.defaultColor = cfg.defaultColor !== undefined ? cfg.defaultColor : "#00BBFF";
        this.defaultLabelsVisible = cfg.defaultLabelsVisible !== false;
        this.zIndex = cfg.zIndex || 10000;

        this._onMouseOver = (event, measurement) => {
            this.fire("mouseOver", {
                plugin: this,
                angleMeasurement: measurement,
                measurement,
                event
            });
        };

        this._onMouseLeave = (event, measurement) => {
            this.fire("mouseLeave", {
                plugin: this,
                angleMeasurement: measurement,
                measurement,
                event
            });
        };

        this._onContextMenu = (event, measurement) => {
            this.fire("contextMenu", {
                plugin: this,
                angleMeasurement: measurement,
                measurement,
                event
            });
        };
    }

    /**
     * Gets the plugin's HTML container element, if any.
     * @returns {*|HTMLElement|HTMLElement}
     */
    getContainerElement() {
        return this._container;
    }


    /**
     * @private
     */
    send(name, value) {

    }

    /**
     * Gets the default {@link AngleMeasurementsMouseControl}.
     *
     * @type {AngleMeasurementsMouseControl}
     * @deprecated
     */
    get control() {
        if (!this._defaultControl) {
            this._defaultControl = new AngleMeasurementsMouseControl(this, {});
        }
        return this._defaultControl;
    }

    /**
     * Gets the existing {@link AngleMeasurement}s, each mapped to its {@link AngleMeasurement#id}.
     *
     * @type {{String:AngleMeasurement}}
     */
    get measurements() {
        return this._measurements;
    }

    /**
     * Creates an {@link AngleMeasurement}.
     *
     * The AngleMeasurement is then registered by {@link AngleMeasurement#id} in {@link AngleMeasurementsPlugin#measurements}.
     *
     * @param {Object} params {@link AngleMeasurement} configuration.
     * @param {String} params.id Unique ID to assign to {@link AngleMeasurement#id}. The AngleMeasurement will be registered by this in {@link AngleMeasurementsPlugin#measurements} and {@link Scene.components}. Must be unique among all components in the {@link Viewer}.
     * @param {Number[]} params.origin.worldPos Origin World-space 3D position.
     * @param {Entity} params.origin.entity Origin Entity.
     * @param {Number[]} params.corner.worldPos Corner World-space 3D position.
     * @param {Entity} params.corner.entity Corner Entity.
     * @param {Number[]} params.target.worldPos Target World-space 3D position.
     * @param {Entity} params.target.entity Target Entity.
     * @param {Boolean} [params.visible=true] Whether to initially show the {@link AngleMeasurement}.
     * @returns {AngleMeasurement} The new {@link AngleMeasurement}.
     */
    createMeasurement(params = {}) {
        if (this.viewer.scene.components[params.id]) {
            this.error("Viewer scene component with this ID already exists: " + params.id);
            delete params.id;
        }
        const origin = params.origin;
        const corner = params.corner;
        const target = params.target;
        const measurement = new AngleMeasurement(this, {
            id: params.id,
            plugin: this,
            container: this._container,
            origin: {
                entity: origin.entity,
                worldPos: origin.worldPos
            },
            corner: {
                entity: corner.entity,
                worldPos: corner.worldPos
            },
            target: {
                entity: target.entity,
                worldPos: target.worldPos
            },

            visible: params.visible,
            originVisible: true,
            originWireVisible: true,
            cornerVisible: true,
            targetWireVisible: true,
            targetVisible: true,
            onMouseOver: this._onMouseOver,
            onMouseLeave: this._onMouseLeave,
            onContextMenu: this._onContextMenu
        });
        this._measurements[measurement.id] = measurement;
        measurement.on("destroyed", () => {
            delete this._measurements[measurement.id];
        });
        measurement.clickable = true;
        this.fire("measurementCreated", measurement);
        return measurement;
    }

    /**
     * Destroys a {@link AngleMeasurement}.
     *
     * @param {String} id ID of AngleMeasurement to destroy.
     */
    destroyMeasurement(id) {
        const measurement = this._measurements[id];
        if (!measurement) {
            this.log("AngleMeasurement not found: " + id);
            return;
        }
        measurement.destroy();
        this.fire("measurementDestroyed", measurement);
    }

    /**
     * Shows all or hides the angle label of each {@link AngleMeasurement}.
     *
     * @param {Boolean} labelsShown Whether or not to show the labels.
     */
    setLabelsShown(labelsShown) {
        for (const [key, measurement] of Object.entries(this.measurements)) {
            measurement.labelShown = labelsShown;
        }
    }

    /**
     * Destroys all {@link AngleMeasurement}s.
     */
    clear() {
        const ids = Object.keys(this._measurements);
        for (var i = 0, len = ids.length; i < len; i++) {
            this.destroyMeasurement(ids[i]);
        }
    }

    /**
     * Destroys this AngleMeasurementsPlugin.
     *
     * Destroys all {@link AngleMeasurement}s first.
     */
    destroy() {
        this.clear();
        super.destroy();
    }
}

math.vec2();

math.vec3();
math.vec3();
math.vec3();

const tempVec3$2$1 = math.vec3();
const tempVec3a$5 = math.vec3();
const tempVec3b$2 = math.vec3();
const tempVec3c$1 = math.vec3();

/**
 * {@link Viewer} plugin that saves and loads BCF viewpoints as JSON objects.
 *
 * [<img src="http://xeokit.github.io/xeokit-sdk/assets/images/BCFViewpointsPlugin.png">](/examples/index.html#BCF_SaveViewpoint)
 *
 * * [[Example 1: Saving viewer state to a BCF viewpoint](https://xeokit.github.io/xeokit-sdk/examples/index.html#BCF_SaveViewpoint)]
 * * [[Example 2: Loading viewer state from a BCF viewpoint](https://xeokit.github.io/xeokit-sdk/examples/index.html#BCF_LoadViewpoint)]
 *
 * ## Overview
 *
 * BCF is an open standard that enables workflow communications between BIM software tools. An XML schema, called
 * Building Collaboration Format (BCF), encodes messages that inform one BIM tool of issues found by another.
 *
 * A BCF viewpoint captures a viewpoint of a model that highlights an issue. The viewpoint can then be loaded by another
 * viewer to examine the issue.
 *
 * Using this plugin, a xeokit {@link Viewer} can exchange BCF-encoded viewpoints with other BIM software,
 * allowing us to use the Viewer to report and view issues in BIM models.
 *
 * This plugin's viewpoints conform to the <a href="https://github.com/buildingSMART/BCF-API">BCF Version 2.1</a> specification.
 *
 * ## Supported BCF Elements
 *
 * BCFViewpointsPlugin saves and loads the following state in BCF viewpoints:
 *
 * * {@link Camera} position, orientation and projection
 * * {@link Entity} visibilities and selection states
 * * {@link SectionPlane}s to slice the model
 * * {@link LineSet}s to show 3D lines
 * * {@link Bitmap}s to show images
 *
 * ## Saving a BCF Viewpoint
 *
 * In the example below we'll create a {@link Viewer}, load an ````.XKT```` model into it using an {@link XKTLoaderPlugin},
 * slice the model in half using a {@link SectionPlanesPlugin}, create a grid ground plane using a {@link LineSet} and a 2D
 * plan view using a {@link Bitmap}, then use a {@link BCFViewpointsPlugin#getViewpoint}
 * to save a viewpoint to JSON, which we'll log to the JavaScript developer console.
 *
 * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#BCF_SaveViewpoint)]
 *
 * ````javascript
 * import {Viewer, XKTLoaderPlugin, SectionPlanesPlugin,
 *      LineSet, Bitmap, buildGridGeometry, BCFViewpointsPlugin} from "xeokit-sdk.es.js";
 *
 * // Create a Viewer
 * const viewer = new Viewer({
 *      canvasId: "myCanvas",
 *      transparent: true
 * });
 *
 * // Set camera position and orientation
 * viewer.scene.camera.eye = [-48.93, 54.54, 50.41];
 * viewer.scene.camera.look = [0.55, -0.61, -0.55];
 * viewer.scene.camera.up = [0, -1, 0];
 * viewer.scene.camera.perspective.fov = 60;
 *
 * // Add a XKTLoaderPlugin
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * // Add a SectionPlanesPlugin
 * const sectionPlanes = new SectionPlanesPlugin(viewer);
 *
 * // Add a BCFViewpointsPlugin
 * const bcfViewpoints = new BCFViewpointsPlugin(viewer);
 *
 * // Load an .XKT model
 * const modelNode = xktLoader.load({
 *      id: "myModel",
 *      src: "./models/xkt/Schependomlaan.xkt",
 *      edges: true // Emphasise edges
 * });
 *
 * // Slice it in half
 * sectionPlanes.createSectionPlane({
 *      id: "myClip",
 *      pos: [0, 0, 0],
 *      dir: [0.5, 0.0, 0.5]
 * });
 *
 * // Create a bitmap
 * const bitmap = new Bitmap(viewer.scene, {
 *     src: "../assets/images/schependomlaanPlanView.png",
 *      visible: true,
 *      height: 24.0,
 *      pos: [-15, 0, -10],
 *      normal: [0, -1, 0],
 *      up: [0, 0, 1],
 *      collidable: false,
 *      opacity: 1.0,
 *      clippable: false,
 *      pickable: true
 *  });
 *
 * // Create a grid ground plane
 * const geometryArrays = buildGridGeometry({
 *      size: 60,
 *      divisions: 10
 *  });
 *
 * new LineSet(viewer.scene, {
 *      positions: geometryArrays.positions,
 *      indices: geometryArrays.indices,
 *      position: [10,0,10],
 *      clippable: false
 *  });
 *
 * // When model is loaded, select some objects and capture a BCF viewpoint to the console
 * modelNode.on("loaded", () => {
 *
 *      const scene = viewer.scene;
 *
 *      scene.setObjectsSelected([
 *          "3b2U496P5Ebhz5FROhTwFH",
 *          "2MGtJUm9nD$Re1_MDIv0g2",
 *          "3IbuwYOm5EV9Q6cXmwVWqd",
 *          "3lhisrBxL8xgLCRdxNG$2v",
 *          "1uDn0xT8LBkP15zQc9MVDW"
 *      ], true);
 *
 *      const viewpoint = bcfViewpoints.getViewpoint();
 *      const viewpointStr = JSON.stringify(viewpoint, null, 4);
 *
 *      console.log(viewpointStr);
 * });
 * ````
 *
 * The saved BCF viewpoint would look something like below. Note that some elements are truncated for brevity.
 *
 * ````json
 * {
 *      "perspective_camera": {
 *          "camera_view_point": { "x": -48.93, "y": 54.54, "z": 50.41 },
 *          "camera_direction": { "x": 0.55, "y": -0.61, "z": -0.55},
 *          "camera_up_vector": { "x": 0.37, "y": -0.41, "z": 0.83 },
 *          "field_of_view": 60.0
 *      },
 *      "lines": [{
 *          "start_point": { "x": 1.0, "y": 1.0, "z": 1.0 },
 *          "end_point": { "x": 0.0, "y": 0.0, "z": 0.0 },
 *          //...(truncated)
 *      }],
 *      "bitmaps": [{
 *          "bitmap_type": "png",
 *          "bitmap_data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB9AAAAdp...", //...(truncated)
 *          "location": { "x": -15, "y": 10, "z": 0 },
 *          "normal": { "x": 0, "y": 0, "z": -1 },
 *          "up": { "x": 0, "y": -1, "z": 0 },
 *          "height": 24
 *      }],
 *      "clipping_planes": [{
 *          "location": { "x": 0.0, "y": 0.0, "z": 0.0 },
 *          "direction": { "x": 0.5, "y": 0.0, "z": 0.5 }
 *      }],
 *      "snapshot": {
 *          "snapshot_type": "png",
 *          "snapshot_data": "data:image/png;base64,......"
 *      },
 *      "components": {
 *          "visibility": {
 *              "default_visibility": false,
 *              "exceptions": [{
 *                      "ifc_guid": "4$cshxZO9AJBebsni$z9Yk",
 *                      "originating_system": "xeokit.io",
 *                      "authoring_tool_id": "xeokit/v3.2"
 *                  },
 *                  //...
 *              ]
 *          },
 *          "selection": [{
 *                  "ifc_guid": "4$cshxZO9AJBebsni$z9Yk",
 *              },
 *              //...
 *          ]
 *      }
 * }
 * ````
 *
 * ## Saving View Setup Hints
 *
 * BCFViewpointsPlugin can optionally save hints in the viewpoint, which indicate how to set up the view when
 * loading it again.
 *
 * Here's the {@link BCFViewpointsPlugin#getViewpoint} call again, this time saving some hints:
 *
 * ````javascript
 * const viewpoint = bcfViewpoints.getViewpoint({ // Options
 *     spacesVisible: true, // Force IfcSpace types visible in the viewpoint (default is false)
 *     spaceBoundariesVisible: false, // Show IfcSpace boundaries in the viewpoint (default is false)
 *     openingsVisible: true // Force IfcOpening types visible in the viewpoint (default is false)
 * });
 * ````
 *
 * ## Loading a BCF Viewpoint
 *
 * Assuming that we have our BCF viewpoint in a JSON object, let's now restore it with {@link BCFViewpointsPlugin#setViewpoint}:
 *
 * ````javascript
 * bcfViewpoints.setViewpoint(viewpoint);
 * ````
 *
 * ## Handling BCF Incompatibility with xeokit's Camera
 *
 * xeokit's {@link Camera#look} is the current 3D *point-of-interest* (POI).
 *
 * A BCF viewpoint, however, has a direction vector instead of a POI, and so {@link BCFViewpointsPlugin#getViewpoint} saves
 * xeokit's POI as a normalized vector from {@link Camera#eye} to {@link Camera#look}, which unfortunately loses
 * that positional information. Loading the viewpoint with {@link BCFViewpointsPlugin#setViewpoint} will restore {@link Camera#look} to
 * the viewpoint's camera position, offset by the normalized vector.
 *
 * As shown below, providing a ````rayCast```` option to ````setViewpoint```` will set {@link Camera#look} to the closest
 * surface intersection on the direction vector. Internally, ````setViewpoint```` supports this option by firing a ray
 * along the vector, and if that hits an {@link Entity}, sets {@link Camera#look} to ray's intersection point with the
 * Entity's surface.
 *
 * ````javascript
 * bcfViewpoints.setViewpoint(viewpoint, {
 *      rayCast: true // <<--------------- Attempt to set Camera#look to surface intersection point (default)
 * });
 * ````
 *
 * ## Dealing With Loaded Models that are not in the Viewpoint
 *
 * If, for example, we load model "duplex", hide some objects, then save a BCF viewpoint with
 * ````BCFViewpointsPlugin#getViewpoint````, then load another model, "schependomlaan", then load the viewpoint again
 * with ````BCFViewpointsPlugin#setViewpoint````, then sometimes all of the objects in model "schependomlaan" become
 * visible, along with the visible objects in the viewpoint, which belong to model "duplex".
 *
 * The reason is that, when saving a BCF viewpoint, BCF logic works like the following pseudo code:
 *
 * ````
 * If numVisibleObjects < numInvisibleObjects
 *      save IDs of visible objects in BCF
 *      exceptions = "visible objects"
 * else
 *      save IDS of invisible objects in BCF
 *      exceptions = "invisible objects"
 * ````
 *
 * When loading the viewpoint again:
 *
 * ````
 * If exceptions = "visible objects"
 *      hide all objects
 *      show visible objects in BCF
 * else
 *      show all objects
 *      hide invisible objects in BCF
 * ````
 *
 * When the exception is "visible objects", loading the viewpoint shows all the objects in the first, which includes
 * objects in "schependomlaan", which can be confusing, because those were not even loaded when we first
 * saved the viewpoint..
 *
 * To solve this, we can supply a ````defaultInvisible```` option to {@link BCFViewpointsPlugin#getViewpoint}, which
 * will force the plugin to save the IDs of all visible objects while making invisible objects the exception.
 *
 * That way, when we load the viewpoint again, after loading model "schependomlaan", the plugin will hide all objects
 * in the scene first (which will include objects belonging to model "schependomlaan"), then make the objects in the
 * viewpoint visible (which will only be those of object "duplex").
 *
 * ````javascript
 * const viewpoint = bcfViewpoints.getViewpoint({ // Options
 *     //..
 *     defaultInvisible: true
 * });
 * ````
 *
 * [[Run an example](/examples/index.html#BCF_LoadViewpoint_defaultInvisible)]
 *
 * ## Behaviour with XKTLoaderPlugin globalizeObjectIds
 *
 * Whenever we use {@link XKTLoaderPlugin} to load duplicate copies of the same model, after configuring
 * {@link XKTLoaderPlugin#globalizeObjectIds} ````true```` to avoid ````Entity```` ID clashes, this has consequences
 * for BCF viewpoints created by {@link BCFViewpointsPlugin#getViewpoint}.
 *
 * When no duplicate copies of a model are loaded like this, viewpoints created by {@link BCFViewpointsPlugin#getViewpoint} will
 * continue to load as usual in other BIM viewers. Conversely, a viewpoint created for a single model in other BIM viewers
 * will continue to load as usual with ````BCFViewpointsPlugin````.
 *
 * When duplicate copies of a model are loaded, however, viewpoints created by {@link BCFViewpointsPlugin#getViewpoint}
 * will contain certain changes that will affect the viewpoint's portability, however. Such viewpoints will
 * use ````authoring_tool_id```` fields to save the globalized ````Entity#id```` values, which enables the viewpoints to
 * capture the states of the individual ````Entitys```` that represent the duplicate IFC elements. Take a look at the
 * following two examples to learn more.
 *
 * * [Example: Saving a BCF viewpoint containing duplicate models](https://xeokit.github.io/xeokit-sdk/examples/index.html#BCF_SaveViewpoint_MultipleModels)
 * * [Example: Loading a BCF viewpoint containing duplicate models](https://xeokit.github.io/xeokit-sdk/examples/index.html#BCF_LoadViewpoint_MultipleModels)
 *
 * **Caveat:** when loading a BCF viewpoint, we always assume that we have loaded in our target BIM viewer the same models that were
 * loaded in the viewpoint's original authoring application when the viewpoint was created.  In the case of multi-model
 * viewpoints, the target BIM viewer, whether it be xeokit or another BIM viewer, will need to first have those exact
 * models loaded, with their objects having globalized IDs, following the same prefixing scheme we're using in
 * xeokit. Then, the viewpoint's ````authoring_tool_id```` fields will be able to resolve to their objects within the
 * target viewer.
 *
 * @class BCFViewpointsPlugin
 */
class BCFViewpointsPlugin extends Plugin {

    /**
     * @constructor
     * @param {Viewer} viewer The Viewer.
     * @param {Object} cfg  Plugin configuration.
     * @param {String} [cfg.id="BCFViewpoints"] Optional ID for this plugin, so that we can find it within {@link Viewer#plugins}.
     * @param {String} [cfg.originatingSystem] Identifies the originating system for BCF records.
     * @param {String} [cfg.authoringTool] Identifies the authoring tool for BCF records.
     */
    constructor(viewer, cfg = {}) {

        super("BCFViewpoints", viewer, cfg);

        /**
         * Identifies the originating system to include in BCF viewpoints saved by this plugin.
         * @property originatingSystem
         * @type {string}
         */
        this.originatingSystem = cfg.originatingSystem || "xeokit.io";

        /**
         * Identifies the authoring tool to include in BCF viewpoints saved by this plugin.
         * @property authoringTool
         * @type {string}
         */
        this.authoringTool = cfg.authoringTool || "xeokit.io";
    }

    /**
     * Saves viewer state to a BCF viewpoint.
     *
     * See ````BCFViewpointsPlugin```` class comments for more info.
     *
     * @param {*} [options] Options for getting the viewpoint.
     * @param {Boolean} [options.spacesVisible=false] Indicates whether ````IfcSpace```` types should be forced visible in the viewpoint.
     * @param {Boolean} [options.openingsVisible=false] Indicates whether ````IfcOpening```` types should be forced visible in the viewpoint.
     * @param {Boolean} [options.spaceBoundariesVisible=false] Indicates whether the boundaries of ````IfcSpace```` types should be visible in the viewpoint.
     * @param {Boolean} [options.spacesTranslucent=false] Indicates whether ````IfcSpace```` types should be forced translucent in the viewpoint.
     * @param {Boolean} [options.spaceBoundariesTranslucent=false] Indicates whether the boundaries of ````IfcSpace```` types should be forced translucent in the viewpoint.
     * @param {Boolean} [options.openingsTranslucent=true] Indicates whether ````IfcOpening```` types should be forced translucent in the viewpoint.
     * @param {Boolean} [options.snapshot=true] Indicates whether the snapshot should be included in the viewpoint.
     * @param {Boolean} [options.defaultInvisible=false] When ````true````, will save the default visibility of all objects
     * as ````false````. This means that when we load the viewpoint again, and there are additional models loaded that
     * were not saved in the viewpoint, those models will be hidden when we load the viewpoint, and that only the
     * objects in the viewpoint will be visible.
     * @param {Boolean} [options.reverseClippingPlanes=false] When ````true````, clipping planes are reversed (https://github.com/buildingSMART/BCF-XML/issues/193)
     * @returns {*} BCF JSON viewpoint object
     */
    getViewpoint(options = {}) {
        const scene = this.viewer.scene;
        const camera = scene.camera;
        const realWorldOffset = scene.realWorldOffset;
        const reverseClippingPlanes = (options.reverseClippingPlanes === true);
        let bcfViewpoint = {};

        // Camera
        let lookDirection = math.normalizeVec3(math.subVec3(camera.look, camera.eye, math.vec3()));
        let eye = camera.eye;
        let up = camera.up;

        if (camera.yUp) {
            // BCF is Z up
            lookDirection = YToZ(lookDirection);
            eye = YToZ(eye);
            up = YToZ(up);
        }

        const camera_view_point = xyzArrayToObject(math.addVec3(eye, realWorldOffset));

        if (camera.projection === "ortho") {
            bcfViewpoint.orthogonal_camera = {
                camera_view_point: camera_view_point,
                camera_direction: xyzArrayToObject(lookDirection),
                camera_up_vector: xyzArrayToObject(up),
                view_to_world_scale: camera.ortho.scale,
            };
        } else {
            bcfViewpoint.perspective_camera = {
                camera_view_point: camera_view_point,
                camera_direction: xyzArrayToObject(lookDirection),
                camera_up_vector: xyzArrayToObject(up),
                field_of_view: camera.perspective.fov,
            };
        }

        // Section planes

        const sectionPlanes = scene.sectionPlanes;
        for (let id in sectionPlanes) {
            if (sectionPlanes.hasOwnProperty(id)) {
                let sectionPlane = sectionPlanes[id];
                if (!sectionPlane.active) {
                    continue;
                }
                let location = sectionPlane.pos;

                let direction;
                if (reverseClippingPlanes) {
                    direction = math.negateVec3(sectionPlane.dir, math.vec3());
                } else {
                    direction = sectionPlane.dir;
                }

                if (camera.yUp) {
                    // BCF is Z up
                    location = YToZ(location);
                    direction = YToZ(direction);
                }
                math.addVec3(location, realWorldOffset);

                location = xyzArrayToObject(location);
                direction = xyzArrayToObject(direction);
                if (!bcfViewpoint.clipping_planes) {
                    bcfViewpoint.clipping_planes = [];
                }
                bcfViewpoint.clipping_planes.push({location, direction});
            }
        }

        // Lines

        const lineSets = scene.lineSets;
        for (let id in lineSets) {
            if (lineSets.hasOwnProperty(id)) {
                const lineSet = lineSets[id];
                if (!bcfViewpoint.lines) {
                    bcfViewpoint.lines = [];
                }
                const positions = lineSet.positions;
                const indices = lineSet.indices;
                for (let i = 0, len = indices.length / 2; i < len; i++) {
                    const a = indices[i * 2];
                    const b = indices[(i * 2) + 1];
                    bcfViewpoint.lines.push({
                        start_point: {
                            x: positions[a * 3 + 0],
                            y: positions[a * 3 + 1],
                            z: positions[a * 3 + 2]
                        },
                        end_point: {
                            x: positions[b * 3 + 0],
                            y: positions[b * 3 + 1],
                            z: positions[b * 3 + 2]
                        }
                    });
                }

            }
        }

        // Bitmaps

        const bitmaps = scene.bitmaps;
        for (let id in bitmaps) {
            if (bitmaps.hasOwnProperty(id)) {
                let bitmap = bitmaps[id];
                let location = bitmap.pos;
                let normal = bitmap.normal;
                let up = bitmap.up;
                if (camera.yUp) {
                    // BCF is Z up
                    location = YToZ(location);
                    normal = YToZ(normal);
                    up = YToZ(up);
                }
                math.addVec3(location, realWorldOffset);
                if (!bcfViewpoint.bitmaps) {
                    bcfViewpoint.bitmaps = [];
                }
                bcfViewpoint.bitmaps.push({
                    bitmap_type: bitmap.type,
                    bitmap_data: bitmap.imageData,
                    location: xyzArrayToObject(location),
                    normal: xyzArrayToObject(normal),
                    up: xyzArrayToObject(up),
                    height: bitmap.height
                });
            }
        }

        // Entity states

        bcfViewpoint.components = {
            visibility: {
                view_setup_hints: {
                    spaces_visible: !!options.spacesVisible,
                    space_boundaries_visible: !!options.spaceBoundariesVisible,
                    openings_visible: !!options.openingsVisible,
                    spaces_translucent: !!options.spaces_translucent,
                    space_boundaries_translucent: !!options.space_boundaries_translucent,
                    openings_translucent: !!options.openings_translucent
                }
            }
        };

        const opacityObjectIds = new Set(scene.opacityObjectIds);
        const xrayedObjectIds = new Set(scene.xrayedObjectIds);
        const colorizedObjectIds = new Set(scene.colorizedObjectIds);

        const coloringMap = Object.values(scene.objects)
            .filter(entity => opacityObjectIds.has(entity.id) || colorizedObjectIds.has(entity.id) || xrayedObjectIds.has(entity.id))
            .reduce((coloringMap, entity) => {

                let color = colorizeToRGB(entity.colorize);
                let alpha;

                if (entity.xrayed) {
                    if (scene.xrayMaterial.fillAlpha === 0.0 && scene.xrayMaterial.edgeAlpha !== 0.0) {
                        // BCF can't deal with edges. If xRay is implemented only with edges, set an arbitrary opacity
                        alpha = 0.1;
                    } else {
                        alpha = scene.xrayMaterial.fillAlpha;
                    }
                    alpha = Math.round(alpha * 255).toString(16).padStart(2, "0");
                    color = alpha + color;
                } else if (opacityObjectIds.has(entity.id)) {
                    alpha = Math.round(entity.opacity * 255).toString(16).padStart(2, "0");
                    color = alpha + color;
                }

                if (!coloringMap[color]) {
                    coloringMap[color] = [];
                }

                const objectId = entity.id;
                const originalSystemId = entity.originalSystemId;
                const component = {
                    ifc_guid: originalSystemId,
                    originating_system: this.originatingSystem
                };
                if (originalSystemId !== objectId) {
                    component.authoring_tool_id = objectId;
                }

                coloringMap[color].push(component);

                return coloringMap;

            }, {});

        const coloringArray = Object.entries(coloringMap).map(([color, components]) => {
            return {color, components};
        });

        bcfViewpoint.components.coloring = coloringArray;

        const objectIds = scene.objectIds;
        const visibleObjects = scene.visibleObjects;
        const visibleObjectIds = scene.visibleObjectIds;
        const invisibleObjectIds = objectIds.filter(id => !visibleObjects[id]);
        const selectedObjectIds = scene.selectedObjectIds;

        if (options.defaultInvisible || visibleObjectIds.length < invisibleObjectIds.length) {
            bcfViewpoint.components.visibility.exceptions = this._createBCFComponents(visibleObjectIds);
            bcfViewpoint.components.visibility.default_visibility = false;
        } else {
            bcfViewpoint.components.visibility.exceptions = this._createBCFComponents(invisibleObjectIds);
            bcfViewpoint.components.visibility.default_visibility = true;
        }

        bcfViewpoint.components.selection = this._createBCFComponents(selectedObjectIds);

        bcfViewpoint.components.translucency = this._createBCFComponents(scene.xrayedObjectIds);

        if (options.snapshot !== false) {
            bcfViewpoint.snapshot = {
                snapshot_type: "png",
                snapshot_data: this.viewer.getSnapshot({format: "png"})
            };
        }

        return bcfViewpoint;
    }

    _createBCFComponents(objectIds) {
        const scene = this.viewer.scene;
        const components = [];
        for (let i = 0, len = objectIds.length; i < len; i++) {
            const objectId = objectIds[i];
            const entity = scene.objects[objectId];
            if (entity) {
                const component = {
                    ifc_guid: entity.originalSystemId,
                    originating_system: this.originatingSystem
                };
                if (entity.originalSystemId !== objectId) {
                    component.authoring_tool_id = objectId;
                }
                components.push(component);
            }
        }
        return components;
    }

    /**
     * Sets viewer state to the given BCF viewpoint.
     *
     * Note that xeokit's {@link Camera#look} is the **point-of-interest**, whereas the BCF ````camera_direction```` is a
     * direction vector. Therefore, when loading a BCF viewpoint, we set {@link Camera#look} to the absolute position
     * obtained by offsetting the BCF ````camera_view_point````  along ````camera_direction````.
     *
     * When loading a viewpoint, we also have the option to find {@link Camera#look} as the closest point of intersection
     * (on the surface of any visible and pickable {@link Entity}) with a 3D ray fired from ````camera_view_point```` in
     * the direction of ````camera_direction````.
     *
     * @param {*} bcfViewpoint  BCF JSON viewpoint object,
     * shows default visible entities and restores camera to initial default position.
     * @param {*} [options] Options for setting the viewpoint.
     * @param {Boolean} [options.rayCast=true] When ````true```` (default), will attempt to set {@link Camera#look} to the closest
     * point of surface intersection with a ray fired from the BCF ````camera_view_point```` in the direction of ````camera_direction````.
     * @param {Boolean} [options.immediate=true] When ````true```` (default), immediately set camera position.
     * @param {Boolean} [options.duration] Flight duration in seconds.  Overrides {@link CameraFlightAnimation#duration}. Only applies when ````immediate```` is ````false````.
     * @param {Boolean} [options.reset=true] When ````true```` (default), set {@link Entity#xrayed} and {@link Entity#highlighted} ````false```` on all scene objects.
     * @param {Boolean} [options.reverseClippingPlanes=false] When ````true````, clipping planes are reversed (https://github.com/buildingSMART/BCF-XML/issues/193)
     * @param {Boolean} [options.updateCompositeObjects=false] When ````true````, then when visibility and selection updates refer to composite objects (eg. an IfcBuildingStorey),
     * then this method will apply the updates to objects within those composites.
     */
    setViewpoint(bcfViewpoint, options = {}) {
        if (!bcfViewpoint) {
            return;
        }

        const viewer = this.viewer;
        const scene = viewer.scene;
        const camera = scene.camera;
        const rayCast = (options.rayCast !== false);
        const immediate = (options.immediate !== false);
        const reset = (options.reset !== false);
        const realWorldOffset = scene.realWorldOffset;
        const reverseClippingPlanes = (options.reverseClippingPlanes === true);

        scene.clearSectionPlanes();

        if (bcfViewpoint.clipping_planes && bcfViewpoint.clipping_planes.length > 0) {
            bcfViewpoint.clipping_planes.forEach(function (e) {
                let pos = xyzObjectToArray(e.location, tempVec3$2$1);
                let dir = xyzObjectToArray(e.direction, tempVec3$2$1);

                if (reverseClippingPlanes) {
                    math.negateVec3(dir);
                }
                math.subVec3(pos, realWorldOffset);

                if (camera.yUp) {
                    pos = ZToY(pos);
                    dir = ZToY(dir);
                }
                new SectionPlane(scene, {pos, dir});
            });
        }

        scene.clearLines();

        if (bcfViewpoint.lines && bcfViewpoint.lines.length > 0) {
            const positions = [];
            const indices = [];
            let i = 0;
            bcfViewpoint.lines.forEach((e) => {
                if (!e.start_point) {
                    return;
                }
                if (!e.end_point) {
                    return;
                }
                positions.push(e.start_point.x);
                positions.push(e.start_point.y);
                positions.push(e.start_point.z);
                positions.push(e.end_point.x);
                positions.push(e.end_point.y);
                positions.push(e.end_point.z);
                indices.push(i++);
                indices.push(i++);
            });
            new LineSet(scene, {
                positions,
                indices,
                clippable: false,
                collidable: true
            });
        }

        scene.clearBitmaps();

        if (bcfViewpoint.bitmaps && bcfViewpoint.bitmaps.length > 0) {
            bcfViewpoint.bitmaps.forEach(function (e) {
                const bitmap_type = e.bitmap_type || "jpg"; // "jpg" | "png"
                const bitmap_data = e.bitmap_data; // base64
                let location = xyzObjectToArray(e.location, tempVec3a$5);
                let normal = xyzObjectToArray(e.normal, tempVec3b$2);
                let up = xyzObjectToArray(e.up, tempVec3c$1);
                let height = e.height || 1;
                if (!bitmap_data) {
                    return;
                }
                if (!location) {
                    return;
                }
                if (!normal) {
                    return;
                }
                if (!up) {
                    return;
                }
                if (camera.yUp) {
                    location = ZToY(location);
                    normal = ZToY(normal);
                    up = ZToY(up);
                }
                new Bitmap(scene, {
                    src: bitmap_data,
                    type: bitmap_type,
                    pos: location,
                    normal: normal,
                    up: up,
                    clippable: false,
                    collidable: true,
                    height
                });
            });
        }

        if (reset) {
            scene.setObjectsXRayed(scene.xrayedObjectIds, false);
            scene.setObjectsHighlighted(scene.highlightedObjectIds, false);
            scene.setObjectsSelected(scene.selectedObjectIds, false);
        }

        if (bcfViewpoint.components) {

            if (bcfViewpoint.components.visibility) {

                if (!bcfViewpoint.components.visibility.default_visibility) {
                    scene.setObjectsVisible(scene.objectIds, false);
                    if (bcfViewpoint.components.visibility.exceptions) {
                        bcfViewpoint.components.visibility.exceptions.forEach((component) => this._withBCFComponent(options, component, entity => entity.visible = true));
                    }
                } else {
                    scene.setObjectsVisible(scene.objectIds, true);
                    if (bcfViewpoint.components.visibility.exceptions) {
                        bcfViewpoint.components.visibility.exceptions.forEach((component) => this._withBCFComponent(options, component, entity => entity.visible = false));
                    }
                }

                const view_setup_hints = bcfViewpoint.components.visibility.view_setup_hints;
                if (view_setup_hints) {
                    if (view_setup_hints.spaces_visible === false) {
                        scene.setObjectsVisible(viewer.metaScene.getObjectIDsByType("IfcSpace"), false);
                    }
                    if (view_setup_hints.spaces_translucent !== undefined) {
                        scene.setObjectsXRayed(viewer.metaScene.getObjectIDsByType("IfcSpace"), true);
                    }
                    if (view_setup_hints.space_boundaries_visible !== undefined) ;
                    if (view_setup_hints.openings_visible === false) {
                        scene.setObjectsVisible(viewer.metaScene.getObjectIDsByType("IfcOpening"), true);
                    }
                    if (view_setup_hints.space_boundaries_translucent !== undefined) ;
                    if (view_setup_hints.openings_translucent !== undefined) {
                        scene.setObjectsXRayed(viewer.metaScene.getObjectIDsByType("IfcOpening"), true);
                    }
                }
            }

            if (bcfViewpoint.components.selection) {
                scene.setObjectsSelected(scene.selectedObjectIds, false);
                bcfViewpoint.components.selection.forEach(component => this._withBCFComponent(options, component, entity => entity.selected = true));

            }

            if (bcfViewpoint.components.translucency) {
                scene.setObjectsXRayed(scene.xrayedObjectIds, false);
                bcfViewpoint.components.translucency.forEach(component => this._withBCFComponent(options, component, entity => entity.xrayed = true));
            }

            if (bcfViewpoint.components.coloring) {
                bcfViewpoint.components.coloring.forEach(coloring => {

                    let color = coloring.color;
                    let alpha = 0;
                    let alphaDefined = false;

                    if (color.length === 8) {
                        alpha = parseInt(color.substring(0, 2), 16) / 256;
                        if (alpha <= 1.0 && alpha >= 0.95) {
                            alpha = 1.0;
                        }
                        color = color.substring(2);
                        alphaDefined = true;
                    }

                    const colorize = [
                        parseInt(color.substring(0, 2), 16) / 256,
                        parseInt(color.substring(2, 4), 16) / 256,
                        parseInt(color.substring(4, 6), 16) / 256
                    ];

                    coloring.components.map(component =>
                        this._withBCFComponent(options, component, entity => {
                            entity.colorize = colorize;
                            if (alphaDefined) {
                                entity.opacity = alpha;
                            }
                        }));
                });
            }
        }

        if (bcfViewpoint.perspective_camera || bcfViewpoint.orthogonal_camera) {
            let eye;
            let look;
            let up;
            let projection;

            if (bcfViewpoint.perspective_camera) {
                eye = xyzObjectToArray(bcfViewpoint.perspective_camera.camera_view_point, tempVec3$2$1);
                look = xyzObjectToArray(bcfViewpoint.perspective_camera.camera_direction, tempVec3$2$1);
                up = xyzObjectToArray(bcfViewpoint.perspective_camera.camera_up_vector, tempVec3$2$1);

                camera.perspective.fov = bcfViewpoint.perspective_camera.field_of_view;

                projection = "perspective";
            } else {
                eye = xyzObjectToArray(bcfViewpoint.orthogonal_camera.camera_view_point, tempVec3$2$1);
                look = xyzObjectToArray(bcfViewpoint.orthogonal_camera.camera_direction, tempVec3$2$1);
                up = xyzObjectToArray(bcfViewpoint.orthogonal_camera.camera_up_vector, tempVec3$2$1);

                camera.ortho.scale = bcfViewpoint.orthogonal_camera.view_to_world_scale;

                projection = "ortho";
            }

            math.subVec3(eye, realWorldOffset);

            if (camera.yUp) {
                eye = ZToY(eye);
                look = ZToY(look);
                up = ZToY(up);
            }

            if (rayCast) {
                const hit = scene.pick({
                    pickSurface: true,  // <<------ This causes picking to find the intersection point on the entity
                    origin: eye,
                    direction: look
                });
                look = (hit ? hit.worldPos : math.addVec3(eye, look, tempVec3$2$1));
            } else {
                look = math.addVec3(eye, look, tempVec3$2$1);
            }

            if (immediate) {
                camera.eye = eye;
                camera.look = look;
                camera.up = up;
                camera.projection = projection;
            } else {
                viewer.cameraFlight.flyTo({eye, look, up, duration: options.duration, projection});
            }
        }
    }

    _withBCFComponent(options, component, callback) {

        const viewer = this.viewer;
        const scene = viewer.scene;

        if (component.authoring_tool_id && component.originating_system === this.originatingSystem) {

            const id = component.authoring_tool_id;
            const entity = scene.objects[id];

            if (entity) {
                callback(entity);
                return
            }

            if (options.updateCompositeObjects) {
                const metaObject = viewer.metaScene.metaObjects[id];
                if (metaObject) {
                    scene.withObjects(viewer.metaScene.getObjectIDsInSubtree(id), callback);
                    return;
                }
            }
        }

        if (component.ifc_guid) {

            const originalSystemId = component.ifc_guid;
            const entity = scene.objects[originalSystemId];

            if (entity) {
                callback(entity);
                return;
            }

            if (options.updateCompositeObjects) {
                const metaObject = viewer.metaScene.metaObjects[originalSystemId];
                if (metaObject) {
                    scene.withObjects(viewer.metaScene.getObjectIDsInSubtree(originalSystemId), callback);
                    return;
                }
            }

            Object.keys(scene.models).forEach((modelId) => {

                const id = math.globalizeObjectId(modelId, originalSystemId);
                const entity = scene.objects[id];

                if (entity) {
                    callback(entity);
                    return;
                }

                if (options.updateCompositeObjects) {
                    const metaObject = viewer.metaScene.metaObjects[id];
                    if (metaObject) {
                        scene.withObjects(viewer.metaScene.getObjectIDsInSubtree(id), callback);

                    }
                }
            });
        }
    }

    /**
     * Destroys this BCFViewpointsPlugin.
     */
    destroy() {
        super.destroy();
    }
}

function xyzArrayToObject(arr) {
    return {"x": arr[0], "y": arr[1], "z": arr[2]};
}

function xyzObjectToArray(xyz, arry) {
    arry = new Float64Array(3);
    arry[0] = xyz.x;
    arry[1] = xyz.y;
    arry[2] = xyz.z;
    return arry;
}

function YToZ(vec) {
    return new Float64Array([vec[0], -vec[2], vec[1]]);
}

function ZToY(vec) {
    return new Float64Array([vec[0], vec[2], -vec[1]]);
}

function colorizeToRGB(color) {
    let rgb = "";
    rgb += Math.round(color[0] * 255).toString(16).padStart(2, "0");
    rgb += Math.round(color[1] * 255).toString(16).padStart(2, "0");
    rgb += Math.round(color[2] * 255).toString(16).padStart(2, "0");
    return rgb;
}

const tmpVec3a = math.vec3();
const tmpVec3b = math.vec3();
const tmpVec3c = math.vec3();
math.vec4();
math.vec4();
math.vec4();

/**
 * @desc Measures the distance between two 3D points.
 *
 * See {@link DistanceMeasurementsPlugin} for more info.
 */
class DistanceMeasurement extends Component {

    /**
     * @private
     */
    constructor(plugin, cfg = {}) {

        const scene = plugin.viewer.scene;

        super(scene, cfg);

        /**
         * The {@link DistanceMeasurementsPlugin} that owns this DistanceMeasurement.
         * @type {DistanceMeasurementsPlugin}
         */
        this.plugin = plugin;

        const container = cfg.container;
        if (!container) {
            throw "config missing: container";
        }

        this._color = cfg.color || plugin.defaultColor;

        const channel = function(v, defaultIfUndefined) {
            const listeners = [ ];
            let value = v !== undefined ? Boolean(v) : defaultIfUndefined;
            return {
                reg: (l) => listeners.push(l),
                get: () => value,
                set: (v) => {
                    value = v !== undefined ? Boolean(v) : defaultIfUndefined;
                    listeners.forEach(l => l(value));
                }
            };
        };

        this._visible               = channel(cfg.visible,               plugin.defaultVisible);
        this._originVisible         = channel(cfg.originVisible,         plugin.defaultOriginVisible);
        this._targetVisible         = channel(cfg.targetVisible,         plugin.defaultTargetVisible);
        this._axisVisible           = channel(cfg.axisVisible,           plugin.defaultAxisVisible);
        this._xAxisVisible          = channel(cfg.xAxisVisible,          plugin.defaultAxisVisible);
        this._yAxisVisible          = channel(cfg.yAxisVisible,          plugin.defaultAxisVisible);
        this._zAxisVisible          = channel(cfg.zAxisVisible,          plugin.defaultAxisVisible);
        this._axisEnabled           = channel(true,                      plugin.defaultAxisVisible);
        this._wireVisible           = channel(cfg.wireVisible,           plugin.defaultWireVisible);
        this._xLabelEnabled         = channel(cfg.xLabelEnabled,         plugin.defaultXLabelEnabled);
        this._yLabelEnabled         = channel(cfg.yLabelEnabled,         plugin.defaultYLabelEnabled);
        this._zLabelEnabled         = channel(cfg.zLabelEnabled,         plugin.defaultZLabelEnabled);
        this._lengthLabelEnabled    = channel(cfg.lengthLabelEnabled,    plugin.defaultLengthLabelEnabled);
        this._labelsVisible         = channel(cfg.labelsVisible,         plugin.defaultLabelsVisible);
        this._clickable             = channel(false,                     false);
        this._labelsOnWires         = channel(cfg.labelsOnWires,         plugin.defaultLabelsOnWires);
        this._useRotationAdjustment = channel(cfg.useRotationAdjustment, plugin.useRotationAdjustment);

        this._axesBasis = math.identityMat4();
        this.approximate = cfg.approximate;


        const canvas = scene.canvas.canvas;

        const onMouseOver = cfg.onMouseOver ? (event) => {
            cfg.onMouseOver(event, this);
            canvas.dispatchEvent(new MouseEvent('mouseover', event));
        } : null;

        const onMouseLeave = cfg.onMouseLeave ? (event) => {
            cfg.onMouseLeave(event, this);
            canvas.dispatchEvent(new MouseEvent('mouseleave', event));
        } : null;

        const onContextMenu = cfg.onContextMenu ? (event) => {
            cfg.onContextMenu(event, this);
        } : null;

        const onMouseDown  = (event) => canvas.dispatchEvent(new MouseEvent('mousedown', event));
        const onMouseUp    = (event) => canvas.dispatchEvent(new MouseEvent('mouseup', event));
        const onMouseMove  = (event) => canvas.dispatchEvent(new MouseEvent('mousemove', event));
        const onMouseWheel = (event) => canvas.dispatchEvent(new WheelEvent('wheel', event));


        this._cleanups = [ ];

        [ "units", "scale" ].forEach(evt => {
            const handler = scene.metrics.on("units", () => this._update());
            this._cleanups.push(() => scene.metrics.off(handler));
        });

        this._drawables = [ ];

        const registerDrawable = (drawable, visibilityChannels) => {
            const updateVisibility = () => drawable.setVisible(visibilityChannels.every(ch => ch.get()));
            visibilityChannels.forEach(ch => ch.reg(updateVisibility));
            this._drawables.push(drawable);
            this._cleanups.push(() => drawable.destroy());
        };

        const makeWire = (color, thickness, visibilityChannels) => {
            const wire = new Wire3D(scene, container, {
                color: color,
                thickness: thickness,
                thicknessClickable: 6,
                zIndex: plugin.zIndex !== undefined ? plugin.zIndex + 1 : undefined,
                onMouseOver,
                onMouseLeave,
                onMouseWheel,
                onMouseDown,
                onMouseUp,
                onMouseMove,
                onContextMenu
            });
            registerDrawable(wire, visibilityChannels);
            return {
                setEnds: (p0, p1) => wire.setEnds(p0, p1),
                setColor: value => wire.setColor(value)
            };
        };
        this._lengthWire = makeWire(this._color, 2, [ this._visible, this._wireVisible ]);
        this._xAxisWire  = makeWire("red",       1, [ this._visible, this._axisEnabled, this._axisVisible, this._xAxisVisible ]);
        this._yAxisWire  = makeWire("green",     1, [ this._visible, this._axisEnabled, this._axisVisible, this._yAxisVisible ]);
        this._zAxisWire  = makeWire("blue",      1, [ this._visible, this._axisEnabled, this._axisVisible, this._zAxisVisible ]);

        const makeLabel = (color, zIndexOffset, visibilityChannels) => {
            const label = new Label3D(scene, container, {
                fillColor: color,
                zIndex: plugin.zIndex !== undefined ? plugin.zIndex + zIndexOffset : undefined,
                onMouseOver,
                onMouseLeave,
                onMouseWheel,
                onMouseDown,
                onMouseUp,
                onMouseMove,
                onContextMenu
            });
            registerDrawable(label, visibilityChannels);
            return {
                setFillColor:  value => label.setFillColor(value),
                setPosOnWire:  (p0, p1, offset, labelMinAxisLength) => label.setPosOnWire(p0, p1, offset, labelMinAxisLength),
                setPosBetween: (p0, p1, p2) => label.setPosBetween(p0, p1, p2),
                setText:       str => label.setText(str.replace(/ /g, "&nbsp;"))
            };
        };
        this._lengthLabel = makeLabel(this._color, 4, [ this._visible, this._wireVisible, this._labelsVisible, this._clickable, this._axisEnabled, this._lengthLabelEnabled ]);
        this._xAxisLabel  = makeLabel("red",       3, [ this._visible, this._axisEnabled, this._axisVisible, this._xAxisVisible, this._labelsVisible, this._clickable, this._xLabelEnabled ]);
        this._yAxisLabel  = makeLabel("green",     3, [ this._visible, this._axisEnabled, this._axisVisible, this._yAxisVisible, this._labelsVisible, this._clickable, this._yLabelEnabled ]);
        this._zAxisLabel  = makeLabel("blue",      3, [ this._visible, this._axisEnabled, this._axisVisible, this._zAxisVisible, this._labelsVisible, this._clickable, this._zLabelEnabled ]);

        const makeDot = (cfg, visibilityChannels) => {
            const dot = new Dot3D(scene, cfg, container, {
                fillColor: this._color,
                zIndex: plugin.zIndex !== undefined ? plugin.zIndex + 2 : undefined,
                onMouseOver,
                onMouseLeave,
                onMouseWheel,
                onMouseDown,
                onMouseUp,
                onMouseMove,
                onContextMenu
            });
            dot.on("worldPos", () => this._update());
            registerDrawable(dot, visibilityChannels);
            return dot;
        };
        this._originDot = makeDot(cfg.origin, [ this._visible, this._originVisible ]);
        this._targetDot = makeDot(cfg.target, [ this._visible, this._targetVisible ]);

        this._update();
    }

    _update() {
        if (! this._targetDot) {
            return;
        }

        const p0 = this._originDot.worldPos;
        const p1 = this._targetDot.worldPos;
        const axesBasis = this._axesBasis;
        const delta = math.subVec3(p1, p0, tmpVec3a);
        const factors = math.transformVec3(axesBasis, delta, delta);

        const measurementOrientationVertical = this._useRotationAdjustment.get() && Math.abs(delta[1]) > 0;

        const setWireCoordinates = (xEnd, zStart) => {

            const metrics = this.plugin.viewer.scene.metrics;
            const scale = metrics.scale;
            const unit = metrics.unitsInfo[metrics.units].abbrev;

            const setAxisLabelCoords = (label, a, b, offsetIdx) => {
                if (this._labelsOnWires.get()) {
                    label.setPosOnWire(a, b, 0, this.plugin.labelMinAxisLength);
                } else {
                    label.setPosOnWire(p0, p1, offsetIdx * 35, 0);
                }
            };
            const unitStr = len => (this._approximate ? " ~ " : " = ") + len.toFixed(2) + unit;

            this._xAxisWire.setEnds(p0, xEnd);
            setAxisLabelCoords(this._xAxisLabel, p0, xEnd, 1);
            this._xAxisLabel.setText("X" + unitStr(math.distVec3(p0, xEnd) * scale));

            this._yAxisWire.setEnds(xEnd, zStart);
            setAxisLabelCoords(this._yAxisLabel, xEnd, zStart, 2);
            this._yAxisLabel.setText("Y" + unitStr(math.distVec3(xEnd, zStart) * scale));

            this._zAxisWire.setEnds(zStart, p1);
            setAxisLabelCoords(this._zAxisLabel, zStart, p1, 3);
            this._zAxisLabel.setText((measurementOrientationVertical ? "" : "Z") + unitStr(math.distVec3(zStart, p1) * scale));

            this._lengthWire.setEnds(p0, p1);
            setAxisLabelCoords(this._lengthLabel, p0, p1, 0);
            this._length = math.distVec3(p0, p1) * scale;
            this._lengthLabel.setText(unitStr(this._length));
        };

        if (measurementOrientationVertical) {
            tmpVec3c[0] = p0[0];
            tmpVec3c[1] = p1[1];
            tmpVec3c[2] = p0[2];

            setWireCoordinates(p0, tmpVec3c);
        }
        else {
            tmpVec3b[0] = p0[0] + axesBasis[0] * factors[0];
            tmpVec3b[1] = p0[1] + axesBasis[4] * factors[0];
            tmpVec3b[2] = p0[2] + axesBasis[8] * factors[0];

            tmpVec3c[0] = tmpVec3b[0] + axesBasis[1] * factors[1];
            tmpVec3c[1] = tmpVec3b[1] + axesBasis[5] * factors[1];
            tmpVec3c[2] = tmpVec3b[2] + axesBasis[9] * factors[1];

            setWireCoordinates(tmpVec3b, tmpVec3c);
        }
    }

    /**
     * Sets the axes basis for the measurement.
     *
     * The value is a 4x4 matrix where each column-vector defines an axis and must have unit length.
     *
     * This is the ```identity``` matrix by default, meaning the measurement axes are the same as the world axes.
     *
     * @param {number[]} value
     */
    set axesBasis(value) {
        this._axesBasis.set(value);
        this._update();
    }

    /**
     * Gets the axes basis for the measurement.
     *
     * The value is a 4x4 matrix where each column-vector defines an axis and must have unit length.
     *
     * This is the ```identity``` matrix by default, meaning the measurement axes are the same as the world axes.
     *
     * @type {number[]}
     */
    get axesBasis() {
        return this._axesBasis;
    }

    /**
     * Sets whether this DistanceMeasurement indicates that its measurement is approximate.
     *
     * This is ````true```` by default.
     *
     * @type {Boolean}
     */
    set approximate(approximate) {
        approximate = approximate !== false;
        if (this._approximate === approximate) {
            return;
        }
        this._approximate = approximate;
        this._update();
    }

    /**
     * Gets whether this DistanceMeasurement indicates that its measurement is approximate.
     *
     * This is ````true```` by default.
     *
     * @type {Boolean}
     */
    get approximate() {
        return this._approximate;
    }

    /**
     * Gets the origin {@link Dot3D}.
     *
     * @type {Dot3D}
     */
    get origin() {
        return this._originDot;
    }

    /**
     * Gets the target {@link Dot3D}.
     *
     * @type {Dot3D}
     */
    get target() {
        return this._targetDot;
    }

    /**
     * Gets the World-space direct point-to-point distance between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target}.
     *
     * @type {Number}
     */
    get length() {
        return this._length;
    }

    get color() {
        return this._color;
    }

    set color(value) {
        this._color = value;
        this._originDot.setFillColor(value);
        this._targetDot.setFillColor(value);
        this._lengthWire.setColor(value);
        this._lengthLabel.setFillColor(value);
    }

    /**
     * Sets whether this DistanceMeasurement is visible or not.
     *
     * @type {Boolean}
     */
    set visible(value) {
        this._visible.set(value);
    }

    /**
     * Gets whether this DistanceMeasurement is visible or not.
     *
     * @type {Boolean}
     */
    get visible() {
        return this._visible.get();
    }

    /**
     * Sets if the origin {@link Dot3D} is visible.
     *
     * @type {Boolean}
     */
    set originVisible(value) {
        this._originVisible.set(value);
    }

    /**
     * Gets if the origin {@link Dot3D} is visible.
     *
     * @type {Boolean}
     */
    get originVisible() {
        return this._originVisible.get();
    }

    /**
     * Sets if the target {@link Dot3D} is visible.
     *
     * @type {Boolean}
     */
    set targetVisible(value) {
        this._targetVisible.set(value);
    }

    /**
     * Gets if the target {@link Dot3D} is visible.
     *
     * @type {Boolean}
     */
    get targetVisible() {
        return this._targetVisible.get();
    }

    /**
     * Sets if the measurement is adjusted based on rotation
     *
     * @type {Boolean}
     */
    set useRotationAdjustment(value) {
        this._useRotationAdjustment.set(value);
        this._update();
    }

    /**
     * Gets if the measurement is adjusted based on rotation
     *
     * @type {Boolean}
     */
    get useRotationAdjustment() {
        return this._useRotationAdjustment.get();
    }

    /**
     * Sets if the axis-aligned wires between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} are enabled.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    set axisEnabled(value) {
        this._axisEnabled.set(value);
    }

    /**
     * Gets if the axis-aligned wires between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} are enabled.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    get axisEnabled() {
        return this._axisEnabled.get();
    }

    /**
     * Sets if the axis-aligned wires between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} are visible.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    set axisVisible(value) {
        this._axisVisible.set(value);
    }

    /**
     * Gets if the axis-aligned wires between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} are visible.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    get axisVisible() {
        return this._axisVisible.get();
    }

    /**
     * Sets if the X-axis-aligned wire between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} is visible.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    set xAxisVisible(value) {
        this._xAxisVisible.set(value);
    }

    /**
     * Gets if the X-axis-aligned wires between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} are visible.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    get xAxisVisible() {
        return this._xAxisVisible.get();
    }

    /**
     * Sets if the Y-axis-aligned wire between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} is visible.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    set yAxisVisible(value) {
        this._yAxisVisible.set(value);
    }

    /**
     * Gets if the Y-axis-aligned wires between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} are visible.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    get yAxisVisible() {
        return this._yAxisVisible.get();
    }

    /**
     * Sets if the Z-axis-aligned wire between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} is visible.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    set zAxisVisible(value) {
        this._zAxisVisible.set(value);
    }

    /**
     * Gets if the Z-axis-aligned wires between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} are visible.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    get zAxisVisible() {
        return this._zAxisVisible.get();
    }

    /**
     * Sets if the direct point-to-point wire between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} is visible.
     *
     * @type {Boolean}
     */
    set wireVisible(value) {
        this._wireVisible.set(value);
    }

    /**
     * Gets if the direct point-to-point wire between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} is visible.
     *
     * @type {Boolean}
     */
    get wireVisible() {
        return this._wireVisible.get();
    }

    /**
     * Sets if the labels are visible except the length label.
     *
     * @type {Boolean}
     */
    set labelsVisible(value) {
        this._labelsVisible.set(value);
    }

    /**
     * Gets if the labels are visible.
     *
     * @type {Boolean}
     */
    get labelsVisible() {
        return this._labelsVisible.get();
    }

    /**
     * Sets if the x label is enabled.
     *
     * @type {Boolean}
     */
    set xLabelEnabled(value) {
        this._xLabelEnabled.set(value);
    }

    /**
     * Gets if the x label is enabled.
     *
     * @type {Boolean}
     */
    get xLabelEnabled(){
        return this._xLabelEnabled.get();
    }

    /**
     * Sets if the y label is enabled.
     *
     * @type {Boolean}
     */
    set yLabelEnabled(value) {
        this._yLabelEnabled.set(value);
    }

    /**
     * Gets if the y label is enabled.
     *
     * @type {Boolean}
     */
    get yLabelEnabled(){
        return this._yLabelEnabled.get();
    }

    /**
     * Sets if the z label is enabled.
     *
     * @type {Boolean}
     */
    set zLabelEnabled(value) {
        this._zLabelEnabled.set(value);
    }

    /**
     * Gets if the z label is enabled.
     *
     * @type {Boolean}
     */
    get zLabelEnabled(){
        return this._zLabelEnabled.get();
    }

    /**
     * Sets if the length label is enabled.
     *
     * @type {Boolean}
     */
    set lengthLabelEnabled(value) {
        this._lengthLabelEnabled.set(value);
    }

    /**
     * Gets if the length label is enabled.
     *
     * @type {Boolean}
     */
    get lengthLabelEnabled(){
        return this._lengthLabelEnabled.get();
    }

    /**
     * Sets if labels should be positioned on the wires.
     *
     * @type {Boolean}
     */
    set labelsOnWires(value) {
        this._labelsOnWires.set(value);
        this._update();
    }

    /**
     * Gets if labels should be positioned on the wires.
     *
     * @type {Boolean}
     */
    get labelsOnWires() {
        return this._labelsOnWires.get();
    }

    /**
     * Sets if this DistanceMeasurement appears highlighted.
     * @param highlighted
     */
    setHighlighted(highlighted) {
        this._drawables.forEach(d => d.setHighlighted(highlighted));
    }

    /**
     * Sets if the wires, dots ad labels will fire "mouseOver" "mouseLeave" and "contextMenu" events, or ignore mouse events altogether.
     *
     * @type {Boolean}
     */
    set clickable(value) {
        this._clickable.set(!!value);
        this._drawables.forEach(d => d.setClickable(this._clickable.get()));
    }

    /**
     * Gets if the wires, dots ad labels will fire "mouseOver" "mouseLeave" and "contextMenu" events.
     *
     * @type {Boolean}
     */
    get clickable() {
        return this._clickable.get();
    }

    /**
     * @private
     */
    destroy() {
        this._cleanups.forEach(cleanup => cleanup());
        super.destroy();
    }
}

/**
 * Creates {@link DistanceMeasurement}s in a {@link DistanceMeasurementsPlugin} from user input.
 *
 * @interface
 * @abstract
 */
class DistanceMeasurementsControl extends Component {

    /**
     * Gets if this DistanceMeasurementsControl is currently active, where it is responding to input.
     *
     * @returns {boolean} True if this DistanceMeasurementsControl is active.
     * @abstract
     */
    get active() {
    }

    /**
     * Sets whether snap-to-vertex and snap-to-edge are enabled for this DistanceMeasurementsControl.
     *
     * This is `true` by default.
     *
     * Internally, this deactivates then activates the DistanceMeasurementsControl when changed, which means that
     * it will destroy any DistanceMeasurements currently under construction, and incurs some overhead, since it unbinds
     * and rebinds various input handlers.
     *
     * @param {boolean} snapping Whether to enable snap-to-vertex and snap-edge for this DistanceMeasurementsControl.
     */
    set snapping(snapping) {
    }

    /**
     * Gets whether snap-to-vertex and snap-to-edge are enabled for this DistanceMeasurementsControl.
     *
     * This is `true` by default.
     *
     * @returns {boolean} Whether snap-to-vertex and snap-to-edge are enabled for this DistanceMeasurementsControl.
     */
    get snapping() {
        return true;
    }

    /**
     * Activates this DistanceMeasurementsControl, ready to respond to input.
     *
     * @abstract
     */
    activate() {
    }

    /**
     * Deactivates this DistanceMeasurementsControl, making it unresponsive to input.
     *
     * Destroys any {@link DistanceMeasurement} under construction by this DistanceMeasurementsControl.
     *
     * @abstract
     */
    deactivate() {
    }

    /**
     * Resets this DistanceMeasurementsControl.
     *
     * Destroys any {@link DistanceMeasurement} under construction by this DistanceMeasurementsControl.
     *
     * Does nothing if the DistanceMeasurementsControl is not active.
     *
     * @abstract
     */
    reset() {
    }

    /**
     * Gets the {@link DistanceMeasurement} under construction by this DistanceMeasurementsControl, if any.
     *
     * @returns {null|DistanceMeasurement}
     *
     * @abstract
     */
    get currentMeasurement() {
        return null;
    }

    /**
     * Destroys this DistanceMeasurementsControl.
     *
     * Destroys any {@link DistanceMeasurement} under construction by this DistanceMeasurementsControl.
     *
     * @abstract
     */
    destroy() {
    }
}

const MOUSE_FIRST_CLICK_EXPECTED = 0;

/**
 * Creates {@link DistanceMeasurement}s in a {@link DistanceMeasurementsPlugin} from mouse input.
 *
 * ## Usage
 *
 * [[Run example](/examples/measurement/#distance_createWithMouse_snapping)]
 *
 * ````javascript
 * import {Viewer, XKTLoaderPlugin, DistanceMeasurementsPlugin, DistanceMeasurementsMouseControl, PointerLens} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 * });
 *
 * viewer.camera.eye = [-3.93, 2.85, 27.01];
 * viewer.camera.look = [4.40, 3.72, 8.89];
 * viewer.camera.up = [-0.01, 0.99, 0.039];
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const sceneModel = xktLoader.load({
 *     id: "myModel",
 *     src: "Duplex.xkt"
 * });
 *
 * const distanceMeasurements = new DistanceMeasurementsPlugin(viewer);
 *
 * const distanceMeasurementsControl  = new DistanceMeasurementsMouseControl(DistanceMeasurements, {
 *     pointerLens: new PointerLens(viewer)
 * })
 *
 * distanceMeasurementsControl.snapping = true;
 *
 * distanceMeasurementsControl.activate();
 * ````
 */
class DistanceMeasurementsMouseControl extends DistanceMeasurementsControl {

    /**
     * Creates a DistanceMeasurementsMouseControl bound to the given DistanceMeasurementsPlugin.
     *
     * @param {DistanceMeasurementsPlugin} distanceMeasurementsPlugin The AngleMeasurementsPlugin to control.
     * @param [cfg] Configuration
     * @param {function} [cfg.canvasToPagePos] Optional function to map canvas-space coordinates to page coordinates.
     * @param {PointerLens} [cfg.pointerLens] A PointerLens to use to provide a magnified view of the cursor when snapping is enabled.
     * @param {boolean} [cfg.snapping=true] Whether to initially enable snap-to-vertex and snap-to-edge for this DistanceMeasurementsMouseControl.
     */
    constructor(distanceMeasurementsPlugin, cfg = {}) {

        super(distanceMeasurementsPlugin.viewer.scene);

        this._canvasToPagePos = cfg.canvasToPagePos;

        this.pointerLens = cfg.pointerLens;

        this._active = false;

        this._currentDistanceMeasurement = null;

        this._currentDistanceMeasurementInitState = {
            wireVisible: null,
            axisVisible: null,
            xAxisVisible: null,
            yaxisVisible: null,
            zAxisVisible: null,
            targetVisible: null,
        };

        this._initMarkerDiv();

        this._snapping = cfg.snapping !== false;
        this._mouseState = MOUSE_FIRST_CLICK_EXPECTED;

        this._attachPlugin(distanceMeasurementsPlugin, cfg);
    }

    _initMarkerDiv() {
        const markerDiv = document.createElement('div');
        const canvas = this.scene.canvas.canvas;
        canvas.parentNode.insertBefore(markerDiv, canvas);
        markerDiv.style.background = "black";
        markerDiv.style.border = "2px solid blue";
        markerDiv.style.borderRadius = "10px";
        markerDiv.style.width = "5px";
        markerDiv.style.height = "5px";
        markerDiv.style.top = "-200px";
        markerDiv.style.left = "-200px";
        markerDiv.style.margin = "0 0";
        markerDiv.style.zIndex = "100";
        markerDiv.style.position = "absolute";
        markerDiv.style.pointerEvents = "none";

        this._markerDiv = markerDiv;
    }

    _destroyMarkerDiv() {
        if (this._markerDiv) {
            this._markerDiv.parentNode.removeChild(this._markerDiv);
            this._markerDiv = null;
        }
    }

    _attachPlugin(distanceMeasurementsPlugin, cfg = {}) {

        /**
         * The {@link DistanceMeasurementsPlugin} that owns this DistanceMeasurementsMouseControl.
         * @type {DistanceMeasurementsPlugin}
         */
        this.distanceMeasurementsPlugin = distanceMeasurementsPlugin;

        /**
         * The {@link DistanceMeasurementsPlugin} that owns this DistanceMeasurementsMouseControl.
         * @type {DistanceMeasurementsPlugin}
         */
        this.plugin = distanceMeasurementsPlugin;
    }

    /**
     * Gets if this DistanceMeasurementsMouseControl is currently active, where it is responding to input.
     *
     * @returns {boolean} True if this DistanceMeasurementsMouseControl is active.
     */
    get active() {
        return this._active;
    }

    /**
     * Sets whether snap-to-vertex and snap-to-edge are enabled for this DistanceMeasurementsMouseControl.
     *
     * This is `true` by default.
     *
     * @param snapping
     */
    set snapping(snapping) {
        this._snapping = snapping;
    }

    /**
     * Gets whether snap-to-vertex and snap-to-edge are enabled for this DistanceMeasurementsMouseControl.
     *
     * This is `true` by default.
     * @returns {*}
     */
    get snapping() {
        return this._snapping;
    }

    /**
     * Activates this DistanceMeasurementsMouseControl, ready to respond to input.
     */
    activate() {

        if (this._active) {
            return;
        }

        if (!this._markerDiv) {
            this._initMarkerDiv();
        }

        this.fire("activated", true);

        const distanceMeasurementsPlugin = this.distanceMeasurementsPlugin;
        const scene = this.scene;
        const cameraControl = distanceMeasurementsPlugin.viewer.cameraControl;
        const canvas = scene.canvas.canvas;
        scene.input;
        let mouseHovering = false;
        const pointerWorldPos = math.vec3();
        const pointerCanvasPos = math.vec2();
        let pointerDownCanvasX;
        let pointerDownCanvasY;
        const clickTolerance = 20;
        let hoveredEntity = null;

        this._mouseState = MOUSE_FIRST_CLICK_EXPECTED;

        const pagePos = math.vec2();

        const hoverOn = event => {
                const canvasPos = event.snappedCanvasPos || event.canvasPos;
                mouseHovering = true;
                pointerWorldPos.set(event.worldPos);
                pointerCanvasPos.set(event.canvasPos);
                if (this._mouseState === MOUSE_FIRST_CLICK_EXPECTED) {

                    if (this._canvasToPagePos) {
                        this._canvasToPagePos(canvas, canvasPos, pagePos);
                        this._markerDiv.style.left = `${pagePos[0] - 5}px`;
                        this._markerDiv.style.top = `${pagePos[1] - 5}px`;
                    } else {
                        const markerPos = math.vec2(canvasPos);
                        transformToNode(canvas, this._markerDiv.parentNode, markerPos);
                        this._markerDiv.style.left = `${markerPos[0] - 5}px`;
                        this._markerDiv.style.top = `${markerPos[1] - 5}px`;
                    }

                    this._markerDiv.style.background = "pink";
                    if (event.snappedToVertex || event.snappedToEdge) {
                        if (this.pointerLens) {
                            this.pointerLens.visible = true;
                            this.pointerLens.canvasPos = event.canvasPos;
                            this.pointerLens.snappedCanvasPos = event.snappedCanvasPos || event.canvasPos;
                            this.pointerLens.snapped = true;
                        }
                        this._markerDiv.style.background = "greenyellow";
                        this._markerDiv.style.border = "2px solid green";
                    } else {
                        if (this.pointerLens) {
                            this.pointerLens.visible = true;
                            this.pointerLens.canvasPos = event.canvasPos;
                            this.pointerLens.snappedCanvasPos = event.canvasPos;
                            this.pointerLens.snapped = false;
                        }
                        this._markerDiv.style.background = "pink";
                        this._markerDiv.style.border = "2px solid red";
                    }
                    hoveredEntity = event.entity;
                } else {
                    this._markerDiv.style.left = `-10000px`;
                    this._markerDiv.style.top = `-10000px`;
                }
                canvas.style.cursor = "pointer";
                if (this._currentDistanceMeasurement) {
                    this._currentDistanceMeasurement.wireVisible = this._currentDistanceMeasurementInitState.wireVisible;
                    this._currentDistanceMeasurement.axisVisible = this._currentDistanceMeasurementInitState.axisVisible && this.distanceMeasurementsPlugin.defaultAxisVisible;
                    this._currentDistanceMeasurement.xAxisVisible = this._currentDistanceMeasurementInitState.xAxisVisible && this.distanceMeasurementsPlugin.defaultXAxisVisible;
                    this._currentDistanceMeasurement.yAxisVisible = this._currentDistanceMeasurementInitState.yAxisVisible && this.distanceMeasurementsPlugin.defaultYAxisVisible;
                    this._currentDistanceMeasurement.zAxisVisible = this._currentDistanceMeasurementInitState.zAxisVisible && this.distanceMeasurementsPlugin.defaultZAxisVisible;
                    this._currentDistanceMeasurement.targetVisible = this._currentDistanceMeasurementInitState.targetVisible;
                    this._currentDistanceMeasurement.target.worldPos = pointerWorldPos.slice();
                    this._markerDiv.style.left = `-10000px`;
                    this._markerDiv.style.top = `-10000px`;
                }
        };
        this._onHoverSnapOrSurface = cameraControl.on("hoverSnapOrSurface", e => { if (this._snapping)   hoverOn(e); });
        this._onHoverSurface       = cameraControl.on("hoverSurface",       e => { if (! this._snapping) hoverOn(e); });

        canvas.addEventListener('mousedown', this._onMouseDown = (e) => {
            if (e.which !== 1) {
                return;
            }
            pointerDownCanvasX = e.clientX;
            pointerDownCanvasY = e.clientY;
        });

        canvas.addEventListener("mouseup", this._onMouseUp = (e) => {
            if (e.which !== 1) {
                return;
            }
            if (e.clientX > pointerDownCanvasX + clickTolerance ||
                e.clientX < pointerDownCanvasX - clickTolerance ||
                e.clientY > pointerDownCanvasY + clickTolerance ||
                e.clientY < pointerDownCanvasY - clickTolerance) {
                return;
            }
            if (this._currentDistanceMeasurement) {
                if (mouseHovering) {
                    this._currentDistanceMeasurement.target.entity = hoveredEntity;
                    hoveredEntity = null;
                    this._currentDistanceMeasurement.clickable = true;
                    this.distanceMeasurementsPlugin.fire("measurementEnd", this._currentDistanceMeasurement);
                    this._currentDistanceMeasurement = null;
                } else {
                    this._currentDistanceMeasurement.destroy();
                    this.distanceMeasurementsPlugin.fire("measurementCancel", this._currentDistanceMeasurement);
                    this._currentDistanceMeasurement = null;
                    hoveredEntity = null;
                }
            } else {
                if (mouseHovering) {
                    this._currentDistanceMeasurement = distanceMeasurementsPlugin.createMeasurement({
                        id: math.createUUID(),
                        origin: {
                            worldPos: pointerWorldPos.slice(),
                            entity: hoveredEntity
                        },
                        target: {
                            worldPos: pointerWorldPos.slice(),
                            entity: hoveredEntity
                        },
                        approximate: true
                    });
                    this._currentDistanceMeasurementInitState.axisVisible = this._currentDistanceMeasurement.axisVisible && this.distanceMeasurementsPlugin.defaultAxisVisible;
                    this._currentDistanceMeasurementInitState.xAxisVisible = this._currentDistanceMeasurement.xAxisVisible && this.distanceMeasurementsPlugin.defaultXAxisVisible;
                    this._currentDistanceMeasurementInitState.yAxisVisible = this._currentDistanceMeasurement.yAxisVisible && this.distanceMeasurementsPlugin.defaultYAxisVisible;
                    this._currentDistanceMeasurementInitState.zAxisVisible = this._currentDistanceMeasurement.zAxisVisible && this.distanceMeasurementsPlugin.defaultZAxisVisible;
                    this._currentDistanceMeasurementInitState.wireVisible = this._currentDistanceMeasurement.wireVisible;
                    this._currentDistanceMeasurementInitState.targetVisible = this._currentDistanceMeasurement.targetVisible;
                    this._currentDistanceMeasurement.clickable = false;
                    this._currentDistanceMeasurement.origin.entity = hoveredEntity;
                    hoveredEntity = null;
                    this.distanceMeasurementsPlugin.fire("measurementStart", this._currentDistanceMeasurement);
                }
            }
        });

        const hoverOff = event => {
                if (this.pointerLens) {
                    this.pointerLens.visible = true;
                    this.pointerLens.canvasPos = event.canvasPos;
                    this.pointerLens.snappedCanvasPos = event.snappedCanvasPos || event.canvasPos;
                }
                mouseHovering = false;
                this._markerDiv.style.left = `-100px`;
                this._markerDiv.style.top = `-100px`;
                if (this._currentDistanceMeasurement) {
                    this._currentDistanceMeasurement.wireVisible = false;
                    this._currentDistanceMeasurement.targetVisible = false;
                    this._currentDistanceMeasurement.axisVisible = false;
                }
                canvas.style.cursor = "default";
        };
        this._onHoverSnapOrSurfaceOff = cameraControl.on("hoverSnapOrSurfaceOff", e => { if (this._snapping)   hoverOff(e); });
        this._onHoverOff              = cameraControl.on("hoverOff",              e => { if (! this._snapping) hoverOff(e); });

        this._active = true;
    }

    /**
     * Deactivates this DistanceMeasurementsMouseControl, making it unresponsive to input.
     *
     * Destroys any {@link DistanceMeasurement} under construction by this DistanceMeasurementsMouseControl.
     */
    deactivate() {
        if (!this._active) {
            return;
        }

        this.fire("activated", false);

        if (this.pointerLens) {
            this.pointerLens.visible = false;
        }

        this.reset();

        const canvas = this.scene.canvas.canvas;
        canvas.removeEventListener("mousedown", this._onMouseDown);
        canvas.removeEventListener("mouseup", this._onMouseUp);
        const cameraControl = this.distanceMeasurementsPlugin.viewer.cameraControl;
        cameraControl.off(this._onHoverSnapOrSurface);
        cameraControl.off(this._onHoverSurface);
        cameraControl.off(this._onHoverSnapOrSurfaceOff);
        cameraControl.off(this._onHoverOff);

        this._active = false;
    }

    /**
     * Resets this DistanceMeasurementsMouseControl.
     *
     * Destroys any {@link DistanceMeasurement} under construction by this DistanceMeasurementsMouseControl.
     *
     * Does nothing if the DistanceMeasurementsMouseControl is not active.
     */
    reset() {
        if (!this._active) {
            return;
        }

        this._destroyMarkerDiv();
        this._initMarkerDiv();

        if (this._currentDistanceMeasurement) {
            this.distanceMeasurementsPlugin.fire("measurementCancel", this._currentDistanceMeasurement);
            this._currentDistanceMeasurement.destroy();
            this._currentDistanceMeasurement = null;
        }

        this._mouseState = MOUSE_FIRST_CLICK_EXPECTED;
    }

    /**
     * Gets the {@link DistanceMeasurement} under construction by this DistanceMeasurementsMouseControl, if any.
     *
     * @returns {null|DistanceMeasurement}
     */
    get currentMeasurement() {
        return this._currentDistanceMeasurement;
    }

    /**
     * Destroys this DistanceMeasurementsMouseControl.
     *
     * Destroys any {@link DistanceMeasurement} under construction by this DistanceMeasurementsMouseControl.
     */
    destroy() {
        this.deactivate();
        super.destroy();
    }
}

/**
 * {@link Viewer} plugin for measuring point-to-point distances.
 *
 * [<img src="https://user-images.githubusercontent.com/83100/63047331-867a0a80-bed4-11e9-892f-398740013c5f.gif">](https://xeokit.github.io/xeokit-sdk/examples/index.html#measurements_distance_createWithMouse)
 *
 * * [[Example 1: Model with distance measurements](https://xeokit.github.io/xeokit-sdk/examples/index.html#measurements_distance_modelWithMeasurements)]
 * * [[Example 2: Create distance measurements with mouse](https://xeokit.github.io/xeokit-sdk/examples/measurement/#distance_createWithMouse_snapping)]
 * * [[Example 3: Configuring units and scale](https://xeokit.github.io/xeokit-sdk/examples/measurement/#distance_unitsAndScale)
 *
 * ## Overview
 *
 * * A {@link DistanceMeasurement} represents a point-to-point measurement between two 3D points on one or two {@link Entity}s.
 * * As shown on the screen capture above, a DistanceMeasurement has one wire (light blue) that shows the direct point-to-point measurement,
 * and three more wires (red, green and blue) that show the distance on each of the World-space X, Y and Z axis.
 * * Create DistanceMeasurements programmatically with {@link DistanceMeasurementsPlugin#createMeasurement}.
 * * Create DistanceMeasurements interactively using a {@link DistanceMeasurementsControl}.
 * * Existing DistanceMeasurements are registered by ID in {@link DistanceMeasurementsPlugin#measurements}.
 * * Destroy DistanceMeasurements using {@link DistanceMeasurementsPlugin#destroyMeasurement}.
 * * Configure global measurement units and scale via {@link Metrics}, located at {@link Scene#metrics}.
 *
 * ## Example 1: Creating DistanceMeasurements Programmatically
 *
 * In our first example, we'll use an {@link XKTLoaderPlugin} to load a model, and then use a DistanceMeasurementsPlugin to programmatically create two {@link DistanceMeasurement}s.
 *
 * Note how each DistanceMeasurement has ````origin```` and ````target```` endpoints, which each indicate a 3D World-space
 * position on the surface of an {@link Entity}. The endpoints can be attached to the same Entity, or to different Entitys.
 *
 * [[Run example](https://xeokit.github.io/xeokit-sdk/examples/measurement/#distance_modelWithMeasurements)]
 *
 * ````JavaScript
 * import {Viewer, XKTLoaderPlugin, DistanceMeasurementsPlugin} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.scene.camera.eye = [-2.37, 18.97, -26.12];
 * viewer.scene.camera.look = [10.97, 5.82, -11.22];
 * viewer.scene.camera.up = [0.36, 0.83, 0.40];
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const distanceMeasurements = new DistanceMeasurementsPlugin(viewer);
 *
 * const model = xktLoader.load({
 *      src: "./models/xkt/duplex/duplex.xkt"
 * });
 *
 * model.on("loaded", () => {
 *
 *      const myMeasurement1 = distanceMeasurements.createMeasurement({
 *          id: "distanceMeasurement1",
 *          origin: {
 *              entity: viewer.scene.objects["2O2Fr$t4X7Zf8NOew3FLOH"],
 *              worldPos: [0.044, 5.998, 17.767]
 *          },
 *          target: {
 *              entity: viewer.scene.objects["2O2Fr$t4X7Zf8NOew3FLOH"],
 *              worldPos: [4.738, 3.172, 17.768]
 *          },
 *          visible: true,
 *          wireVisible: true
 *      });
 *
 *      const myMeasurement2 = distanceMeasurements.createMeasurement({
 *          id: "distanceMeasurement2",
 *          origin: {
 *              entity: viewer.scene.objects["2O2Fr$t4X7Zf8NOew3FNr2"],
 *              worldPos: [0.457, 2.532, 17.766]
 *          },
 *          target: {
 *              entity: viewer.scene.objects["1CZILmCaHETO8tf3SgGEXu"],
 *              worldPos: [0.436, 0.001, 22.135]
 *          },
 *          visible: true,
 *          wireVisible: true
 *      });
 * });
 * ````
 *
 * ## Example 2: Creating DistanceMeasurements with Mouse Input
 *
 * In our second example, we'll use an {@link XKTLoaderPlugin} to load a model, then we'll use a
 * {@link DistanceMeasurementsMouseControl} to interactively create {@link DistanceMeasurement}s with mouse input.
 *
 * After we've activated the DistanceMeasurementsMouseControl, the first click on any {@link Entity} begins constructing a DistanceMeasurement, fixing its
 * origin to that Entity. The next click on any Entity will complete the DistanceMeasurement, fixing its target to that second Entity.
 *
 * The DistanceMeasurementsMouseControl will then wait for the next click on any Entity, to begin constructing
 * another DistanceMeasurement, and so on, until deactivated again.
 *
 * [[Run example](https://xeokit.github.io/xeokit-sdk/examples/measurement/#distance_createWithMouse_snapping)]
 *
 * ````JavaScript
 * import {Viewer, XKTLoaderPlugin, DistanceMeasurementsPlugin, DistanceMeasurementsMouseControl, PointerLens} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.scene.camera.eye = [-2.37, 18.97, -26.12];
 * viewer.scene.camera.look = [10.97, 5.82, -11.22];
 * viewer.scene.camera.up = [0.36, 0.83, 0.40];
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const distanceMeasurements = new DistanceMeasurementsPlugin(viewer);
 *
 * const distanceMeasurementsControl  = new DistanceMeasurementsMouseControl(distanceMeasurements, {
 *     pointerLens : new PointerLens(viewer)
 * })
 *
 * distanceMeasurementsControl.snapToVertex = true;
 * distanceMeasurementsControl.snapToEdge = true;
 *
 * distanceMeasurementsControl.activate();
 *
 * const model = xktLoader.load({
 *     src: "./models/xkt/duplex/duplex.xkt"
 * });
 * ````
 *
 * ## Example 3: Configuring Measurement Units and Scale
 *
 * In our third example, we'll use the  {@link Scene}'s {@link Metrics} to set the global unit of measurement to ````"meters"````. We'll also specify that a unit within the World-space coordinate system represents ten meters.
 *
 * The wires belonging to our DistanceMeasurements show their lengths in Real-space coordinates, in the current unit of measurement. They will dynamically update as we set these configurations.
 *
 * * [[Run example](https://xeokit.github.io/xeokit-sdk/examples/index.html#measurements_distance_unitsAndScale)]
 *
 * ````JavaScript
 * const metrics = viewer.scene.metrics;

 * metrics.units = "meters";
 * metrics.scale = 10.0;
 * ````
 *
 * ## Example 4: Attaching Mouse Handlers
 *
 * In our fourth example, we'll attach event handlers to our plugin, to catch when the user
 * hovers or right-clicks over our measurements.
 *
 * [[Run example](https://xeokit.github.io/xeokit-sdk/examples/index.html#measurements_distance_modelWithMeasurements)]
 *
 * ````javascript
 * import {Viewer, XKTLoaderPlugin, DistanceMeasurementsPlugin, DistanceMeasurementsMouseControl, PointerLens} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.scene.camera.eye = [-2.37, 18.97, -26.12];
 * viewer.scene.camera.look = [10.97, 5.82, -11.22];
 * viewer.scene.camera.up = [0.36, 0.83, 0.40];
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const distanceMeasurements = new DistanceMeasurementsPlugin(viewer);
 *
 * const distanceMeasurementsControl  = new DistanceMeasurementsMouseControl(distanceMeasurements, {
 *     pointerLens : new PointerLens(viewer)
 * })
 *
 * distanceMeasurementsControl.snapToVertex = true;
 * distanceMeasurementsControl.snapToEdge = true;
 *
 * distanceMeasurementsControl.activate();
 *
 * distanceMeasurements.on("mouseOver", (e) => {
 *     e.measurement.setHighlighted(true);
 * });
 *
 * distanceMeasurements.on("mouseLeave", (e) => {
 *     e.measurement.setHighlighted(false);
 * });
 *
 * distanceMeasurements.on("contextMenu", (e) => {
 *     // Show context menu
 *     e.event.preventDefault();
 * });
 *
 * const model = xktLoader.load({
 *      src: "Duplex.xkt"
 * });
 *
 * model.on("loaded", () => {
 *
 *      const myMeasurement1 = distanceMeasurements.createMeasurement({
 *          id: "distanceMeasurement1",
 *          origin: {
 *              entity: viewer.scene.objects["2O2Fr$t4X7Zf8NOew3FLOH"],
 *              worldPos: [0.044, 5.998, 17.767]
 *          },
 *          target: {
 *              entity: viewer.scene.objects["2O2Fr$t4X7Zf8NOew3FLOH"],
 *              worldPos: [4.738, 3.172, 17.768]
 *          },
 *          visible: true,
 *          wireVisible: true
 *      });
 *
 *      const myMeasurement2 = distanceMeasurements.createMeasurement({
 *          id: "distanceMeasurement2",
 *          origin: {
 *              entity: viewer.scene.objects["2O2Fr$t4X7Zf8NOew3FNr2"],
 *              worldPos: [0.457, 2.532, 17.766]
 *          },
 *          target: {
 *              entity: viewer.scene.objects["1CZILmCaHETO8tf3SgGEXu"],
 *              worldPos: [0.436, 0.001, 22.135]
 *          },
 *          visible: true,
 *          wireVisible: true
 *      });
 * });
 * ````
 *
 * ## Example 5: Creating DistanceMeasurements with Touch Input
 *
 * In our fifth example, we'll show how to create distance measurements with touch input, with snapping
 * to the nearest vertex or edge. While creating the measurements, a long-touch when setting the
 * start or end point will cause the point to snap to the nearest vertex or edge. A quick
 * touch-release will immediately set the point at the tapped position on the object surface.
 *
 * [[Run example](https://xeokit.github.io/xeokit-sdk/examples/measurement/#distance_createWithTouch_snapping)]
 *
 * ````javascript
 * import {Viewer, XKTLoaderPlugin, DistanceMeasurementsPlugin, DistanceMeasurementsTouchControl} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.scene.camera.eye = [-2.37, 18.97, -26.12];
 * viewer.scene.camera.look = [10.97, 5.82, -11.22];
 * viewer.scene.camera.up = [0.36, 0.83, 0.40];
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const distanceMeasurements = new DistanceMeasurementsPlugin(viewer);
 *
 * const model = xktLoader.load({
 *      src: "./models/xkt/duplex/duplex.xkt"
 * });
 *
 * const distanceMeasurements = new DistanceMeasurementsPlugin(viewer);
 *
 * const distanceMeasurementsTouchControl  = new DistanceMeasurementsTouchControl(distanceMeasurements, {
 *     pointerLens : new PointerLens(viewer),
 *     snapToVertex: true,
 *     snapToEdge: true
 * })
 *
 * distanceMeasurementsTouchControl.activate();
 * ````
 */
class DistanceMeasurementsPlugin extends Plugin {
  /**
   * @constructor
   * @param {Viewer} viewer The Viewer.
   * @param {Object} [cfg]  Plugin configuration.
   * @param {String} [cfg.id="DistanceMeasurements"] Optional ID for this plugin, so that we can find it within {@link Viewer#plugins}.
   * @param {Number} [cfg.labelMinAxisLength=25] The minimum length, in pixels, of an axis wire beyond which its label is shown.
   * @param {HTMLElement} [cfg.container] Container DOM element for markers and labels. Defaults to ````document.body````.
   * @param {boolean} [cfg.defaultVisible=true] The default value of the DistanceMeasurements `visible` property.
   * @param {boolean} [cfg.defaultOriginVisible=true] The default value of the DistanceMeasurements `originVisible` property.
   * @param {boolean} [cfg.defaultTargetVisible=true] The default value of the DistanceMeasurements `targetVisible` property.
   * @param {boolean} [cfg.defaultWireVisible=true] The default value of the DistanceMeasurements `wireVisible` property.
   * @param {boolean} [cfg.defaultLabelsVisible=true] The default value of the DistanceMeasurements `labelsVisible` property.
   * @param {boolean} [cfg.defaultXLabelEnabled=true] The default value of the DistanceMeasurements `xLabelEnabled` property.
   * @param {boolean} [cfg.defaultYLabelEnabled=true] The default value of the DistanceMeasurements `yLabelEnabled` property.
   * @param {boolean} [cfg.defaultZLabelEnabled=true] The default value of the DistanceMeasurements `zLabelEnabled` property.
   * @param {boolean} [cfg.defaultLengthLabelEnabled=true] The default value of the DistanceMeasurements `lengthLabelEnabled` property.
   * @param {boolean} [cfg.defaultAxisVisible=true] The default value of the DistanceMeasurements `axisVisible` property.
   * @param {boolean} [cfg.defaultXAxisVisible=true] The default value of the DistanceMeasurements `xAxisVisible` property.
   * @param {boolean} [cfg.defaultYAxisVisible=true] The default value of the DistanceMeasurements `yAxisVisible` property.
   * @param {boolean} [cfg.defaultZAxisVisible=true] The default value of the DistanceMeasurements `zAxisVisible` property.
   * @param {boolean} [cfg.useRotationAdjustment=false] The default value of DistanceMeasurements `useRotationAdjustment` property.
   * @param {string} [cfg.defaultColor=#00BBFF] The default color of the length dots, wire and label.
   * @param {number} [cfg.zIndex] If set, the wires, dots and labels will have this zIndex (+1 for dots and +2 for labels).
   * @param {boolean} [cfg.defaultLabelsOnWires=true] The default value of the DistanceMeasurements `labelsOnWires` property.
   * @param {PointerCircle} [cfg.pointerLens] A PointerLens to help the user position the pointer. This can be shared with other plugins.
   */
  constructor(viewer, cfg = {}) {
    super("DistanceMeasurements", viewer);

    this._pointerLens = cfg.pointerLens;

    this._container = cfg.container || document.body;

    this._defaultControl = null;

    this._measurements = {};

    this.labelMinAxisLength = cfg.labelMinAxisLength;
    this.defaultVisible = cfg.defaultVisible !== false;
    this.defaultOriginVisible = cfg.defaultOriginVisible !== false;
    this.defaultTargetVisible = cfg.defaultTargetVisible !== false;
    this.defaultWireVisible = cfg.defaultWireVisible !== false;
    this.defaultXLabelEnabled = cfg.defaultXLabelEnabled !== false;
    this.defaultYLabelEnabled = cfg.defaultYLabelEnabled !== false;
    this.defaultZLabelEnabled = cfg.defaultZLabelEnabled !== false;
    this.defaultLengthLabelEnabled = cfg.defaultLengthLabelEnabled !== false;
    this.defaultLabelsVisible = cfg.defaultLabelsVisible !== false;
    this.defaultAxisVisible = cfg.defaultAxisVisible !== false;
    this.defaultXAxisVisible = cfg.defaultXAxisVisible !== false;
    this.defaultYAxisVisible = cfg.defaultYAxisVisible !== false;
    this.defaultZAxisVisible = cfg.defaultZAxisVisible !== false;
    this.defaultColor =cfg.defaultColor !== undefined ? cfg.defaultColor : "#00BBFF";
    this.zIndex = cfg.zIndex || 10000;
    this.defaultLabelsOnWires = cfg.defaultLabelsOnWires !== false;
    this.useRotationAdjustment = cfg.useRotationAdjustment !== undefined ? cfg.useRotationAdjustment !== false : false;

    this._onMouseOver = (event, measurement) => {
      this.fire("mouseOver", {
        plugin: this,
        distanceMeasurement: measurement,
        measurement,
        event,
      });
    };

    this._onMouseLeave = (event, measurement) => {
      this.fire("mouseLeave", {
        plugin: this,
        distanceMeasurement: measurement,
        measurement,
        event,
      });
    };

    this._onContextMenu = (event, measurement) => {
      this.fire("contextMenu", {
        plugin: this,
        distanceMeasurement: measurement,
        measurement,
        event,
      });
    };
  }

  /**
   * Gets the plugin's HTML container element, if any.
   * @returns {*|HTMLElement|HTMLElement}
   */
  getContainerElement() {
    return this._container;
  }

  /**
   * @private
   */
  send(name, value) {}

  /**
   * Gets the PointerLens attached to this DistanceMeasurementsPlugin.
   * @returns {PointerCircle}
   */
  get pointerLens() {
    return this._pointerLens;
  }

  /**
   * Gets the default {@link DistanceMeasurementsControl}.
   *
   * @type {DistanceMeasurementsControl}
   * @deprecated
   */
  get control() {
    if (!this._defaultControl) {
      this._defaultControl = new DistanceMeasurementsMouseControl(this, {});
    }
    return this._defaultControl;
  }

  /**
   * Gets the existing {@link DistanceMeasurement}s, each mapped to its {@link DistanceMeasurement#id}.
   *
   * @type {{String:DistanceMeasurement}}
   */
  get measurements() {
    return this._measurements;
  }

  /**
   * Sets the minimum length, in pixels, of an axis wire beyond which its label is shown.
   *
   * The axis wire's label is not shown when its length is less than this value.
   *
   * This is ````25```` pixels by default.
   *
   * Must not be less than ````1````.
   *
   * @type {number}
   */
  set labelMinAxisLength(labelMinAxisLength) {
    if (labelMinAxisLength < 1) {
      this.error("labelMinAxisLength must be >= 1; defaulting to 25");
      labelMinAxisLength = 25;
    }
    this._labelMinAxisLength = labelMinAxisLength || 25;
  }

  /**
   * Gets the minimum length, in pixels, of an axis wire beyond which its label is shown.
   * @returns {number}
   */
  get labelMinAxisLength() {
    return this._labelMinAxisLength;
  }

  /**
   * Sets whether the measurement added is rotation adjusted or not.
   *
   * @type {boolean}
   */
  set useRotationAdjustment(_useRotationAdjustment) {
    _useRotationAdjustment = _useRotationAdjustment !== undefined ? Boolean(_useRotationAdjustment) : false;
    this._useRotationAdjustment = _useRotationAdjustment;
  }

  /**
   * Gets whether the measurement added is rotation adjusted or not.
   * @returns {number}
   */
  get useRotationAdjustment(){
    return this._useRotationAdjustment;
  }
  /**
   * Creates a {@link DistanceMeasurement}.
   *
   * The DistanceMeasurement is then registered by {@link DistanceMeasurement#id} in {@link DistanceMeasurementsPlugin#measurements}.
   *
   * @param {Object} params {@link DistanceMeasurement} configuration.
   * @param {String} params.id Unique ID to assign to {@link DistanceMeasurement#id}. The DistanceMeasurement will be registered by this in {@link DistanceMeasurementsPlugin#measurements} and {@link Scene.components}. Must be unique among all components in the {@link Viewer}.
   * @param {Number[]} params.origin.worldPos Origin World-space 3D position.
   * @param {Entity} params.origin.entity Origin Entity.
   * @param {Number[]} params.target.worldPos Target World-space 3D position.
   * @param {Entity} params.target.entity Target Entity.
   * @param {Boolean} [params.visible=true] Whether to initially show the {@link DistanceMeasurement}.
   * @param {Boolean} [params.originVisible=true] Whether to initially show the {@link DistanceMeasurement} origin.
   * @param {Boolean} [params.targetVisible=true] Whether to initially show the {@link DistanceMeasurement} target.
   * @param {Boolean} [params.wireVisible=true] Whether to initially show the direct point-to-point wire between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target}.
   * @param {Boolean} [params.axisVisible=true] Whether to initially show the axis-aligned wires between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target}.
   * @param {Boolean} [params.xAxisVisible=true] Whether to initially show the X-axis-aligned wires between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target}.
   * @param {Boolean} [params.yAxisVisible=true] Whether to initially show the Y-axis-aligned wires between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target}.
   * @param {Boolean} [params.zAxisVisible=true] Whether to initially show the Z-axis-aligned wires between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target}.
   * @param {Boolean} [params.xLabelEnabled=true] Whether to initially show the x label.
   * @param {Boolean} [params.yLabelEnabled=true] Whether to initially show the y label.
   * @param {Boolean} [params.zLabelEnabled=true] Whether to initially show the z label.
   * @param {Boolean} [params.lengthLabelEnabled=true] Whether to initially show the length label.
   * @param {Boolean} [params.labelsVisible=true] Whether to initially show the labels.
   * @param {Boolean} [params.lengthLabelVisible=true] Whether to initially show the labels.
   * @param {string} [params.color] The color of the length dot, wire and label.
   * @param {Boolean} [params.labelsOnWires=true] Determines if labels will be set on wires or one below the other.
   * @returns {DistanceMeasurement} The new {@link DistanceMeasurement}.
   */
  createMeasurement(params = {}) {
    if (this.viewer.scene.components[params.id]) {
      this.error(
        "Viewer scene component with this ID already exists: " + params.id
      );
      delete params.id;
    }
    const origin = params.origin;
    const target = params.target;
    const measurement = new DistanceMeasurement(this, {
      id: params.id,
      plugin: this,
      container: this._container,
      origin: {
        entity: origin.entity,
        worldPos: origin.worldPos,
      },
      target: {
        entity: target.entity,
        worldPos: target.worldPos,
      },
      visible: params.visible,
      wireVisible: params.wireVisible,
      axisVisible: params.axisVisible !== false && this.defaultAxisVisible !== false,
      xAxisVisible: params.xAxisVisible !== false && this.defaultXAxisVisible !== false,
      yAxisVisible: params.yAxisVisible !== false && this.defaultYAxisVisible !== false,
      zAxisVisible: params.zAxisVisible !== false && this.defaultZAxisVisible !== false,
      xLabelEnabled: params.xLabelEnabled !== false && this.defaultXLabelEnabled !== false,
      yLabelEnabled: params.yLabelEnabled !== false && this.defaultYLabelEnabled !== false,
      zLabelEnabled: params.zLabelEnabled !== false && this.defaultZLabelEnabled !== false,
      lengthLabelEnabled: params.lengthLabelEnabled !== false && this.defaultLengthLabelEnabled !== false,
      labelsVisible: params.labelsVisible !== false && this.defaultLabelsVisible !== false,
      useRotationAdjustment: this.useRotationAdjustment,
      originVisible: params.originVisible,
      targetVisible: params.targetVisible,
      color: params.color,
      labelsOnWires:params.labelsOnWires !== false && this.defaultLabelsOnWires !== false,
      onMouseOver: this._onMouseOver,
      onMouseLeave: this._onMouseLeave,
      onContextMenu: this._onContextMenu,
    });
    this._measurements[measurement.id] = measurement;
    measurement.clickable = true;
    measurement.on("destroyed", () => {
      delete this._measurements[measurement.id];
    });
    this.fire("measurementCreated", measurement);
    return measurement;
  }

  /**
   * Destroys a {@link DistanceMeasurement}.
   *
   * @param {String} id ID of DistanceMeasurement to destroy.
   */
  destroyMeasurement(id) {
    const measurement = this._measurements[id];
    if (!measurement) {
      this.log("DistanceMeasurement not found: " + id);
      return;
    }
    measurement.destroy();
    this.fire("measurementDestroyed", measurement);
  }

  /**
   * Shows all or hides the distance label of each {@link DistanceMeasurement}.
   *
   * @param {Boolean} labelsShown Whether or not to show the labels.
   */
  setLabelsShown(labelsShown) {
    for (const [key, measurement] of Object.entries(this.measurements)) {
      measurement.labelShown = labelsShown;
    }
  }

  /**
   * Shows all or hides the axis wires of each {@link DistanceMeasurement}.
   *
   * @param {Boolean} labelsShown Whether or not to show the axis wires.
   */
  setAxisVisible(axisVisible) {
    for (const [key, measurement] of Object.entries(this.measurements)) {
      measurement.axisVisible = axisVisible;
    }
    this.defaultAxisVisible = axisVisible;
  }

  /**
   * Gets if the axis wires of each {@link DistanceMeasurement} are visible.
   *
   * @returns {Boolean} Whether or not the axis wires are visible.
   */
  getAxisVisible() {
    return this.defaultAxisVisible;
  }

  /**
   * Destroys all {@link DistanceMeasurement}s.
   */
  clear() {
    const ids = Object.keys(this._measurements);
    for (var i = 0, len = ids.length; i < len; i++) {
      this.destroyMeasurement(ids[i]);
    }
  }

  /**
   * Destroys this DistanceMeasurementsPlugin.
   *
   * Destroys all {@link DistanceMeasurement}s first.
   */
  destroy() {
    this.clear();
    super.destroy();
  }
}

math.vec2();

/**
 * {@link Viewer} plugin that makes interaction smoother with large models, by temporarily switching
 * the Viewer to faster, lower-quality rendering modes whenever we interact.
 *
 * [<img src="https://xeokit.io/img/docs/FastNavPlugin/FastNavPlugin.gif">](https://xeokit.github.io/xeokit-sdk/examples/index.html#performance_FastNavPlugin)
 *
 * FastNavPlugin works by hiding specified Viewer rendering features, and optionally scaling the Viewer's canvas
 * resolution, whenever we interact with the Viewer. Then, once we've finished interacting, FastNavPlugin restores those
 * rendering features and the original canvas scale, after a configured delay.
 *
 * Depending on how we configure FastNavPlugin, we essentially switch to a smooth-rendering low-quality view while
 * interacting, then return to the normal higher-quality view after we stop, following an optional delay.
 *
 * Down-scaling the canvas resolution gives particularly good results. For example, scaling by ````0.5```` means that
 * we're rendering a quarter of the pixels while interacting, which can make the Viewer noticeably smoother with big models.
 *
 * The screen capture above shows FastNavPlugin in action. In this example, whenever we move the Camera or resize the Canvas,
 * FastNavPlugin switches off enhanced edges and ambient shadows (SAO), and down-scales the canvas, making it slightly
 * blurry. When ````0.5```` seconds passes with no interaction, the plugin shows edges and SAO again, and restores the
 * original canvas scale.
 *
 * # Usage
 *
 * In the example below, we'll create a {@link Viewer}, add a {@link FastNavPlugin}, then use an {@link XKTLoaderPlugin} to load a model.
 *
 * Whenever we interact with the Viewer, our FastNavPlugin will:
 *
 * * hide edges,
 * * hide ambient shadows (SAO),
 * * hide physically-based materials (switching to non-PBR),
 * * hide transparent objects, and
 * * scale the canvas resolution by 0.5, causing the GPU to render 75% less pixels.
 * <br>
 *
 * We'll also configure a 0.5 second delay before we transition back to high-quality each time we stop ineracting, so that we're
 * not continually flipping between low and high quality as we interact. Since we're only rendering ambient shadows when not interacting, we'll also treat ourselves
 * to expensive, high-quality SAO settings, that we wouldn't normally configure for an interactive SAO effect.
 *
 * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#performance_FastNavPlugin)]
 *
 * ````javascript
 * import {Viewer, XKTLoaderPlugin, FastNavPlugin} from "xeokit-sdk.es.js";
 *
 * // Create a Viewer with PBR and SAO enabled
 *
 * const viewer = new Viewer({
 *      canvasId: "myCanvas",
 *      transparent: true,
 *      pbr: true,                          // Enable physically-based rendering for Viewer
 *      sao: true                           // Enable ambient shadows for Viewer
 *  });
 *
 * viewer.scene.camera.eye = [-66.26, 105.84, -281.92];
 * viewer.scene.camera.look = [42.45, 49.62, -43.59];
 * viewer.scene.camera.up = [0.05, 0.95, 0.15];
 *
 * // Higher-quality SAO settings
 *
 * viewer.scene.sao.enabled = true;
 * viewer.scene.sao.numSamples = 60;
 * viewer.scene.sao.kernelRadius = 170;
 *
 * // Install a FastNavPlugin
 *
 * new FastNavPlugin(viewer, {
 *      hideEdges: true,                // Don't show edges while we interact (default is true)
 *      hideSAO: true,                  // Don't show ambient shadows while we interact (default is true)
 *      hideColorTexture: true,        // No color textures while we interact (default is true)
 *      hidePBR: true,                  // No physically-based rendering while we interact (default is true)
 *      hideTransparentObjects: true,   // Hide transparent objects while we interact (default is false)
 *      scaleCanvasResolution: true,    // Scale canvas resolution while we interact (default is false)
 *      defaultScaleCanvasResolutionFactor: 1.0, // Factor by which we scale canvas resolution when we stop interacting (default is 1.0)
 *      scaleCanvasResolutionFactor: 0.5,  // Factor by which we scale canvas resolution when we interact (default is 0.6)
 *      delayBeforeRestore: true,       // When we stop interacting, delay before restoring normal render (default is true)
 *      delayBeforeRestoreSeconds: 0.5  // The delay duration, in seconds (default is 0.5)
 * });
 *
 * // Load a BIM model from XKT
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const model = xktLoader.load({
 *      id: "myModel",
 *      src: "./models/xkt/HolterTower.xkt",
 *      sao: true,                          // Enable ambient shadows for this model
 *      pbr: true                           // Enable physically-based rendering for this model
 * });
 * ````
 *
 * @class FastNavPlugin
 */
class FastNavPlugin extends Plugin {

    /**
     * @constructor
     * @param {Viewer} viewer The Viewer.
     * @param {Object} cfg FastNavPlugin configuration.
     * @param {String} [cfg.id="FastNav"] Optional ID for this plugin, so that we can find it within {@link Viewer#plugins}.
     * @param {Boolean} [cfg.hideColorTexture=true] Whether to temporarily hide color textures whenever we interact with the Viewer.
     * @param {Boolean} [cfg.hidePBR=true] Whether to temporarily hide physically-based rendering (PBR) whenever we interact with the Viewer.
     * @param {Boolean} [cfg.hideSAO=true] Whether to temporarily hide scalable ambient occlusion (SAO) whenever we interact with the Viewer.
     * @param {Boolean} [cfg.hideEdges=true] Whether to temporarily hide edges whenever we interact with the Viewer.
     * @param {Boolean} [cfg.hideTransparentObjects=false] Whether to temporarily hide transparent objects whenever we interact with the Viewer.
     * @param {Number} [cfg.scaleCanvasResolution=false] Whether to temporarily down-scale the canvas resolution whenever we interact with the Viewer.
     * @param {Number} [cfg.defaultScaleCanvasResolutionFactor=0.6] The factor by which we downscale the canvas resolution whenever we stop interacting with the Viewer.
     * @param {Number} [cfg.scaleCanvasResolutionFactor=0.6] The factor by which we downscale the canvas resolution whenever we interact with the Viewer.
     * @param {Boolean} [cfg.delayBeforeRestore=true] Whether to temporarily have a delay before restoring normal rendering after we stop interacting with the Viewer.
     * @param {Number} [cfg.delayBeforeRestoreSeconds=0.5] Delay in seconds before restoring normal rendering after we stop interacting with the Viewer.
     * @param {Function} [cfg.onMoved] Optional callback function fired during moving mode, should return the callback function that will be fired when the interaction stops.
     */
    constructor(viewer, cfg = {}) {

        super("FastNav", viewer);

        this._hideColorTexture = cfg.hideColorTexture !== false;
        this._hidePBR = cfg.hidePBR !== false;
        this._hideSAO = cfg.hideSAO !== false;
        this._hideEdges = cfg.hideEdges !== false;
        this._hideTransparentObjects = !!cfg.hideTransparentObjects;
        this._scaleCanvasResolution = !!cfg.scaleCanvasResolution;
        this._defaultScaleCanvasResolutionFactor = cfg.defaultScaleCanvasResolutionFactor;
        this._scaleCanvasResolutionFactor = cfg.scaleCanvasResolutionFactor || 0.6;
        this._delayBeforeRestore = (cfg.delayBeforeRestore !== false);
        this._delayBeforeRestoreSeconds = cfg.delayBeforeRestoreSeconds || 0.5;
        this._onMoved = cfg.onMoved;
        this._onStopped = null;

        let timer = this._delayBeforeRestoreSeconds * 1000;
        let fastMode = false;

        const switchToLowQuality = () => {
            timer = (this._delayBeforeRestoreSeconds * 1000);
            if (!fastMode) {
                viewer.scene._renderer.setColorTextureEnabled(!this._hideColorTexture);
                viewer.scene._renderer.setPBREnabled(!this._hidePBR);
                viewer.scene._renderer.setSAOEnabled(!this._hideSAO);
                viewer.scene._renderer.setTransparentEnabled(!this._hideTransparentObjects);
                viewer.scene._renderer.setEdgesEnabled(!this._hideEdges);
                this._originalCanvasResolutionScale = viewer.scene.canvas.resolutionScale;
                if (this._scaleCanvasResolution) {
                    viewer.scene.canvas.resolutionScale = this._scaleCanvasResolutionFactor;
                } else {
                    viewer.scene.canvas.resolutionScale = this._defaultScaleCanvasResolutionFactor;
                }
                if (this._onMoved) {
                    this._onStopped = this._onMoved();
                }

                fastMode = true;
            }
        };

        const switchToHighQuality = () => {
            viewer.scene.canvas.resolutionScale = this._defaultScaleCanvasResolutionFactor || this._originalCanvasResolutionScale || 1.0;
            viewer.scene._renderer.setEdgesEnabled(true);
            viewer.scene._renderer.setColorTextureEnabled(true);
            viewer.scene._renderer.setPBREnabled(true);
            viewer.scene._renderer.setSAOEnabled(true);
            viewer.scene._renderer.setTransparentEnabled(true);
            if (this._onStopped) {
                this._onStopped();
            }
            fastMode = false;
        };

        this._onCameraMatrix = viewer.scene.camera.on("matrix", switchToLowQuality);

        this._onSceneTick = viewer.scene.on("tick", (tickEvent) => {
            if (!fastMode) {
                return;
            }
            timer -= tickEvent.deltaTime;
            if ((!this._delayBeforeRestore) || timer <= 0) {
                switchToHighQuality();
            }
        });
    }

    /**
     * Gets whether to temporarily hide color textures whenever we interact with the Viewer.
     *
     * Default is ````true````.
     *
     * @return {Boolean} ````true```` if hiding color textures.
     */
    get hideColorTexture() {
        return this._hideColorTexture;
    }

    /**
     * Sets whether to temporarily hide color textures whenever we interact with the Viewer.
     *
     * Default is ````true````.
     *
     * @param {Boolean} hideColorTexture ````true```` to hide color textures.
     */
    set hideColorTexture(hideColorTexture) {
        this._hideColorTexture = hideColorTexture;
    }
    
    /**
     * Gets whether to temporarily hide physically-based rendering (PBR) whenever we interact with the Viewer.
     *
     * Default is ````true````.
     *
     * @return {Boolean} ````true```` if hiding PBR.
     */
    get hidePBR() {
        return this._hidePBR;
    }

    /**
     * Sets whether to temporarily hide physically-based rendering (PBR) whenever we interact with the Viewer.
     *
     * Default is ````true````.
     *
     * @param {Boolean} hidePBR ````true```` to hide PBR.
     */
    set hidePBR(hidePBR) {
        this._hidePBR = hidePBR;
    }

    /**
     * Gets whether to temporarily hide scalable ambient shadows (SAO) whenever we interact with the Viewer.
     *
     * Default is ````true````.
     *
     * @return {Boolean} ````true```` if hiding SAO.
     */
    get hideSAO() {
        return this._hideSAO;
    }

    /**
     * Sets whether to temporarily hide scalable ambient shadows (SAO) whenever we interact with the Viewer.
     *
     * Default is ````true````.
     *
     * @param {Boolean} hideSAO ````true```` to hide SAO.
     */
    set hideSAO(hideSAO) {
        this._hideSAO = hideSAO;
    }

    /**
     * Gets whether to temporarily hide edges whenever we interact with the Viewer.
     *
     * Default is ````true````.
     *
     * @return {Boolean}  ````true```` if hiding edges.
     */
    get hideEdges() {
        return this._hideEdges;
    }

    /**
     * Sets whether to temporarily hide edges whenever we interact with the Viewer.
     *
     * Default is ````true````.
     *
     * @param {Boolean} hideEdges ````true```` to hide edges.
     */
    set hideEdges(hideEdges) {
        this._hideEdges = hideEdges;
    }

    /**
     * Gets whether to temporarily hide transparent objects whenever we interact with the Viewer.
     *
     * Does not hide X-rayed, selected, highlighted objects.
     *
     * Default is ````false````.
     *
     * @return {Boolean} ````true```` if hiding transparent objects.
     */
    get hideTransparentObjects() {
        return this._hideTransparentObjects
    }

    /**
     * Sets whether to temporarily hide transparent objects whenever we interact with the Viewer.
     *
     * Does not hide X-rayed, selected, highlighted objects.
     *
     * Default is ````false````.
     *
     * @param {Boolean} hideTransparentObjects ````true```` to hide transparent objects.
     */
    set hideTransparentObjects(hideTransparentObjects) {
        this._hideTransparentObjects = (hideTransparentObjects !== false);
    }

    /**
     * Gets whether to temporarily scale the canvas resolution whenever we interact with the Viewer.
     *
     * Default is ````false````.
     *
     * The scaling factor is configured via {@link FastNavPlugin#scaleCanvasResolutionFactor}.
     *
     * @return {Boolean} ````true```` if scaling the canvas resolution.
     */
    get scaleCanvasResolution() {
        return this._scaleCanvasResolution;
    }

    /**
     * Sets the factor to which we restore the canvas resolution scale when we stop interacting with the viewer.
     *
     * Default is ````false````.
     *
     * The scaling factor is configured via {@link FastNavPlugin#scaleCanvasResolutionFactor}.
     *
     * @param {Boolean} scaleCanvasResolution ````true```` to scale the canvas resolution.
     */
    set scaleCanvasResolution(scaleCanvasResolution) {
        this._scaleCanvasResolution = scaleCanvasResolution;
    }

    /**
     * Gets the factor to which we restore the canvas resolution scale when we stop interacting with the viewer.
     *
     * Default is ````1.0````.
     *
     * Enable canvas resolution scaling by setting {@link FastNavPlugin#scaleCanvasResolution} ````true````.
     *
     * @return {Number} Factor by scale canvas resolution when we stop interacting with the viewer.
     */
    get defaultScaleCanvasResolutionFactor() {
        return this._defaultScaleCanvasResolutionFactor;
    }

    /**
     * Sets the factor to which we restore the canvas resolution scale when we stop interacting with the viewer.
     *
     * Accepted range is ````[0.0 .. 1.0]````.
     *
     * Default is ````1.0````.
     *
     * Enable canvas resolution scaling by setting {@link FastNavPlugin#scaleCanvasResolution} ````true````.
     *
     * @param {Number} defaultScaleCanvasResolutionFactor Factor by scale canvas resolution when we stop interacting with the viewer.
     */
    set defaultScaleCanvasResolutionFactor(defaultScaleCanvasResolutionFactor) {
        this._defaultScaleCanvasResolutionFactor = defaultScaleCanvasResolutionFactor;
    }

    /**
     * Gets the factor by which we temporarily scale the canvas resolution when we interact with the viewer.
     *
     * Default is ````0.6````.
     *
     * Enable canvas resolution scaling by setting {@link FastNavPlugin#scaleCanvasResolution} ````true````.
     *
     * @return {Number} Factor by which we scale the canvas resolution.
     */
    get scaleCanvasResolutionFactor() {
        return this._scaleCanvasResolutionFactor;
    }

    /**
     * Sets the factor by which we temporarily scale the canvas resolution when we interact with the viewer.
     *
     * Accepted range is ````[0.0 .. 1.0]````.
     *
     * Default is ````0.6````.
     *
     * Enable canvas resolution scaling by setting {@link FastNavPlugin#scaleCanvasResolution} ````true````.
     *
     * @param {Number} scaleCanvasResolutionFactor Factor by which we scale the canvas resolution.
     */
    set scaleCanvasResolutionFactor(scaleCanvasResolutionFactor) {
        this._scaleCanvasResolutionFactor = scaleCanvasResolutionFactor || 0.6;
    }

    /**
     * Gets whether to have a delay before restoring normal rendering after we stop interacting with the Viewer.
     *
     * The delay duration is configured via {@link FastNavPlugin#delayBeforeRestoreSeconds}.
     *
     * Default is ````true````.
     *
     * @return {Boolean} Whether to have a delay.
     */
    get delayBeforeRestore() {
        return this._delayBeforeRestore;
    }

    /**
     * Sets whether to have a delay before restoring normal rendering after we stop interacting with the Viewer.
     *
     * The delay duration is configured via {@link FastNavPlugin#delayBeforeRestoreSeconds}.
     *
     * Default is ````true````.
     *
     * @param {Boolean} delayBeforeRestore Whether to have a delay.
     */
    set delayBeforeRestore(delayBeforeRestore) {
        this._delayBeforeRestore = delayBeforeRestore;
    }

    /**
     * Gets the delay before restoring normal rendering after we stop interacting with the Viewer.
     *
     * The delay is enabled when {@link FastNavPlugin#delayBeforeRestore} is ````true````.
     *
     * Default is ````0.5```` seconds.
     *
     * @return {Number} Delay in seconds.
     */
    get delayBeforeRestoreSeconds() {
        return this._delayBeforeRestoreSeconds;
    }

    /**
     * Sets the delay before restoring normal rendering after we stop interacting with the Viewer.
     *
     * The delay is enabled when {@link FastNavPlugin#delayBeforeRestore} is ````true````.
     *
     * Default is ````0.5```` seconds.
     *
     * @param {Number} delayBeforeRestoreSeconds Delay in seconds.
     */
    set delayBeforeRestoreSeconds(delayBeforeRestoreSeconds) {
        this._delayBeforeRestoreSeconds = delayBeforeRestoreSeconds !== null && delayBeforeRestoreSeconds !== undefined ? delayBeforeRestoreSeconds : 0.5;
    }

    /**
     * @private
     */
    send(name, value) {
    }

    /**
     * Destroys this plugin.
     */
    destroy() {
        this.viewer.scene.camera.off(this._onCameraMatrix);
        this.viewer.scene.canvas.off(this._onCanvasBoundary);
        this.viewer.scene.input.off(this._onSceneMouseDown);
        this.viewer.scene.input.off(this._onSceneMouseUp);
        this.viewer.scene.input.off(this._onSceneMouseMove);
        this.viewer.scene.off(this._onSceneTick);
        super.destroy();
    }
}

// Purpose: include this in your module to avoid
/** true if running in a browser */
// @ts-ignore process does not exist on browser
Boolean(typeof process !== 'object' || String(process) !== '[object process]' || process.browser);
// Extract node major version
const matches$1 = typeof process !== 'undefined' && process.version && /v([0-9]*)/.exec(process.version);
/** Major Node version (as a number) */
(matches$1 && parseFloat(matches$1[1])) || 0;

// Do not name these variables the same as the global objects - will break bundling
const window_ = globalThis;
const process_ = globalThis.process || {};

// based on https://github.com/cheton/is-electron
// https://github.com/electron/electron/issues/2288
/* eslint-disable complexity */
function isElectron(mockUserAgent) {
    // Renderer process
    // @ts-expect-error
    if (typeof window !== 'undefined' && window.process?.type === 'renderer') {
        return true;
    }
    // Main process
    // eslint-disable-next-line
    if (typeof process !== 'undefined' && Boolean(process.versions?.['electron'])) {
        return true;
    }
    // Detect the user agent when the `nodeIntegration` option is set to true
    const realUserAgent = typeof navigator !== 'undefined' && navigator.userAgent;
    const userAgent = realUserAgent;
    return Boolean(userAgent && userAgent.indexOf('Electron') >= 0);
}

// This function is needed in initialization stages,
/** Check if in browser by duck-typing Node context */
function isBrowser$1() {
    const isNode = 
    // @ts-expect-error
    typeof process === 'object' && String(process) === '[object process]' && !process?.browser;
    return !isNode || isElectron();
}

// Extract injected version from package.json (injected by babel plugin)
// @ts-expect-error
const VERSION$7 = "4.1.0" ;
// TODO - wish we could just export a constant
// export const isBrowser = checkIfBrowser();

// probe.gl, MIT license
function getStorage(type) {
    try {
        const storage = window[type];
        const x = '__storage_test__';
        storage.setItem(x, x);
        storage.removeItem(x);
        return storage;
    }
    catch (e) {
        return null;
    }
}
// Store keys in local storage via simple interface
class LocalStorage {
    constructor(id, defaultConfig, type = 'sessionStorage') {
        this.storage = getStorage(type);
        this.id = id;
        this.config = defaultConfig;
        this._loadConfiguration();
    }
    getConfiguration() {
        return this.config;
    }
    setConfiguration(configuration) {
        Object.assign(this.config, configuration);
        if (this.storage) {
            const serialized = JSON.stringify(this.config);
            this.storage.setItem(this.id, serialized);
        }
    }
    // Get config from persistent store, if available
    _loadConfiguration() {
        let configuration = {};
        if (this.storage) {
            const serializedConfiguration = this.storage.getItem(this.id);
            configuration = serializedConfiguration ? JSON.parse(serializedConfiguration) : {};
        }
        Object.assign(this.config, configuration);
        return this;
    }
}

// probe.gl, MIT license
/**
 * Format time
 */
function formatTime(ms) {
    let formatted;
    if (ms < 10) {
        formatted = `${ms.toFixed(2)}ms`;
    }
    else if (ms < 100) {
        formatted = `${ms.toFixed(1)}ms`;
    }
    else if (ms < 1000) {
        formatted = `${ms.toFixed(0)}ms`;
    }
    else {
        formatted = `${(ms / 1000).toFixed(2)}s`;
    }
    return formatted;
}
function leftPad(string, length = 8) {
    const padLength = Math.max(length - string.length, 0);
    return `${' '.repeat(padLength)}${string}`;
}

var COLOR;
(function (COLOR) {
    COLOR[COLOR["BLACK"] = 30] = "BLACK";
    COLOR[COLOR["RED"] = 31] = "RED";
    COLOR[COLOR["GREEN"] = 32] = "GREEN";
    COLOR[COLOR["YELLOW"] = 33] = "YELLOW";
    COLOR[COLOR["BLUE"] = 34] = "BLUE";
    COLOR[COLOR["MAGENTA"] = 35] = "MAGENTA";
    COLOR[COLOR["CYAN"] = 36] = "CYAN";
    COLOR[COLOR["WHITE"] = 37] = "WHITE";
    COLOR[COLOR["BRIGHT_BLACK"] = 90] = "BRIGHT_BLACK";
    COLOR[COLOR["BRIGHT_RED"] = 91] = "BRIGHT_RED";
    COLOR[COLOR["BRIGHT_GREEN"] = 92] = "BRIGHT_GREEN";
    COLOR[COLOR["BRIGHT_YELLOW"] = 93] = "BRIGHT_YELLOW";
    COLOR[COLOR["BRIGHT_BLUE"] = 94] = "BRIGHT_BLUE";
    COLOR[COLOR["BRIGHT_MAGENTA"] = 95] = "BRIGHT_MAGENTA";
    COLOR[COLOR["BRIGHT_CYAN"] = 96] = "BRIGHT_CYAN";
    COLOR[COLOR["BRIGHT_WHITE"] = 97] = "BRIGHT_WHITE";
})(COLOR || (COLOR = {}));
const BACKGROUND_INCREMENT = 10;
function getColor(color) {
    if (typeof color !== 'string') {
        return color;
    }
    color = color.toUpperCase();
    return COLOR[color] || COLOR.WHITE;
}
function addColor(string, color, background) {
    if (!isBrowser$1 && typeof string === 'string') {
        if (color) {
            const colorCode = getColor(color);
            string = `\u001b[${colorCode}m${string}\u001b[39m`;
        }
        if (background) {
            // background colors values are +10
            const colorCode = getColor(background);
            string = `\u001b[${colorCode + BACKGROUND_INCREMENT}m${string}\u001b[49m`;
        }
    }
    return string;
}

// Copyright (c) 2015 - 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
/**
 * Binds the "this" argument of all functions on a class instance to the instance
 * @param obj - class instance (typically a react component)
 */
function autobind(obj, predefined = ['constructor']) {
    const proto = Object.getPrototypeOf(obj);
    const propNames = Object.getOwnPropertyNames(proto);
    const object = obj;
    for (const key of propNames) {
        const value = object[key];
        if (typeof value === 'function') {
            if (!predefined.find((name) => key === name)) {
                object[key] = value.bind(obj);
            }
        }
    }
}

function assert$3(condition, message) {
    if (!condition) {
        throw new Error('Assertion failed');
    }
}

// probe.gl, MIT license
/** Get best timer available. */
function getHiResTimestamp() {
    let timestamp;
    if (isBrowser$1() && window_.performance) {
        timestamp = window_?.performance?.now?.();
    }
    else if ('hrtime' in process_) {
        // @ts-ignore
        const timeParts = process_?.hrtime?.();
        timestamp = timeParts[0] * 1000 + timeParts[1] / 1e6;
    }
    else {
        timestamp = Date.now();
    }
    return timestamp;
}

// probe.gl, MIT license
// Instrumentation in other packages may override console methods, so preserve them here
const originalConsole = {
    debug: isBrowser$1() ? console.debug || console.log : console.log,
    log: console.log,
    info: console.info,
    warn: console.warn,
    error: console.error
};
const DEFAULT_LOG_CONFIGURATION = {
    enabled: true,
    level: 0
};
function noop() { } // eslint-disable-line @typescript-eslint/no-empty-function
const cache = {};
const ONCE = { once: true };
/** A console wrapper */
class Log {
    constructor({ id } = { id: '' }) {
        this.VERSION = VERSION$7;
        this._startTs = getHiResTimestamp();
        this._deltaTs = getHiResTimestamp();
        this.userData = {};
        // TODO - fix support from throttling groups
        this.LOG_THROTTLE_TIMEOUT = 0; // Time before throttled messages are logged again
        this.id = id;
        this.userData = {};
        this._storage = new LocalStorage(`__probe-${this.id}__`, DEFAULT_LOG_CONFIGURATION);
        this.timeStamp(`${this.id} started`);
        autobind(this);
        Object.seal(this);
    }
    set level(newLevel) {
        this.setLevel(newLevel);
    }
    get level() {
        return this.getLevel();
    }
    isEnabled() {
        return this._storage.config.enabled;
    }
    getLevel() {
        return this._storage.config.level;
    }
    /** @return milliseconds, with fractions */
    getTotal() {
        return Number((getHiResTimestamp() - this._startTs).toPrecision(10));
    }
    /** @return milliseconds, with fractions */
    getDelta() {
        return Number((getHiResTimestamp() - this._deltaTs).toPrecision(10));
    }
    /** @deprecated use logLevel */
    set priority(newPriority) {
        this.level = newPriority;
    }
    /** @deprecated use logLevel */
    get priority() {
        return this.level;
    }
    /** @deprecated use logLevel */
    getPriority() {
        return this.level;
    }
    // Configure
    enable(enabled = true) {
        this._storage.setConfiguration({ enabled });
        return this;
    }
    setLevel(level) {
        this._storage.setConfiguration({ level });
        return this;
    }
    /** return the current status of the setting */
    get(setting) {
        return this._storage.config[setting];
    }
    // update the status of the setting
    set(setting, value) {
        this._storage.setConfiguration({ [setting]: value });
    }
    /** Logs the current settings as a table */
    settings() {
        if (console.table) {
            console.table(this._storage.config);
        }
        else {
            console.log(this._storage.config);
        }
    }
    // Unconditional logging
    assert(condition, message) {
        if (!condition) {
            throw new Error(message || 'Assertion failed');
        }
    }
    warn(message) {
        return this._getLogFunction(0, message, originalConsole.warn, arguments, ONCE);
    }
    error(message) {
        return this._getLogFunction(0, message, originalConsole.error, arguments);
    }
    /** Print a deprecation warning */
    deprecated(oldUsage, newUsage) {
        return this.warn(`\`${oldUsage}\` is deprecated and will be removed \
in a later version. Use \`${newUsage}\` instead`);
    }
    /** Print a removal warning */
    removed(oldUsage, newUsage) {
        return this.error(`\`${oldUsage}\` has been removed. Use \`${newUsage}\` instead`);
    }
    probe(logLevel, message) {
        return this._getLogFunction(logLevel, message, originalConsole.log, arguments, {
            time: true,
            once: true
        });
    }
    log(logLevel, message) {
        return this._getLogFunction(logLevel, message, originalConsole.debug, arguments);
    }
    info(logLevel, message) {
        return this._getLogFunction(logLevel, message, console.info, arguments);
    }
    once(logLevel, message) {
        return this._getLogFunction(logLevel, message, originalConsole.debug || originalConsole.info, arguments, ONCE);
    }
    /** Logs an object as a table */
    table(logLevel, table, columns) {
        if (table) {
            return this._getLogFunction(logLevel, table, console.table || noop, (columns && [columns]), {
                tag: getTableHeader(table)
            });
        }
        return noop;
    }
    time(logLevel, message) {
        return this._getLogFunction(logLevel, message, console.time ? console.time : console.info);
    }
    timeEnd(logLevel, message) {
        return this._getLogFunction(logLevel, message, console.timeEnd ? console.timeEnd : console.info);
    }
    timeStamp(logLevel, message) {
        return this._getLogFunction(logLevel, message, console.timeStamp || noop);
    }
    group(logLevel, message, opts = { collapsed: false }) {
        const options = normalizeArguments({ logLevel, message, opts });
        const { collapsed } = opts;
        // @ts-expect-error
        options.method = (collapsed ? console.groupCollapsed : console.group) || console.info;
        return this._getLogFunction(options);
    }
    groupCollapsed(logLevel, message, opts = {}) {
        return this.group(logLevel, message, Object.assign({}, opts, { collapsed: true }));
    }
    groupEnd(logLevel) {
        return this._getLogFunction(logLevel, '', console.groupEnd || noop);
    }
    // EXPERIMENTAL
    withGroup(logLevel, message, func) {
        this.group(logLevel, message)();
        try {
            func();
        }
        finally {
            this.groupEnd(logLevel)();
        }
    }
    trace() {
        if (console.trace) {
            console.trace();
        }
    }
    // PRIVATE METHODS
    /** Deduces log level from a variety of arguments */
    _shouldLog(logLevel) {
        return this.isEnabled() && this.getLevel() >= normalizeLogLevel(logLevel);
    }
    _getLogFunction(logLevel, message, method, args, opts) {
        if (this._shouldLog(logLevel)) {
            // normalized opts + timings
            opts = normalizeArguments({ logLevel, message, args, opts });
            method = method || opts.method;
            assert$3(method);
            opts.total = this.getTotal();
            opts.delta = this.getDelta();
            // reset delta timer
            this._deltaTs = getHiResTimestamp();
            const tag = opts.tag || opts.message;
            if (opts.once && tag) {
                if (!cache[tag]) {
                    cache[tag] = getHiResTimestamp();
                }
                else {
                    return noop;
                }
            }
            // TODO - Make throttling work with groups
            // if (opts.nothrottle || !throttle(tag, this.LOG_THROTTLE_TIMEOUT)) {
            //   return noop;
            // }
            message = decorateMessage(this.id, opts.message, opts);
            // Bind console function so that it can be called after being returned
            return method.bind(console, message, ...opts.args);
        }
        return noop;
    }
}
Log.VERSION = VERSION$7;
/**
 * Get logLevel from first argument:
 * - log(logLevel, message, args) => logLevel
 * - log(message, args) => 0
 * - log({logLevel, ...}, message, args) => logLevel
 * - log({logLevel, message, args}) => logLevel
 */
function normalizeLogLevel(logLevel) {
    if (!logLevel) {
        return 0;
    }
    let resolvedLevel;
    switch (typeof logLevel) {
        case 'number':
            resolvedLevel = logLevel;
            break;
        case 'object':
            // Backward compatibility
            // TODO - deprecate `priority`
            // @ts-expect-error
            resolvedLevel = logLevel.logLevel || logLevel.priority || 0;
            break;
        default:
            return 0;
    }
    // 'log level must be a number'
    assert$3(Number.isFinite(resolvedLevel) && resolvedLevel >= 0);
    return resolvedLevel;
}
/**
 * "Normalizes" the various argument patterns into an object with known types
 * - log(logLevel, message, args) => {logLevel, message, args}
 * - log(message, args) => {logLevel: 0, message, args}
 * - log({logLevel, ...}, message, args) => {logLevel, message, args}
 * - log({logLevel, message, args}) => {logLevel, message, args}
 */
function normalizeArguments(opts) {
    const { logLevel, message } = opts;
    opts.logLevel = normalizeLogLevel(logLevel);
    // We use `arguments` instead of rest parameters (...args) because IE
    // does not support the syntax. Rest parameters is transpiled to code with
    // perf impact. Doing it here instead avoids constructing args when logging is
    // disabled.
    // TODO - remove when/if IE support is dropped
    const args = opts.args ? Array.from(opts.args) : [];
    // args should only contain arguments that appear after `message`
    // eslint-disable-next-line no-empty
    while (args.length && args.shift() !== message) { }
    switch (typeof logLevel) {
        case 'string':
        case 'function':
            if (message !== undefined) {
                args.unshift(message);
            }
            opts.message = logLevel;
            break;
        case 'object':
            Object.assign(opts, logLevel);
            break;
    }
    // Resolve functions into strings by calling them
    if (typeof opts.message === 'function') {
        opts.message = opts.message();
    }
    const messageType = typeof opts.message;
    // 'log message must be a string' or object
    assert$3(messageType === 'string' || messageType === 'object');
    // original opts + normalized opts + opts arg + fixed up message
    return Object.assign(opts, { args }, opts.opts);
}
function decorateMessage(id, message, opts) {
    if (typeof message === 'string') {
        const time = opts.time ? leftPad(formatTime(opts.total)) : '';
        message = opts.time ? `${id}: ${time}  ${message}` : `${id}: ${message}`;
        message = addColor(message, opts.color, opts.background);
    }
    return message;
}
function getTableHeader(table) {
    for (const key in table) {
        for (const title in table[key]) {
            return title || 'untitled';
        }
    }
    return 'empty';
}

// loaders.gl
// Version constant cannot be imported, it needs to correspond to the build version of **this** module.
// __VERSION__ is injected by babel-plugin-version-inline
// @ts-ignore TS2304: Cannot find name '__VERSION__'.
const VERSION$6 = "4.3.2" ;
const version$1 = VERSION$6[0] >= '0' && VERSION$6[0] <= '9' ? `v${VERSION$6}` : '';
// Make sure we set the global variable
function createLog() {
    const log = new Log({ id: 'loaders.gl' });
    globalThis.loaders = globalThis.loaders || {};
    globalThis.loaders.log = log;
    globalThis.loaders.version = version$1;
    globalThis.probe = globalThis.probe || {};
    globalThis.probe.loaders = log;
    return log;
}
createLog();
function getVersion() {
    if (!globalThis._loadersgl_?.version) {
        globalThis._loadersgl_ = globalThis._loadersgl_ || {};
        // __VERSION__ is injected by babel-plugin-version-inline
        {
            globalThis._loadersgl_.version = "4.3.2";
        }
    }
    return globalThis._loadersgl_.version;
}
getVersion();

// loaders.gl
/** true if running in the browser, false if running in Node.js */
// @ts-ignore process.browser
typeof process !== 'object' || String(process) !== '[object process]' || process.browser;
// Extract node major version
const matches = typeof process !== 'undefined' && process.version && /v([0-9]*)/.exec(process.version);
/** Version of Node.js if running under Node, otherwise 0 */
(matches && parseFloat(matches[1])) || 0;

// loaders.gl
new Log({ id: 'loaders.gl' });

// @ts-ignore TS2339: Property does not exist on type
globalThis.loaders?.parseImageNode;
const ARRAY_CONSTRUCTOR_TO_WEBGL_CONSTANT = [
    [Int8Array, 5120],
    [Uint8Array, 5121],
    [Int16Array, 5122],
    [Uint16Array, 5123],
    [Uint32Array, 5125],
    [Float32Array, 5126],
    [Float64Array, 5130]
];
new Map(ARRAY_CONSTRUCTOR_TO_WEBGL_CONSTANT);

// math.gl
const DEFAULT_CONFIG = {
    EPSILON: 1e-12,
    debug: false,
    precision: 4,
    printTypes: false,
    printDegrees: false,
    printRowMajor: true,
    _cartographicRadians: false
};
// Configuration is truly global as of v3.6 to ensure single config even if multiple copies of math.gl
// Multiple copies of config can be quite tricky to debug...
globalThis.mathgl = globalThis.mathgl || { config: { ...DEFAULT_CONFIG } };
const config = globalThis.mathgl.config;
/**
 * Formats a value into a string
 * @param value
 * @param param1
 * @returns
 */
function formatValue(value, { precision = config.precision } = {}) {
    value = round(value);
    // get rid of trailing zeros
    return `${parseFloat(value.toPrecision(precision))}`;
}
/**
 * Check if value is an "array"
 * Returns `true` if value is either an array or a typed array
 * Note: returns `false` for `ArrayBuffer` and `DataView` instances
 * @note isTypedArray and isNumericArray are often more useful in TypeScript
 */
function isArray(value) {
    return Array.isArray(value) || (ArrayBuffer.isView(value) && !(value instanceof DataView));
}
/* eslint-disable */
/**
 * Compares any two math objects, using `equals` method if available.
 * @param a
 * @param b
 * @param epsilon
 * @returns
 */
function equals(a, b, epsilon) {
    const oldEpsilon = config.EPSILON;
    try {
        if (a === b) {
            return true;
        }
        if (isArray(a) && isArray(b)) {
            if (a.length !== b.length) {
                return false;
            }
            for (let i = 0; i < a.length; ++i) {
                // eslint-disable-next-line max-depth
                if (!equals(a[i], b[i])) {
                    return false;
                }
            }
            return true;
        }
        if (a && a.equals) {
            return a.equals(b);
        }
        if (b && b.equals) {
            return b.equals(a);
        }
        if (typeof a === 'number' && typeof b === 'number') {
            return Math.abs(a - b) <= config.EPSILON * Math.max(1, Math.abs(a), Math.abs(b));
        }
        return false;
    }
    finally {
        config.EPSILON = oldEpsilon;
    }
}
// HELPERS
function round(value) {
    return Math.round(value / config.EPSILON) * config.EPSILON;
}

// math.gl
/** Base class for vectors and matrices */
class MathArray extends Array {
    // Common methods
    /**
     * Clone the current object
     * @returns a new copy of this object
     */
    clone() {
        // @ts-expect-error TS2351: Cannot use 'new' with an expression whose type lacks a call or construct signature.
        return new this.constructor().copy(this); // eslint-disable-line
    }
    fromArray(array, offset = 0) {
        for (let i = 0; i < this.ELEMENTS; ++i) {
            this[i] = array[i + offset];
        }
        return this.check();
    }
    toArray(targetArray = [], offset = 0) {
        for (let i = 0; i < this.ELEMENTS; ++i) {
            targetArray[offset + i] = this[i];
        }
        return targetArray;
    }
    toObject(targetObject) {
        return targetObject;
    }
    from(arrayOrObject) {
        return Array.isArray(arrayOrObject)
            ? this.copy(arrayOrObject)
            : // @ts-ignore
                this.fromObject(arrayOrObject);
    }
    to(arrayOrObject) {
        // @ts-ignore
        if (arrayOrObject === this) {
            return this;
        }
        // @ts-expect-error TS2339: Property 'toObject' does not exist on type 'MathArray'.
        return isArray(arrayOrObject) ? this.toArray(arrayOrObject) : this.toObject(arrayOrObject);
    }
    toTarget(target) {
        return target ? this.to(target) : this;
    }
    /** @deprecated */
    toFloat32Array() {
        return new Float32Array(this);
    }
    toString() {
        return this.formatString(config);
    }
    /** Formats string according to options */
    formatString(opts) {
        let string = '';
        for (let i = 0; i < this.ELEMENTS; ++i) {
            string += (i > 0 ? ', ' : '') + formatValue(this[i], opts);
        }
        return `${opts.printTypes ? this.constructor.name : ''}[${string}]`;
    }
    equals(array) {
        if (!array || this.length !== array.length) {
            return false;
        }
        for (let i = 0; i < this.ELEMENTS; ++i) {
            if (!equals(this[i], array[i])) {
                return false;
            }
        }
        return true;
    }
    exactEquals(array) {
        if (!array || this.length !== array.length) {
            return false;
        }
        for (let i = 0; i < this.ELEMENTS; ++i) {
            if (this[i] !== array[i]) {
                return false;
            }
        }
        return true;
    }
    // Modifiers
    /** Negates all values in this object */
    negate() {
        for (let i = 0; i < this.ELEMENTS; ++i) {
            this[i] = -this[i];
        }
        return this.check();
    }
    lerp(a, b, t) {
        if (t === undefined) {
            return this.lerp(this, a, b);
        }
        for (let i = 0; i < this.ELEMENTS; ++i) {
            const ai = a[i];
            const endValue = typeof b === 'number' ? b : b[i];
            this[i] = ai + t * (endValue - ai);
        }
        return this.check();
    }
    /** Minimal */
    min(vector) {
        for (let i = 0; i < this.ELEMENTS; ++i) {
            this[i] = Math.min(vector[i], this[i]);
        }
        return this.check();
    }
    /** Maximal */
    max(vector) {
        for (let i = 0; i < this.ELEMENTS; ++i) {
            this[i] = Math.max(vector[i], this[i]);
        }
        return this.check();
    }
    clamp(minVector, maxVector) {
        for (let i = 0; i < this.ELEMENTS; ++i) {
            this[i] = Math.min(Math.max(this[i], minVector[i]), maxVector[i]);
        }
        return this.check();
    }
    add(...vectors) {
        for (const vector of vectors) {
            for (let i = 0; i < this.ELEMENTS; ++i) {
                this[i] += vector[i];
            }
        }
        return this.check();
    }
    subtract(...vectors) {
        for (const vector of vectors) {
            for (let i = 0; i < this.ELEMENTS; ++i) {
                this[i] -= vector[i];
            }
        }
        return this.check();
    }
    scale(scale) {
        if (typeof scale === 'number') {
            for (let i = 0; i < this.ELEMENTS; ++i) {
                this[i] *= scale;
            }
        }
        else {
            for (let i = 0; i < this.ELEMENTS && i < scale.length; ++i) {
                this[i] *= scale[i];
            }
        }
        return this.check();
    }
    /**
     * Multiplies all elements by `scale`
     * Note: `Matrix4.multiplyByScalar` only scales its 3x3 "minor"
     */
    multiplyByScalar(scalar) {
        for (let i = 0; i < this.ELEMENTS; ++i) {
            this[i] *= scalar;
        }
        return this.check();
    }
    // Debug checks
    /** Throws an error if array length is incorrect or contains illegal values */
    check() {
        if (config.debug && !this.validate()) {
            throw new Error(`math.gl: ${this.constructor.name} some fields set to invalid numbers'`);
        }
        return this;
    }
    /** Returns false if the array length is incorrect or contains illegal values */
    validate() {
        let valid = this.length === this.ELEMENTS;
        for (let i = 0; i < this.ELEMENTS; ++i) {
            valid = valid && Number.isFinite(this[i]);
        }
        return valid;
    }
    // three.js compatibility
    /** @deprecated */
    sub(a) {
        return this.subtract(a);
    }
    /** @deprecated */
    setScalar(a) {
        for (let i = 0; i < this.ELEMENTS; ++i) {
            this[i] = a;
        }
        return this.check();
    }
    /** @deprecated */
    addScalar(a) {
        for (let i = 0; i < this.ELEMENTS; ++i) {
            this[i] += a;
        }
        return this.check();
    }
    /** @deprecated */
    subScalar(a) {
        return this.addScalar(-a);
    }
    /** @deprecated */
    multiplyScalar(scalar) {
        // Multiplies all elements
        // `Matrix4.scale` only scales its 3x3 "minor"
        for (let i = 0; i < this.ELEMENTS; ++i) {
            this[i] *= scalar;
        }
        return this.check();
    }
    /** @deprecated */
    divideScalar(a) {
        return this.multiplyByScalar(1 / a);
    }
    /** @deprecated */
    clampScalar(min, max) {
        for (let i = 0; i < this.ELEMENTS; ++i) {
            this[i] = Math.min(Math.max(this[i], min), max);
        }
        return this.check();
    }
    /** @deprecated */
    get elements() {
        return this;
    }
}

// math.gl
function validateVector(v, length) {
    if (v.length !== length) {
        return false;
    }
    // Could be arguments "array" (v.every not availasble)
    for (let i = 0; i < v.length; ++i) {
        if (!Number.isFinite(v[i])) {
            return false;
        }
    }
    return true;
}
function checkNumber(value) {
    if (!Number.isFinite(value)) {
        throw new Error(`Invalid number ${JSON.stringify(value)}`);
    }
    return value;
}
function checkVector(v, length, callerName = '') {
    if (config.debug && !validateVector(v, length)) {
        throw new Error(`math.gl: ${callerName} some fields set to invalid numbers'`);
    }
    return v;
}

// math.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
function assert(condition, message) {
    if (!condition) {
        throw new Error(`math.gl assertion ${message}`);
    }
}

// math.gl
/** Base class for vectors with at least 2 elements */
class Vector extends MathArray {
    // ACCESSORS
    get x() {
        return this[0];
    }
    set x(value) {
        this[0] = checkNumber(value);
    }
    get y() {
        return this[1];
    }
    set y(value) {
        this[1] = checkNumber(value);
    }
    /**
     * Returns the length of the vector from the origin to the point described by this vector
     *
     * @note `length` is a reserved word for Arrays, so `v.length()` will return number of elements
     * Instead we provide `len` and `magnitude`
     */
    len() {
        return Math.sqrt(this.lengthSquared());
    }
    /**
     * Returns the length of the vector from the origin to the point described by this vector
     */
    magnitude() {
        return this.len();
    }
    /**
     * Returns the squared length of the vector from the origin to the point described by this vector
     */
    lengthSquared() {
        let length = 0;
        for (let i = 0; i < this.ELEMENTS; ++i) {
            length += this[i] * this[i];
        }
        return length;
    }
    /**
     * Returns the squared length of the vector from the origin to the point described by this vector
     */
    magnitudeSquared() {
        return this.lengthSquared();
    }
    distance(mathArray) {
        return Math.sqrt(this.distanceSquared(mathArray));
    }
    distanceSquared(mathArray) {
        let length = 0;
        for (let i = 0; i < this.ELEMENTS; ++i) {
            const dist = this[i] - mathArray[i];
            length += dist * dist;
        }
        return checkNumber(length);
    }
    dot(mathArray) {
        let product = 0;
        for (let i = 0; i < this.ELEMENTS; ++i) {
            product += this[i] * mathArray[i];
        }
        return checkNumber(product);
    }
    // MODIFIERS
    normalize() {
        const length = this.magnitude();
        if (length !== 0) {
            for (let i = 0; i < this.ELEMENTS; ++i) {
                this[i] /= length;
            }
        }
        return this.check();
    }
    multiply(...vectors) {
        for (const vector of vectors) {
            for (let i = 0; i < this.ELEMENTS; ++i) {
                this[i] *= vector[i];
            }
        }
        return this.check();
    }
    divide(...vectors) {
        for (const vector of vectors) {
            for (let i = 0; i < this.ELEMENTS; ++i) {
                this[i] /= vector[i];
            }
        }
        return this.check();
    }
    // THREE.js compatibility
    lengthSq() {
        return this.lengthSquared();
    }
    distanceTo(vector) {
        return this.distance(vector);
    }
    distanceToSquared(vector) {
        return this.distanceSquared(vector);
    }
    getComponent(i) {
        assert(i >= 0 && i < this.ELEMENTS, 'index is out of range');
        return checkNumber(this[i]);
    }
    setComponent(i, value) {
        assert(i >= 0 && i < this.ELEMENTS, 'index is out of range');
        this[i] = value;
        return this.check();
    }
    addVectors(a, b) {
        return this.copy(a).add(b);
    }
    subVectors(a, b) {
        return this.copy(a).subtract(b);
    }
    multiplyVectors(a, b) {
        return this.copy(a).multiply(b);
    }
    addScaledVector(a, b) {
        // @ts-expect-error error TS2351: Cannot use 'new' with an expression whose type lacks a call or construct signature.
        return this.add(new this.constructor(a).multiplyScalar(b));
    }
}

// @eslint-disable
let ARRAY_TYPE = typeof Float32Array !== 'undefined' ? Float32Array : Array;

// @eslint-disable
/**
 * 2 Dimensional Vector
 * @module vec2
 */
/**
 * Creates a new, empty vec2
 *
 * @returns a new 2D vector
 */
function create$1() {
    const out = new ARRAY_TYPE(2);
    if (ARRAY_TYPE != Float32Array) {
        out[0] = 0;
        out[1] = 0;
    }
    return out;
}
/**
 * Transforms the vec2 with a mat3
 * 3rd vector component is implicitly '1'
 *
 * @param {NumericArray} out the receiving vector
 * @param {Readonly<NumericArray>} a the vector to transform
 * @param {ReadonlyMat3} m matrix to transform with
 * @returns {NumericArray} out
 */
function transformMat3$1(out, a, m) {
    const x = a[0];
    const y = a[1];
    out[0] = m[0] * x + m[3] * y + m[6];
    out[1] = m[1] * x + m[4] * y + m[7];
    return out;
}
/**
 * Perform some operation over an array of vec2s.
 *
 * @param {Array} a the array of vectors to iterate over
 * @param {Number} stride Number of elements between the start of each vec2. If 0 assumes tightly packed
 * @param {Number} offset Number of elements to skip at the beginning of the array
 * @param {Number} count Number of vec2s to iterate over. If 0 iterates over entire array
 * @param {Function} fn Function to call for each vector in the array
 * @param {Object} [arg] additional argument to pass to fn
 * @returns {Array} a
 * @function
 */
((function () {
    const vec = create$1();
    return function (a, stride, offset, count, fn, arg) {
        let i;
        let l;
        if (!stride) {
            stride = 2;
        }
        if (!offset) {
            offset = 0;
        }
        if (count) {
            l = Math.min(count * stride + offset, a.length);
        }
        else {
            l = a.length;
        }
        for (i = offset; i < l; i += stride) {
            vec[0] = a[i];
            vec[1] = a[i + 1];
            fn(vec, vec, arg);
            a[i] = vec[0];
            a[i + 1] = vec[1];
        }
        return a;
    };
}))();

// math.gl
// vec3 additions
// Transform as vector, only uses 3x3 minor matrix
function vec3_transformMat4AsVector(out, a, m) {
    const x = a[0];
    const y = a[1];
    const z = a[2];
    const w = m[3] * x + m[7] * y + m[11] * z || 1.0;
    out[0] = (m[0] * x + m[4] * y + m[8] * z) / w;
    out[1] = (m[1] * x + m[5] * y + m[9] * z) / w;
    out[2] = (m[2] * x + m[6] * y + m[10] * z) / w;
    return out;
}
function vec3_transformMat2(out, a, m) {
    const x = a[0];
    const y = a[1];
    out[0] = m[0] * x + m[2] * y;
    out[1] = m[1] * x + m[3] * y;
    out[2] = a[2];
    return out;
}
function vec4_transformMat3(out, a, m) {
    const x = a[0];
    const y = a[1];
    const z = a[2];
    out[0] = m[0] * x + m[3] * y + m[6] * z;
    out[1] = m[1] * x + m[4] * y + m[7] * z;
    out[2] = m[2] * x + m[5] * y + m[8] * z;
    out[3] = a[3];
    return out;
}

// @eslint-disable
/**
 * 3 Dimensional Vector
 * @module vec3
 */
/**
 * Creates a new, empty vec3
 *
 * @returns {vec3} a new 3D vector
 */
function create() {
    const out = new ARRAY_TYPE(3);
    if (ARRAY_TYPE != Float32Array) {
        out[0] = 0;
        out[1] = 0;
        out[2] = 0;
    }
    return out;
}
/**
 * Calculates the dot product of two vec3's
 *
 * @param {ReadonlyVec3} a the first operand
 * @param {ReadonlyVec3} b the second operand
 * @returns {Number} dot product of a and b
 */
function dot(a, b) {
    return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
/**
 * Computes the cross product of two vec3's
 *
 * @param {vec3} out the receiving vector
 * @param {ReadonlyVec3} a the first operand
 * @param {ReadonlyVec3} b the second operand
 * @returns {vec3} out
 */
function cross(out, a, b) {
    const ax = a[0];
    const ay = a[1];
    const az = a[2];
    const bx = b[0];
    const by = b[1];
    const bz = b[2];
    out[0] = ay * bz - az * by;
    out[1] = az * bx - ax * bz;
    out[2] = ax * by - ay * bx;
    return out;
}
/**
 * Transforms the vec3 with a mat4.
 * 4th vector component is implicitly '1'
 *
 * @param {vec3} out the receiving vector
 * @param {ReadonlyVec3} a the vector to transform
 * @param {ReadonlyMat4} m matrix to transform with
 * @returns {vec3} out
 */
function transformMat4(out, a, m) {
    const x = a[0];
    const y = a[1];
    const z = a[2];
    let w = m[3] * x + m[7] * y + m[11] * z + m[15];
    w = w || 1.0;
    out[0] = (m[0] * x + m[4] * y + m[8] * z + m[12]) / w;
    out[1] = (m[1] * x + m[5] * y + m[9] * z + m[13]) / w;
    out[2] = (m[2] * x + m[6] * y + m[10] * z + m[14]) / w;
    return out;
}
/**
 * Transforms the vec3 with a mat3.
 *
 * @param {vec3} out the receiving vector
 * @param {ReadonlyVec3} a the vector to transform
 * @param {ReadonlyMat3} m the 3x3 matrix to transform with
 * @returns {vec3} out
 */
function transformMat3(out, a, m) {
    const x = a[0];
    const y = a[1];
    const z = a[2];
    out[0] = x * m[0] + y * m[3] + z * m[6];
    out[1] = x * m[1] + y * m[4] + z * m[7];
    out[2] = x * m[2] + y * m[5] + z * m[8];
    return out;
}
/**
 * Transforms the vec3 with a quat
 * Can also be used for dual quaternions. (Multiply it with the real part)
 *
 * @param {vec3} out the receiving vector
 * @param {ReadonlyVec3} a the vector to transform
 * @param {ReadonlyQuat} q quaternion to transform with
 * @returns {vec3} out
 */
function transformQuat(out, a, q) {
    // benchmarks: https://jsperf.com/quaternion-transform-vec3-implementations-fixed
    const qx = q[0];
    const qy = q[1];
    const qz = q[2];
    const qw = q[3];
    const x = a[0];
    const y = a[1];
    const z = a[2];
    // var qvec = [qx, qy, qz];
    // var uv = vec3.cross([], qvec, a);
    let uvx = qy * z - qz * y;
    let uvy = qz * x - qx * z;
    let uvz = qx * y - qy * x;
    // var uuv = vec3.cross([], qvec, uv);
    let uuvx = qy * uvz - qz * uvy;
    let uuvy = qz * uvx - qx * uvz;
    let uuvz = qx * uvy - qy * uvx;
    // vec3.scale(uv, uv, 2 * w);
    const w2 = qw * 2;
    uvx *= w2;
    uvy *= w2;
    uvz *= w2;
    // vec3.scale(uuv, uuv, 2);
    uuvx *= 2;
    uuvy *= 2;
    uuvz *= 2;
    // return vec3.add(out, a, vec3.add(out, uv, uuv));
    out[0] = x + uvx + uuvx;
    out[1] = y + uvy + uuvy;
    out[2] = z + uvz + uuvz;
    return out;
}
/**
 * Rotate a 3D vector around the x-axis
 * @param {vec3} out The receiving vec3
 * @param {ReadonlyVec3} a The vec3 point to rotate
 * @param {ReadonlyVec3} b The origin of the rotation
 * @param {Number} rad The angle of rotation in radians
 * @returns {vec3} out
 */
function rotateX(out, a, b, rad) {
    const p = [];
    const r = [];
    // Translate point to the origin
    p[0] = a[0] - b[0];
    p[1] = a[1] - b[1];
    p[2] = a[2] - b[2];
    // perform rotation
    r[0] = p[0];
    r[1] = p[1] * Math.cos(rad) - p[2] * Math.sin(rad);
    r[2] = p[1] * Math.sin(rad) + p[2] * Math.cos(rad);
    // translate to correct position
    out[0] = r[0] + b[0];
    out[1] = r[1] + b[1];
    out[2] = r[2] + b[2];
    return out;
}
/**
 * Rotate a 3D vector around the y-axis
 * @param {vec3} out The receiving vec3
 * @param {ReadonlyVec3} a The vec3 point to rotate
 * @param {ReadonlyVec3} b The origin of the rotation
 * @param {Number} rad The angle of rotation in radians
 * @returns {vec3} out
 */
function rotateY(out, a, b, rad) {
    const p = [];
    const r = [];
    // Translate point to the origin
    p[0] = a[0] - b[0];
    p[1] = a[1] - b[1];
    p[2] = a[2] - b[2];
    // perform rotation
    r[0] = p[2] * Math.sin(rad) + p[0] * Math.cos(rad);
    r[1] = p[1];
    r[2] = p[2] * Math.cos(rad) - p[0] * Math.sin(rad);
    // translate to correct position
    out[0] = r[0] + b[0];
    out[1] = r[1] + b[1];
    out[2] = r[2] + b[2];
    return out;
}
/**
 * Rotate a 3D vector around the z-axis
 * @param {vec3} out The receiving vec3
 * @param {ReadonlyVec3} a The vec3 point to rotate
 * @param {ReadonlyVec3} b The origin of the rotation
 * @param {Number} rad The angle of rotation in radians
 * @returns {vec3} out
 */
function rotateZ(out, a, b, rad) {
    const p = [];
    const r = [];
    // Translate point to the origin
    p[0] = a[0] - b[0];
    p[1] = a[1] - b[1];
    p[2] = a[2] - b[2];
    // perform rotation
    r[0] = p[0] * Math.cos(rad) - p[1] * Math.sin(rad);
    r[1] = p[0] * Math.sin(rad) + p[1] * Math.cos(rad);
    r[2] = p[2];
    // translate to correct position
    out[0] = r[0] + b[0];
    out[1] = r[1] + b[1];
    out[2] = r[2] + b[2];
    return out;
}
/**
 * Get the angle between two 3D vectors
 * @param {ReadonlyVec3} a The first operand
 * @param {ReadonlyVec3} b The second operand
 * @returns {Number} The angle in radians
 */
function angle(a, b) {
    const ax = a[0];
    const ay = a[1];
    const az = a[2];
    const bx = b[0];
    const by = b[1];
    const bz = b[2];
    const mag = Math.sqrt((ax * ax + ay * ay + az * az) * (bx * bx + by * by + bz * bz));
    const cosine = mag && dot(a, b) / mag;
    return Math.acos(Math.min(Math.max(cosine, -1), 1));
}
/**
 * Perform some operation over an array of vec3s.
 *
 * @param {Array} a the array of vectors to iterate over
 * @param {Number} stride Number of elements between the start of each vec3. If 0 assumes tightly packed
 * @param {Number} offset Number of elements to skip at the beginning of the array
 * @param {Number} count Number of vec3s to iterate over. If 0 iterates over entire array
 * @param {Function} fn Function to call for each vector in the array
 * @param {Object} [arg] additional argument to pass to fn
 * @returns {Array} a
 * @function
 */
((function () {
    const vec = create();
    return function (a, stride, offset, count, fn, arg) {
        let i;
        let l;
        if (!stride) {
            stride = 3;
        }
        if (!offset) {
            offset = 0;
        }
        if (count) {
            l = Math.min(count * stride + offset, a.length);
        }
        else {
            l = a.length;
        }
        for (i = offset; i < l; i += stride) {
            vec[0] = a[i];
            vec[1] = a[i + 1];
            vec[2] = a[i + 2];
            fn(vec, vec, arg);
            a[i] = vec[0];
            a[i + 1] = vec[1];
            a[i + 2] = vec[2];
        }
        return a;
    };
}))();

// math.gl
const ORIGIN = [0, 0, 0];
let ZERO;
/**
 * Three-element vector class with common linear algebra operations.
 * Subclass of Array<number> meaning that it is highly compatible with other libraries
 */
class Vector3 extends Vector {
    static get ZERO() {
        if (!ZERO) {
            ZERO = new Vector3(0, 0, 0);
            Object.freeze(ZERO);
        }
        return ZERO;
    }
    /**
     * @class
     * @param x
     * @param y
     * @param z
     */
    constructor(x = 0, y = 0, z = 0) {
        // PERF NOTE: initialize elements as double precision numbers
        super(-0, -0, -0);
        if (arguments.length === 1 && isArray(x)) {
            this.copy(x);
        }
        else {
            // this.set(x, y, z);
            if (config.debug) {
                checkNumber(x);
                checkNumber(y);
                checkNumber(z);
            }
            // @ts-expect-error TS2412: Property '0' of type 'number | [number, number, number]' is not assignable to numeric index type 'number'
            this[0] = x;
            this[1] = y;
            this[2] = z;
        }
    }
    set(x, y, z) {
        this[0] = x;
        this[1] = y;
        this[2] = z;
        return this.check();
    }
    copy(array) {
        this[0] = array[0];
        this[1] = array[1];
        this[2] = array[2];
        return this.check();
    }
    fromObject(object) {
        if (config.debug) {
            checkNumber(object.x);
            checkNumber(object.y);
            checkNumber(object.z);
        }
        this[0] = object.x;
        this[1] = object.y;
        this[2] = object.z;
        return this.check();
    }
    toObject(object) {
        object.x = this[0];
        object.y = this[1];
        object.z = this[2];
        return object;
    }
    // Getters/setters
    get ELEMENTS() {
        return 3;
    }
    get z() {
        return this[2];
    }
    set z(value) {
        this[2] = checkNumber(value);
    }
    // ACCESSORS
    angle(vector) {
        return angle(this, vector);
    }
    // MODIFIERS
    cross(vector) {
        cross(this, this, vector);
        return this.check();
    }
    rotateX({ radians, origin = ORIGIN }) {
        rotateX(this, this, origin, radians);
        return this.check();
    }
    rotateY({ radians, origin = ORIGIN }) {
        rotateY(this, this, origin, radians);
        return this.check();
    }
    rotateZ({ radians, origin = ORIGIN }) {
        rotateZ(this, this, origin, radians);
        return this.check();
    }
    // Transforms
    // transforms as point (4th component is implicitly 1)
    transform(matrix4) {
        return this.transformAsPoint(matrix4);
    }
    // transforms as point (4th component is implicitly 1)
    transformAsPoint(matrix4) {
        transformMat4(this, this, matrix4);
        return this.check();
    }
    // transforms as vector  (4th component is implicitly 0, ignores translation. slightly faster)
    transformAsVector(matrix4) {
        vec3_transformMat4AsVector(this, this, matrix4);
        return this.check();
    }
    transformByMatrix3(matrix3) {
        transformMat3(this, this, matrix3);
        return this.check();
    }
    transformByMatrix2(matrix2) {
        vec3_transformMat2(this, this, matrix2);
        return this.check();
    }
    transformByQuaternion(quaternion) {
        transformQuat(this, this, quaternion);
        return this.check();
    }
}

// math.gl
/** Base class for matrices */
class Matrix extends MathArray {
    // fromObject(object) {
    //   const array = object.elements;
    //   return this.fromRowMajor(array);
    // }
    // toObject(object) {
    //   const array = object.elements;
    //   this.toRowMajor(array);
    //   return object;
    // }
    // TODO better override formatString?
    toString() {
        let string = '[';
        if (config.printRowMajor) {
            string += 'row-major:';
            for (let row = 0; row < this.RANK; ++row) {
                for (let col = 0; col < this.RANK; ++col) {
                    string += ` ${this[col * this.RANK + row]}`;
                }
            }
        }
        else {
            string += 'column-major:';
            for (let i = 0; i < this.ELEMENTS; ++i) {
                string += ` ${this[i]}`;
            }
        }
        string += ']';
        return string;
    }
    getElementIndex(row, col) {
        return col * this.RANK + row;
    }
    // By default assumes row major indices
    getElement(row, col) {
        return this[col * this.RANK + row];
    }
    // By default assumes row major indices
    setElement(row, col, value) {
        this[col * this.RANK + row] = checkNumber(value);
        return this;
    }
    getColumn(columnIndex, result = new Array(this.RANK).fill(-0)) {
        const firstIndex = columnIndex * this.RANK;
        for (let i = 0; i < this.RANK; ++i) {
            result[i] = this[firstIndex + i];
        }
        return result;
    }
    setColumn(columnIndex, columnVector) {
        const firstIndex = columnIndex * this.RANK;
        for (let i = 0; i < this.RANK; ++i) {
            this[firstIndex + i] = columnVector[i];
        }
        return this;
    }
}

/**
 * Transpose the values of a mat3
 *
 * @param {mat3} out the receiving matrix
 * @param {ReadonlyMat3} a the source matrix
 * @returns {mat3} out
 */
function transpose(out, a) {
    // If we are transposing ourselves we can skip a few steps but have to cache some values
    if (out === a) {
        const a01 = a[1];
        const a02 = a[2];
        const a12 = a[5];
        out[1] = a[3];
        out[2] = a[6];
        out[3] = a01;
        out[5] = a[7];
        out[6] = a02;
        out[7] = a12;
    }
    else {
        out[0] = a[0];
        out[1] = a[3];
        out[2] = a[6];
        out[3] = a[1];
        out[4] = a[4];
        out[5] = a[7];
        out[6] = a[2];
        out[7] = a[5];
        out[8] = a[8];
    }
    return out;
}
/**
 * Inverts a mat3
 *
 * @param {mat3} out the receiving matrix
 * @param {ReadonlyMat3} a the source matrix
 * @returns {mat3} out
 */
function invert(out, a) {
    const a00 = a[0];
    const a01 = a[1];
    const a02 = a[2];
    const a10 = a[3];
    const a11 = a[4];
    const a12 = a[5];
    const a20 = a[6];
    const a21 = a[7];
    const a22 = a[8];
    const b01 = a22 * a11 - a12 * a21;
    const b11 = -a22 * a10 + a12 * a20;
    const b21 = a21 * a10 - a11 * a20;
    // Calculate the determinant
    let det = a00 * b01 + a01 * b11 + a02 * b21;
    if (!det) {
        return null;
    }
    det = 1.0 / det;
    out[0] = b01 * det;
    out[1] = (-a22 * a01 + a02 * a21) * det;
    out[2] = (a12 * a01 - a02 * a11) * det;
    out[3] = b11 * det;
    out[4] = (a22 * a00 - a02 * a20) * det;
    out[5] = (-a12 * a00 + a02 * a10) * det;
    out[6] = b21 * det;
    out[7] = (-a21 * a00 + a01 * a20) * det;
    out[8] = (a11 * a00 - a01 * a10) * det;
    return out;
}
/**
 * Calculates the determinant of a mat3
 *
 * @param {ReadonlyMat3} a the source matrix
 * @returns {Number} determinant of a
 */
function determinant(a) {
    const a00 = a[0];
    const a01 = a[1];
    const a02 = a[2];
    const a10 = a[3];
    const a11 = a[4];
    const a12 = a[5];
    const a20 = a[6];
    const a21 = a[7];
    const a22 = a[8];
    return (a00 * (a22 * a11 - a12 * a21) + a01 * (-a22 * a10 + a12 * a20) + a02 * (a21 * a10 - a11 * a20));
}
/**
 * Multiplies two mat3's
 *
 * @param {mat3} out the receiving matrix
 * @param {ReadonlyMat3} a the first operand
 * @param {ReadonlyMat3} b the second operand
 * @returns {mat3} out
 */
function multiply(out, a, b) {
    const a00 = a[0];
    const a01 = a[1];
    const a02 = a[2];
    const a10 = a[3];
    const a11 = a[4];
    const a12 = a[5];
    const a20 = a[6];
    const a21 = a[7];
    const a22 = a[8];
    const b00 = b[0];
    const b01 = b[1];
    const b02 = b[2];
    const b10 = b[3];
    const b11 = b[4];
    const b12 = b[5];
    const b20 = b[6];
    const b21 = b[7];
    const b22 = b[8];
    out[0] = b00 * a00 + b01 * a10 + b02 * a20;
    out[1] = b00 * a01 + b01 * a11 + b02 * a21;
    out[2] = b00 * a02 + b01 * a12 + b02 * a22;
    out[3] = b10 * a00 + b11 * a10 + b12 * a20;
    out[4] = b10 * a01 + b11 * a11 + b12 * a21;
    out[5] = b10 * a02 + b11 * a12 + b12 * a22;
    out[6] = b20 * a00 + b21 * a10 + b22 * a20;
    out[7] = b20 * a01 + b21 * a11 + b22 * a21;
    out[8] = b20 * a02 + b21 * a12 + b22 * a22;
    return out;
}
/**
 * Translate a mat3 by the given vector
 *
 * @param {mat3} out the receiving matrix
 * @param {ReadonlyMat3} a the matrix to translate
 * @param {ReadonlyVec2} v vector to translate by
 * @returns {mat3} out
 */
function translate(out, a, v) {
    const a00 = a[0];
    const a01 = a[1];
    const a02 = a[2];
    const a10 = a[3];
    const a11 = a[4];
    const a12 = a[5];
    const a20 = a[6];
    const a21 = a[7];
    const a22 = a[8];
    const x = v[0];
    const y = v[1];
    out[0] = a00;
    out[1] = a01;
    out[2] = a02;
    out[3] = a10;
    out[4] = a11;
    out[5] = a12;
    out[6] = x * a00 + y * a10 + a20;
    out[7] = x * a01 + y * a11 + a21;
    out[8] = x * a02 + y * a12 + a22;
    return out;
}
/**
 * Rotates a mat3 by the given angle
 *
 * @param {mat3} out the receiving matrix
 * @param {ReadonlyMat3} a the matrix to rotate
 * @param {Number} rad the angle to rotate the matrix by
 * @returns {mat3} out
 */
function rotate(out, a, rad) {
    const a00 = a[0];
    const a01 = a[1];
    const a02 = a[2];
    const a10 = a[3];
    const a11 = a[4];
    const a12 = a[5];
    const a20 = a[6];
    const a21 = a[7];
    const a22 = a[8];
    const s = Math.sin(rad);
    const c = Math.cos(rad);
    out[0] = c * a00 + s * a10;
    out[1] = c * a01 + s * a11;
    out[2] = c * a02 + s * a12;
    out[3] = c * a10 - s * a00;
    out[4] = c * a11 - s * a01;
    out[5] = c * a12 - s * a02;
    out[6] = a20;
    out[7] = a21;
    out[8] = a22;
    return out;
}
/**
 * Scales the mat3 by the dimensions in the given vec2
 *
 * @param {mat3} out the receiving matrix
 * @param {ReadonlyMat3} a the matrix to scale
 * @param {ReadonlyVec2} v the vec2 to scale the matrix by
 * @returns {mat3} out
 **/
function scale(out, a, v) {
    const x = v[0];
    const y = v[1];
    out[0] = x * a[0];
    out[1] = x * a[1];
    out[2] = x * a[2];
    out[3] = y * a[3];
    out[4] = y * a[4];
    out[5] = y * a[5];
    out[6] = a[6];
    out[7] = a[7];
    out[8] = a[8];
    return out;
}
/**
 * Calculates a 3x3 matrix from the given quaternion
 *
 * @param {mat3} out mat3 receiving operation result
 * @param {ReadonlyQuat} q Quaternion to create matrix from
 *
 * @returns {mat3} out
 */
function fromQuat(out, q) {
    const x = q[0];
    const y = q[1];
    const z = q[2];
    const w = q[3];
    const x2 = x + x;
    const y2 = y + y;
    const z2 = z + z;
    const xx = x * x2;
    const yx = y * x2;
    const yy = y * y2;
    const zx = z * x2;
    const zy = z * y2;
    const zz = z * z2;
    const wx = w * x2;
    const wy = w * y2;
    const wz = w * z2;
    out[0] = 1 - yy - zz;
    out[3] = yx - wz;
    out[6] = zx + wy;
    out[1] = yx + wz;
    out[4] = 1 - xx - zz;
    out[7] = zy - wx;
    out[2] = zx - wy;
    out[5] = zy + wx;
    out[8] = 1 - xx - yy;
    return out;
}

// math.gl
// eslint-disable-next-line no-shadow
var INDICES;
(function (INDICES) {
    INDICES[INDICES["COL0ROW0"] = 0] = "COL0ROW0";
    INDICES[INDICES["COL0ROW1"] = 1] = "COL0ROW1";
    INDICES[INDICES["COL0ROW2"] = 2] = "COL0ROW2";
    INDICES[INDICES["COL1ROW0"] = 3] = "COL1ROW0";
    INDICES[INDICES["COL1ROW1"] = 4] = "COL1ROW1";
    INDICES[INDICES["COL1ROW2"] = 5] = "COL1ROW2";
    INDICES[INDICES["COL2ROW0"] = 6] = "COL2ROW0";
    INDICES[INDICES["COL2ROW1"] = 7] = "COL2ROW1";
    INDICES[INDICES["COL2ROW2"] = 8] = "COL2ROW2";
})(INDICES || (INDICES = {}));
const IDENTITY_MATRIX = Object.freeze([1, 0, 0, 0, 1, 0, 0, 0, 1]);
/**
 * A 3x3 matrix with common linear algebra operations
 * Subclass of Array<number> meaning that it is highly compatible with other libraries
 */
class Matrix3 extends Matrix {
    static get IDENTITY() {
        return getIdentityMatrix();
    }
    static get ZERO() {
        return getZeroMatrix();
    }
    get ELEMENTS() {
        return 9;
    }
    get RANK() {
        return 3;
    }
    get INDICES() {
        return INDICES;
    }
    constructor(array, ...args) {
        // PERF NOTE: initialize elements as double precision numbers
        super(-0, -0, -0, -0, -0, -0, -0, -0, -0);
        if (arguments.length === 1 && Array.isArray(array)) {
            this.copy(array);
        }
        else if (args.length > 0) {
            this.copy([array, ...args]);
        }
        else {
            this.identity();
        }
    }
    copy(array) {
        // Element wise copy for performance
        this[0] = array[0];
        this[1] = array[1];
        this[2] = array[2];
        this[3] = array[3];
        this[4] = array[4];
        this[5] = array[5];
        this[6] = array[6];
        this[7] = array[7];
        this[8] = array[8];
        return this.check();
    }
    // Constructors
    identity() {
        return this.copy(IDENTITY_MATRIX);
    }
    /**
     *
     * @param object
     * @returns self
     */
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    fromObject(object) {
        return this.check();
    }
    /** Calculates a 3x3 matrix from the given quaternion
     * q quat  Quaternion to create matrix from
     */
    fromQuaternion(q) {
        fromQuat(this, q);
        return this.check();
    }
    /**
     * accepts column major order, stores in column major order
     */
    // eslint-disable-next-line max-params
    set(m00, m10, m20, m01, m11, m21, m02, m12, m22) {
        this[0] = m00;
        this[1] = m10;
        this[2] = m20;
        this[3] = m01;
        this[4] = m11;
        this[5] = m21;
        this[6] = m02;
        this[7] = m12;
        this[8] = m22;
        return this.check();
    }
    /**
     * accepts row major order, stores as column major
     */
    // eslint-disable-next-line max-params
    setRowMajor(m00, m01, m02, m10, m11, m12, m20, m21, m22) {
        this[0] = m00;
        this[1] = m10;
        this[2] = m20;
        this[3] = m01;
        this[4] = m11;
        this[5] = m21;
        this[6] = m02;
        this[7] = m12;
        this[8] = m22;
        return this.check();
    }
    // Accessors
    determinant() {
        return determinant(this);
    }
    // Modifiers
    transpose() {
        transpose(this, this);
        return this.check();
    }
    /** Invert a matrix. Note that this can fail if the matrix is not invertible */
    invert() {
        invert(this, this);
        return this.check();
    }
    // Operations
    multiplyLeft(a) {
        multiply(this, a, this);
        return this.check();
    }
    multiplyRight(a) {
        multiply(this, this, a);
        return this.check();
    }
    rotate(radians) {
        rotate(this, this, radians);
        return this.check();
    }
    scale(factor) {
        if (Array.isArray(factor)) {
            scale(this, this, factor);
        }
        else {
            scale(this, this, [factor, factor]);
        }
        return this.check();
    }
    translate(vec) {
        translate(this, this, vec);
        return this.check();
    }
    // Transforms
    transform(vector, result) {
        let out;
        switch (vector.length) {
            case 2:
                out = transformMat3$1(result || [-0, -0], vector, this);
                break;
            case 3:
                out = transformMat3(result || [-0, -0, -0], vector, this);
                break;
            case 4:
                out = vec4_transformMat3(result || [-0, -0, -0, -0], vector, this);
                break;
            default:
                throw new Error('Illegal vector');
        }
        checkVector(out, vector.length);
        return out;
    }
    /** @deprecated */
    transformVector(vector, result) {
        return this.transform(vector, result);
    }
    /** @deprecated */
    transformVector2(vector, result) {
        return this.transform(vector, result);
    }
    /** @deprecated */
    transformVector3(vector, result) {
        return this.transform(vector, result);
    }
}
let ZERO_MATRIX3;
let IDENTITY_MATRIX3 = null;
function getZeroMatrix() {
    if (!ZERO_MATRIX3) {
        ZERO_MATRIX3 = new Matrix3([0, 0, 0, 0, 0, 0, 0, 0, 0]);
        Object.freeze(ZERO_MATRIX3);
    }
    return ZERO_MATRIX3;
}
function getIdentityMatrix() {
    if (!IDENTITY_MATRIX3) {
        IDENTITY_MATRIX3 = new Matrix3();
        Object.freeze(IDENTITY_MATRIX3);
    }
    return IDENTITY_MATRIX3;
}
new Vector3();
new Matrix3();
new Matrix3();

/**
 * @desc Default initial properties for {@link Entity}s loaded from models accompanied by metadata.
 *
 * When loading a model, plugins such as {@link XKTLoaderPlugin} create
 * a tree of {@link Entity}s that represent the model. These loaders can optionally load metadata, to create
 * a {@link MetaModel} corresponding to the root {@link Entity}, with a {@link MetaObject} corresponding to each
 * object {@link Entity} within the tree.
 *
 * @type {{String:Object}}
 */
const IFCObjectDefaults = {

    DEFAULT: {
    }
};

/**
 * @private
 */
function CubeTextureCanvas(viewer, navCubeScene, cfg = {}) {

    const cubeColor = "lightgrey";
    const cubeHighlightColor = cfg.hoverColor || "rgba(0,0,0,0.4)";
    const textColor = cfg.textColor || "black";
    const parentNode = cfg.canvasElement || document.body;

    const height = 500;
    const width = height + (height / 3);
    const scale = width / 24;

    const facesZUp = [
        {boundary: [6, 6, 6, 6], color: cfg.frontColor || cfg.color || "#55FF55"},
        {boundary: [18, 6, 6, 6], color: cfg.backColor || cfg.color || "#55FF55"},
        {boundary: [12, 6, 6, 6], color: cfg.rightColor || cfg.color || "#FF5555"},
        {boundary: [0, 6, 6, 6], color: cfg.leftColor || cfg.color || "#FF5555"},
        {boundary: [6, 0, 6, 6], color: cfg.topColor || cfg.color || "#7777FF"},
        {boundary: [6, 12, 6, 6], color: cfg.bottomColor || cfg.color || "#7777FF"}
    ];

    const areasZUp = [
        {label: "NavCube.front", boundaries: [[7, 7, 4, 4]], dir: [0, 1, 0], up: [0, 0, 1]},
        {label: "NavCube.back", boundaries: [[19, 7, 4, 4]], dir: [0, -1, 0], up: [0, 0, 1]},
        {label: "NavCube.right", boundaries: [[13, 7, 4, 4]], dir: [-1, 0, 0], up: [0, 0, 1]},
        {label: "NavCube.left", boundaries: [[1, 7, 4, 4]], dir: [1, 0, 0], up: [0, 0, 1]},
        {label: "NavCube.top", boundaries: [[7, 1, 4, 4]], dir: [0, 0, -1], up: [0, 1, 0]},
        {label: "NavCube.bottom", boundaries: [[7, 13, 4, 4]], dir: [0, 0, 1], up: [0, -1, 0]},
        {boundaries: [[7, 5, 4, 2]], dir: [0, 1, -1], up: [0, 1, 1]},
        {boundaries: [[1, 6, 4, 1], [6, 1, 1, 4]], dir: [1, 0, -1], up: [1, 0, 1]},
        {boundaries: [[7, 0, 4, 1], [19, 6, 4, 1]], dir: [0, -1, -1], up: [0, -1, 1]},
        {boundaries: [[13, 6, 4, 1], [11, 1, 1, 4]], dir: [-1, 0, -1], up: [-1, 0, 1]},
        {boundaries: [[7, 11, 4, 2]], dir: [0, 1, 1], up: [0, -1, 1]},
        {boundaries: [[1, 11, 4, 1], [6, 13, 1, 4]], dir: [1, 0, 1], up: [-1, 0, 1]},
        {boundaries: [[7, 17, 4, 1], [19, 11, 4, 1]], dir: [0, -1, 1], up: [0, 1, 1]},
        {boundaries: [[13, 11, 4, 1], [11, 13, 1, 4]], dir: [-1, 0, 1], up: [1, 0, 1]},
        {boundaries: [[5, 7, 2, 4]], dir: [1, 1, 0], up: [0, 0, 1]},
        {boundaries: [[11, 7, 2, 4]], dir: [-1, 1, 0], up: [0, 0, 1]},
        {boundaries: [[17, 7, 2, 4]], dir: [-1, -1, 0], up: [0, 0, 1]},
        {boundaries: [[0, 7, 1, 4], [23, 7, 1, 4]], dir: [1, -1, 0], up: [0, 0, 1]},
        {boundaries: [[5, 11, 2, 2]], dir: [1, 1, 1], up: [-1, -1, 1]},
        {boundaries: [[23, 11, 1, 1], [6, 17, 1, 1], [0, 11, 1, 1]], dir: [1, -1, 1], up: [-1, 1, 1]},
        {boundaries: [[5, 5, 2, 2]], dir: [1, 1, -1], up: [1, 1, 1]},
        {boundaries: [[11, 17, 1, 1], [17, 11, 2, 1]], dir: [-1, -1, 1], up: [1, 1, 1]},
        {boundaries: [[17, 6, 2, 1], [11, 0, 1, 1]], dir: [-1, -1, -1], up: [-1, -1, 1]},
        {boundaries: [[11, 11, 2, 2]], dir: [-1, 1, 1], up: [1, -1, 1]},
        {boundaries: [[0, 6, 1, 1], [6, 0, 1, 1], [23, 6, 1, 1]], dir: [1, -1, -1], up: [1, -1, 1]},
        {boundaries: [[11, 5, 2, 2]], dir: [-1, 1, -1], up: [-1, 1, 1]}
    ];

    [
        {boundary: [6, 6, 6, 6], color: cfg.frontColor || cfg.color || "#55FF55"},
        {boundary: [18, 6, 6, 6], color: cfg.backColor || cfg.color || "#55FF55"},
        {boundary: [12, 6, 6, 6], color: cfg.rightColor || cfg.color || "#FF5555"},
        {boundary: [0, 6, 6, 6], color: cfg.leftColor || cfg.color || "#FF5555"},
        {boundary: [6, 0, 6, 6], color: cfg.topColor || cfg.color || "#7777FF"},
        {boundary: [6, 12, 6, 6], color: cfg.bottomColor || cfg.color || "#7777FF"}
    ];

    const areasYUp = [

        // Faces

        {yUp: "", label: "NavCube.front", boundaries: [[7, 7, 4, 4]], dir: [0, 0, -1], up: [0, 1, 0]},
        {label: "NavCube.back", boundaries: [[19, 7, 4, 4]], dir: [0, 0, 1], up: [0, 1, 0]},
        {label: "NavCube.right", boundaries: [[13, 7, 4, 4]], dir: [-1, 0, 0], up: [0, 1, 0]},
        {label: "NavCube.left", boundaries: [[1, 7, 4, 4]], dir: [1, 0, 0], up: [0, 1, 0]},
        {label: "NavCube.top", boundaries: [[7, 1, 4, 4]], dir: [0, -1, 0], up: [0, 0, -1]},
        {label: "NavCube.bottom", boundaries: [[7, 13, 4, 4]], dir: [0, 1, 0], up: [0, 0, 1]},
        {boundaries: [[7, 5, 4, 2]], dir: [0, -0.7071, -0.7071], up: [0, 0.7071, -0.7071]}, // Top-front edge
        {boundaries: [[1, 6, 4, 1], [6, 1, 1, 4]], dir: [1, -1, 0], up: [1, 1, 0]},  // Top-left edge
        {boundaries: [[7, 0, 4, 1], [19, 6, 4, 1]], dir: [0, -0.7071, 0.7071], up: [0, 0.7071, 0.7071]}, // Top-back edge
        {boundaries: [[13, 6, 4, 1], [11, 1, 1, 4]], dir: [-1, -1, 0], up: [-1, 1, 0]}, // Top-right edge
        {boundaries: [[7, 11, 4, 2]], dir: [0, 1, -1], up: [0, 1, 1]},  // Bottom-front edge
        {boundaries: [[1, 11, 4, 1], [6, 13, 1, 4]], dir: [1, 1, 0], up: [-1, 1, 0]}, // Bottom-left edge
        {boundaries: [[7, 17, 4, 1], [19, 11, 4, 1]], dir: [0, 1, 1], up: [0, 1, -1]}, // Bottom-back edge
        {boundaries: [[13, 11, 4, 1], [11, 13, 1, 4]], dir: [-1, 1, 0], up: [1, 1, 0]}, // Bottom-right edge
        {boundaries: [[5, 7, 2, 4]], dir: [1, 0, -1], up: [0, 1, 0]},// Front-left edge
        {boundaries: [[11, 7, 2, 4]], dir: [-1, 0, -1], up: [0, 1, 0]}, // Front-right edge
        {boundaries: [[17, 7, 2, 4]], dir: [-1, 0, 1], up: [0, 1, 0]},// Back-right edge
        {boundaries: [[0, 7, 1, 4], [23, 7, 1, 4]], dir: [1, 0, 1], up: [0, 1, 0]},// Back-left edge
        {boundaries: [[5, 11, 2, 2]], "dir": [0.5, 0.7071, -0.5], "up": [-0.5, 0.7071, 0.5]}, // Bottom-left-front corner
        {boundaries: [[23, 11, 1, 1], [6, 17, 1, 1], [0, 11, 1, 1]],"dir": [0.5, 0.7071, 0.5],"up": [-0.5, 0.7071, -0.5]},// Bottom-back-left corner
        {boundaries: [[5, 5, 2, 2]], "dir": [0.5, -0.7071, -0.5], "up": [0.5, 0.7071, -0.5]}, // Left-front-top corner
        {boundaries: [[11, 17, 1, 1], [17, 11, 2, 1]], "dir": [-0.5, 0.7071, 0.5], "up": [0.5, 0.7071, -0.5]}, // Bottom-back-right corner
        {boundaries: [[17, 6, 2, 1], [11, 0, 1, 1]], "dir": [-0.5, -0.7071, 0.5], "up": [-0.5, 0.7071, 0.5]}, // Top-back-right corner
        {boundaries: [[11, 11, 2, 2]], "dir": [-0.5, 0.7071, -0.5], "up": [0.5, 0.7071, 0.5]}, // Bottom-front-right corner
        {boundaries: [[0, 6, 1, 1], [6, 0, 1, 1], [23, 6, 1, 1]], "dir": [0.5, -0.7071, 0.5], "up": [0.5, 0.7071, 0.5]},// Top-back-left corner
        {boundaries: [[11, 5, 2, 2]], "dir": [-0.5, -0.7071, -0.5], "up": [-0.5, 0.7071, -0.5]}// Top-front-right corner
    ];

    for (let i = 0, len = areasZUp.length; i < len; i++) {
        math.normalizeVec3(areasZUp[i].dir, areasZUp[i].dir);
        math.normalizeVec3(areasZUp[i].up, areasZUp[i].up);
    }

    for (let i = 0, len = areasYUp.length; i < len; i++) {
        math.normalizeVec3(areasYUp[i].dir, areasYUp[i].dir);
        math.normalizeVec3(areasYUp[i].up, areasYUp[i].up);
    }
    var areas = areasYUp;

    this._textureCanvas = document.createElement('canvas');
    this._textureCanvas.width = width;
    this._textureCanvas.height = height;
    this._textureCanvas.style.width = width + "px";
    this._textureCanvas.style.height = height + "px";
    this._textureCanvas.style.padding = "0";
    this._textureCanvas.style.margin = "0";
    this._textureCanvas.style.top = "0";
    this._textureCanvas.style.background = cubeColor;
    this._textureCanvas.style.position = "absolute";
    this._textureCanvas.style.opacity = "1.0";
    this._textureCanvas.style.visibility = "hidden";
    this._textureCanvas.style["z-index"] = 2000000;

    parentNode.appendChild(this._textureCanvas);

    const context = this._textureCanvas.getContext("2d");

    let zUp = false;

    function paint() {

        for (let i = 0, len = facesZUp.length; i < len; i++) {
            const face = facesZUp[i];
            const boundary = face.boundary;
            const xmin = Math.round(boundary[0] * scale);
            const ymin = Math.round(boundary[1] * scale);
            const width = Math.round(boundary[2] * scale);
            const height = Math.round(boundary[3] * scale);
            context.fillStyle = face.color;
            context.fillRect(xmin, ymin, width, height);
        }

        for (let i = 0, len = areas.length; i < len; i++) {
            let xmin;
            let ymin;
            let width;
            let height;
            const area = areas[i];

            const boundaries = area.boundaries;
            for (var j = 0, lenj = boundaries.length; j < lenj; j++) {
                const boundary = boundaries[j];
                xmin = Math.round(boundary[0] * scale);
                ymin = Math.round(boundary[1] * scale);
                width = Math.round(boundary[2] * scale);
                height = Math.round(boundary[3] * scale);
                if (area.highlighted) {
                    context.fillStyle = area.highlighted ? cubeHighlightColor : (area.color || cubeColor);
                    context.fillRect(xmin, ymin, width, height);
                }
            }
            if (area.label) {
                context.fillStyle = textColor;
                context.font = '60px sans-serif';
                context.textAlign = "center";
                var xcenter = xmin + (width * 0.5);
                var ycenter = ymin + (height * 0.7);
                context.fillText(translateLabel(area.label), xcenter, ycenter, 80);
            }
        }

        navCubeScene.glRedraw();
    }

    const translateLabel = (function () {

        const swizzleYUp = {
            "NavCube.front": "NavCube.front",
            "NavCube.back": "NavCube.back",
            "NavCube.right": "NavCube.right",
            "NavCube.left": "NavCube.left",
            "NavCube.top": "NavCube.top",
            "NavCube.bottom": "NavCube.bottom"
        };

        const swizzleZUp = {
            "NavCube.front": "NavCube.front",
            "NavCube.back": "NavCube.back",
            "NavCube.right": "NavCube.right",
            "NavCube.left": "NavCube.left",
            "NavCube.top": "NavCube.top",
            "NavCube.bottom": "NavCube.bottom"
        };

        const defaultLabels = {
            "NavCube.front": "FRONT",
            "NavCube.back": "BACK",
            "NavCube.right": "RIGHT",
            "NavCube.left": "LEFT",
            "NavCube.top": "TOP",
            "NavCube.bottom": "BOTTOM"
        };

        return function (key) {
            const swizzle = zUp ? swizzleZUp : swizzleYUp;
            const swizzledKey = swizzle ? swizzle[key] : null;
            if (swizzledKey) {
                return viewer.localeService.translate(swizzledKey) || defaultLabels[swizzledKey] || swizzledKey;
            }
            return key;
        };
    })();

    this.setZUp = function () {
        zUp = true;
        areas = areasZUp;
        this.clear();
    };

    this.setYUp = function () {
        zUp = false;
        areas = areasYUp;
        this.clear();
    };

    this.clear = function () {
        context.fillStyle = cubeColor;
        context.fillRect(0, 0, width, height);
        for (var i = 0, len = areas.length; i < len; i++) {
            const area = areas[i];
            area.highlighted = false;
        }
        paint();
    };

    this.getArea = function (uv) {
        const s = uv[0] * width;
        const t = height - (uv[1] * height); // Correct for our texture Y-flipping
        for (var i = 0, len = areas.length; i < len; i++) {
            const area = areas[i];
            const boundaries = area.boundaries;
            for (var j = 0, lenj = boundaries.length; j < lenj; j++) {
                const boundary = boundaries[j];
                if (s >= (boundary[0] * scale) && s <= ((boundary[0] + boundary[2]) * scale) && t >= (boundary[1] * scale) && t <= ((boundary[1] + boundary[3]) * scale)) {
                    return i;
                }
            }
        }
        return -1;
    };

    this.setAreaHighlighted = function (areaId, highlighted) {
        var area = areas[areaId];
        if (!area) {
            throw "Area not found: " + areaId;
        }
        area.highlighted = !!highlighted;
        paint();
    };

    this.getAreaDir = function (areaId) {
        var area = areas[areaId];
        if (!area) {
            throw "Unknown area: " + areaId;
        }
        return area.dir;
    };

    this.getAreaUp = function (areaId) {
        var area = areas[areaId];
        if (!area) {
            throw "Unknown area: " + areaId;
        }
        return area.up;
    };

    this.getImage = function () {
        return this._textureCanvas;
    };

    this.destroy = function () {
        if (this._textureCanvas) {
            this._textureCanvas.parentNode.removeChild(this._textureCanvas);
            this._textureCanvas = null;
        }
    };
}

const tempVec3a$4 = math.vec3();
const tempVec3b$1 = math.vec3();
math.mat4();

/**
 * {@link Viewer} plugin that lets us look at the entire {@link Scene} from along a chosen axis or diagonal.
 *
 *  [<img src="https://user-images.githubusercontent.com/83100/55674490-c93c2e00-58b5-11e9-8a28-eb08876947c0.gif">](https://xeokit.github.io/xeokit-sdk/examples/index.html#gizmos_NavCubePlugin)
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#gizmos_NavCubePlugin)]
 *
 * ## Overview
 *
 * * Rotating the NavCube causes the Viewer's {@link Camera} to orbit its current
 * point-of-interest. Conversely, orbiting the Camera causes the NavCube to rotate accordingly.
 * * The faces of the NavCube are aligned with the Viewer's {@link Scene}'s World-space coordinate axis. Clicking on a face moves
 * the Camera to look at the entire Scene along the corresponding axis. Clicking on an edge or a corner looks at
 * the entire Scene along a diagonal.
 * * The NavCube can be configured to either jump or fly the Camera to each new position. We can configure how tightly the
 * NavCube fits the Scene to view, and when flying, we can configure how fast it flies. We can also configure whether the
 * NavCube fits all objects to view, or just the currently visible objects. See below for a usage example.
 * * Clicking the NavCube also sets {@link CameraControl#pivotPos} to the center of the fitted objects.
 *
 * ## Usage
 *
 * In the example below, we'll create a Viewer and add a NavCubePlugin, which will create a NavCube gizmo in the canvas
 * with the given ID. Then we'll use the {@link XKTLoaderPlugin} to load a model into the Viewer's Scene. We can then
 * use the NavCube to look at the model along each axis or diagonal.
 *
 * ````JavaScript
 * import {Viewer, XKTLoaderPlugin, NavCubePlugin} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas"
 * });
 *
 * viewer.camera.eye = [-3.93, 2.85, 27.01];
 * viewer.camera.look = [4.40, 3.72, 8.89];
 * viewer.camera.up = [-0.01, 0.99, 0.03];
 *
 * const navCube = new NavCubePlugin(viewer, {
 *
 *     canvasID: "myNavCubeCanvas",
 *
 *     visible: true,         // Initially visible (default)
 *
 *     cameraFly: true,       // Fly camera to each selected axis/diagonal
 *     cameraFitFOV: 45,      // How much field-of-view the scene takes once camera has fitted it to view
 *     cameraFlyDuration: 0.5,// How long (in seconds) camera takes to fly to each new axis/diagonal
 *
 *     fitVisible: false,     // Fit whole scene, including invisible objects (default)
 *
 *     synchProjection: false // Keep NavCube in perspective projection, even when camera switches to ortho (default)
 * });
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const model = xktLoader.load({
 *     id: "myModel",
 *     src: "./models/xkt/Duplex.ifc.xkt",
 *     edges: true
 * });
 * ````
 */
class NavCubePlugin extends Plugin {

    /**
     * @constructor
     * @param {Viewer} viewer The Viewer.
     * @param {Object} cfg NavCubePlugin configuration.
     * @param {String} [cfg.id="NavCube"] Optional ID for this plugin, so that we can find it within {@link Viewer#plugins}.
     * @param {String} [cfg.canvasId] ID of an existing HTML canvas to display the NavCube - either this or canvasElement is mandatory. When both values are given, the element reference is always preferred to the ID.
     * @param {HTMLCanvasElement} [cfg.canvasElement] Reference of an existing HTML canvas to display the NavCube - either this or canvasId is mandatory. When both values are given, the element reference is always preferred to the ID.
     * @param {Boolean} [cfg.visible=true] Initial visibility.
     * @param {Boolean} [cfg.shadowVisible=true] Whether the shadow of the cube is visible.
     * @param {String} [cfg.cameraFly=true] Whether the {@link Camera} flies or jumps to each selected axis or diagonal.
     * @param {String} [cfg.cameraFitFOV=45] How much of the field-of-view, in degrees, that the 3D scene should fill the {@link Canvas} when the {@link Camera} moves to an axis or diagonal.
     * @param {String} [cfg.cameraFlyDuration=0.5] When flying the {@link Camera} to each new axis or diagonal, how long, in seconds, that the Camera takes to get there.
     * @param {String} [cfg.color="lightgrey"] Custom uniform color for the faces of the NavCube.
     * @param {String} [cfg.frontColor="#55FF55"] Custom color for the front face of the NavCube. Overrides ````color````.
     * @param {String} [cfg.backColor="#55FF55"] Custom color for the back face of the NavCube. Overrides ````color````.
     * @param {String} [cfg.leftColor="#FF5555"] Custom color for the left face of the NavCube. Overrides ````color````.
     * @param {String} [cfg.rightColor="#FF5555"] Custom color for the right face of the NavCube. Overrides ````color````.
     * @param {String} [cfg.topColor="#5555FF"] Custom color for the top face of the NavCube. Overrides ````color````.
     * @param {String} [cfg.bottomColor="#5555FF"] Custom color for the bottom face of the NavCube. Overrides ````color````.
     * @param {String} [cfg.hoverColor="rgba(0,0,0,0.4)"] Custom color for highlighting regions on the NavCube as we hover the pointer over them.
     * @param {String} [cfg.textColor="black"] Custom text color for labels of the NavCube.
     * @param {Boolean} [cfg.fitVisible=false] Sets whether the axis, corner and edge-aligned views will fit the
     * view to the entire {@link Scene} or just to visible object-{@link Entity}s. Entitys are visible objects when {@link Entity#isObject} and {@link Entity#visible} are both ````true````.
     * @param {Boolean} [cfg.synchProjection=false] Sets whether the NavCube switches between perspective and orthographic projections in synchrony with the {@link Camera}. When ````false````, the NavCube will always be rendered with perspective projection.
     * @param {Boolean} [cfg.isProjectNorth] sets whether the NavCube switches between true north and project north - using the project north offset angle.
     * @param {number} [cfg.projectNorthOffsetAngle] sets the NavCube project north offset angle - when the {@link isProjectNorth} is true.
     */
    constructor(viewer, cfg = {}) {

        super("NavCube", viewer, cfg);

        viewer.navCube = this;

        try {
            this._navCubeScene = new Scene(viewer, {
                canvasId: cfg.canvasId,
                canvasElement: cfg.canvasElement,
                transparent: true,
            });

            this._navCubeCanvas = this._navCubeScene.canvas.canvas;

            this._navCubeScene.input.keyboardEnabled = false; // Don't want keyboard input in the NavCube

        } catch (error) {
            this.error(error);
            return;
        }

        const navCubeScene = this._navCubeScene;

        navCubeScene.clearLights();

        new DirLight(navCubeScene, {dir: [0.4, -0.4, 0.8], color: [0.8, 1.0, 1.0], intensity: 1.0, space: "view"});
        new DirLight(navCubeScene, {dir: [-0.8, -0.3, -0.4], color: [0.8, 0.8, 0.8], intensity: 1.0, space: "view"});
        new DirLight(navCubeScene, {dir: [0.8, -0.6, -0.8], color: [1.0, 1.0, 1.0], intensity: 1.0, space: "view"});

        this._navCubeCamera = navCubeScene.camera;
        this._navCubeCamera.ortho.scale = 7.0;
        this._navCubeCamera.ortho.near = 0.1;
        this._navCubeCamera.ortho.far = 2000;

        navCubeScene.edgeMaterial.edgeColor = [0.2, 0.2, 0.2];
        navCubeScene.edgeMaterial.edgeAlpha = 0.6;

        this._zUp = Boolean(viewer.camera.zUp);

        var self = this;

        this.setIsProjectNorth(cfg.isProjectNorth);
        this.setProjectNorthOffsetAngle(cfg.projectNorthOffsetAngle);

        const rotateTrueNorth = (function () {
            const trueNorthMatrix = math.mat4();
            return function (dir, vec, dest) {
                math.identityMat4(trueNorthMatrix);
                math.rotationMat4v(dir * self._projectNorthOffsetAngle * math.DEGTORAD, [0, 1, 0], trueNorthMatrix);
                return math.transformVec3(trueNorthMatrix, vec, dest)
            }
        }());

        this._synchCamera = (function () {
            var matrix = math.rotationMat4c(-90 * math.DEGTORAD, 1, 0, 0);
            var eyeLookVec = math.vec3();
            var eyeLookVecCube = math.vec3();
            var upCube = math.vec3();
            return function () {
                var eye = viewer.camera.eye;
                var look = viewer.camera.look;
                var up = viewer.camera.up;
                eyeLookVec = math.mulVec3Scalar(math.normalizeVec3(math.subVec3(eye, look, eyeLookVec)), 5);

                if (self._isProjectNorth && self._projectNorthOffsetAngle) {
                    eyeLookVec = rotateTrueNorth(-1, eyeLookVec, tempVec3a$4);
                    up = rotateTrueNorth(-1, up, tempVec3b$1);
                }

                if (self._zUp) { // +Z up
                    math.transformVec3(matrix, eyeLookVec, eyeLookVecCube);
                    math.transformVec3(matrix, up, upCube);
                    self._navCubeCamera.look = [0, 0, 0];
                    self._navCubeCamera.eye = math.transformVec3(matrix, eyeLookVec, eyeLookVecCube);
                    self._navCubeCamera.up = math.transformPoint3(matrix, up, upCube);
                } else { // +Y up
                    self._navCubeCamera.look = [0, 0, 0];
                    self._navCubeCamera.eye = eyeLookVec;
                    self._navCubeCamera.up = up;
                }
            };
        }());

        this._cubeTextureCanvas = new CubeTextureCanvas(viewer, navCubeScene, cfg);

        this._cubeSampler = new Texture(navCubeScene, {
            image: this._cubeTextureCanvas.getImage(),
            flipY: true,
            wrapS: ClampToEdgeWrapping,
            wrapT: ClampToEdgeWrapping
        });

        this._cubeMesh = new Mesh(navCubeScene, {
            geometry: new ReadableGeometry(navCubeScene, {
                primitive: "triangles",
                normals: [
                    0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0,
                    0, 1, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, -1,
                    0, 0, -1, 0, 0, -1, 0, 0, -1
                ],
                positions: [
                    1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, 1, 1, 1, 1, -1, -1,
                    1, -1, -1, 1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1,
                    1, 1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, -1
                ],
                uv: [
                    0.5, 0.6666, 0.25, 0.6666, 0.25, 0.3333, 0.5, 0.3333, 0.5, 0.6666, 0.5, 0.3333, 0.75, 0.3333, 0.75, 0.6666,
                    0.5, 0.6666, 0.5, 1, 0.25, 1, 0.25, 0.6666, 0.25, 0.6666, 0.0, 0.6666, 0.0, 0.3333, 0.25, 0.3333,
                    0.25, 0, 0.50, 0, 0.50, 0.3333, 0.25, 0.3333, 0.75, 0.3333, 1.0, 0.3333, 1.0, 0.6666, 0.75, 0.6666
                ],
                indices: [
                    0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16,
                    18, 19, 20, 21, 22, 20, 22, 23
                ]
            }),
            material: new PhongMaterial(navCubeScene, {
                diffuse: [0.4, 0.4, 0.4],
                specular: [0.4, 0.4, 0.4],
                emissive: [.6, .6, .6],
                diffuseMap: this._cubeSampler,
                emissiveMap: this._cubeSampler
            }),
            visible: true,
            edges: true
        });

        this._shadow = cfg.shadowVisible === false ? null : new Mesh(navCubeScene, {
            geometry: new ReadableGeometry(navCubeScene, buildCylinderGeometry({
                center: [0, 0, 0],
                radiusTop: 0.001,
                radiusBottom: 1.4,
                height: 0.01,
                radialSegments: 20,
                heightSegments: 1,
                openEnded: true
            })),
            material: new PhongMaterial(navCubeScene, {
                diffuse: [0.0, 0.0, 0.0], specular: [0, 0, 0], emissive: [0.0, 0.0, 0.0], alpha: 0.5
            }),
            position: [0, -1.5, 0],
            visible: true,
            pickable: false,
            backfaces: false
        });

        this._onCameraMatrix = viewer.camera.on("matrix", this._synchCamera);
        this._onCameraWorldAxis = viewer.camera.on("worldAxis", () => {
            if (viewer.camera.zUp) {
                this._zUp = true;
                this._cubeTextureCanvas.setZUp();
                this._repaint();
                this._synchCamera();
            } else if (viewer.camera.yUp) {
                this._zUp = false;
                this._cubeTextureCanvas.setYUp();
                this._repaint();
                this._synchCamera();
            }
        });
        this._onCameraFOV = viewer.camera.perspective.on("fov", (fov) => {
            if (this._synchProjection) {
                this._navCubeCamera.perspective.fov = fov;
            }
        });
        this._onCameraProjection = viewer.camera.on("projection", (projection) => {
            if (this._synchProjection) {
                this._navCubeCamera.projection = (projection === "ortho" || projection === "perspective") ? projection : "perspective";
            }
        });

        var lastAreaId = -1;

        function actionMove(posX, posY) {
            var yawInc = (posX - lastX) * -sensitivity;
            var pitchInc = (posY - lastY) * -sensitivity;
            viewer.camera.orbitYaw(yawInc);
            viewer.camera.orbitPitch(-pitchInc);
            lastX = posX;
            lastY = posY;
        }

        function getCoordsWithinElement(event) {
            var coords = [0, 0];
            if (!event) {
                event = window.event;
                coords[0] = event.x;
                coords[1] = event.y;
            } else {
                var element = event.target;
                var totalOffsetLeft = 0;
                var totalOffsetTop = 0;
                while (element.offsetParent) {
                    totalOffsetLeft += element.offsetLeft;
                    totalOffsetTop += element.offsetTop;
                    element = element.offsetParent;
                }
                coords[0] = event.pageX - totalOffsetLeft;
                coords[1] = event.pageY - totalOffsetTop;
            }
            return coords;
        }

        {
            var downX = null;
            var downY = null;
            var down = false;
            var over = false;
            var sensitivity = 0.5;

            var lastX;
            var lastY;


            self._navCubeCanvas.addEventListener("mouseenter", self._onMouseEnter = function (e) {
                over = true;
            });


            self._navCubeCanvas.addEventListener("mouseleave", self._onMouseLeave = function (e) {
                over = false;
            });

            self._navCubeCanvas.addEventListener("mousedown", self._onMouseDown = function (e) {
                if (e.which !== 1) {
                    return;
                }
                downX = e.x;
                downY = e.y;
                lastX = e.clientX;
                lastY = e.clientY;
                var canvasPos = getCoordsWithinElement(e);
                var hit = navCubeScene.pick({
                    canvasPos: canvasPos
                });
                if (hit) {
                    down = true;

                } else {
                    down = false;
                }
            });

            self._navCubeCanvas.addEventListener("mouseup", self._onMouseUp = function (e) {
                if (e.which !== 1) {// Left button
                    return;
                }
                down = false;
                if (downX === null) {
                    return;
                }
                var canvasPos = getCoordsWithinElement(e);
                var hit = navCubeScene.pick({
                    canvasPos: canvasPos,
                    pickSurface: true
                });
                if (hit) {
                    if (hit.uv) {
                        var areaId = self._cubeTextureCanvas.getArea(hit.uv);
                        if (areaId >= 0) {
                            self._navCubeCanvas.style.cursor = "pointer";
                            if (lastAreaId >= 0) {
                                self._cubeTextureCanvas.setAreaHighlighted(lastAreaId, false);
                                self._repaint();
                                lastAreaId = -1;
                            }
                            if (areaId >= 0) {
                                self._cubeTextureCanvas.setAreaHighlighted(areaId, true);
                                lastAreaId = areaId;
                                self._repaint();
                                if (e.x < (downX - 3) || e.x > (downX + 3) || e.y < (downY - 3) || e.y > (downY + 3)) {
                                    return;
                                }
                                var dir = self._cubeTextureCanvas.getAreaDir(areaId);
                                if (dir) {
                                    var up = self._cubeTextureCanvas.getAreaUp(areaId);
                                    if (self._isProjectNorth && self._projectNorthOffsetAngle) {
                                        dir = rotateTrueNorth(1, dir, tempVec3a$4);
                                        up = rotateTrueNorth(1, up, tempVec3b$1);
                                    }
                                    flyTo(dir, up, function () {
                                        if (lastAreaId >= 0) {
                                            self._cubeTextureCanvas.setAreaHighlighted(lastAreaId, false);
                                            self._repaint();
                                            lastAreaId = -1;
                                        }
                                        self._navCubeCanvas.style.cursor = "pointer";
                                        if (lastAreaId >= 0) {
                                            self._cubeTextureCanvas.setAreaHighlighted(lastAreaId, false);
                                            self._repaint();
                                            lastAreaId = -1;
                                        }
                                        if (areaId >= 0) {
                                            self._cubeTextureCanvas.setAreaHighlighted(areaId, false);
                                            lastAreaId = -1;
                                            self._repaint();
                                        }
                                    });
                                }
                            }
                        }
                    }
                }
            });

            self._navCubeCanvas.addEventListener("mousemove", self._onMouseMove = function (e) {
                if (lastAreaId >= 0) {
                    self._cubeTextureCanvas.setAreaHighlighted(lastAreaId, false);
                    self._repaint();
                    lastAreaId = -1;
                }
                if (e.buttons === 1 && !down) {
                    return;
                }
                if (down) {
                    var posX = e.clientX;
                    var posY = e.clientY;
                    self._navCubeCanvas.style.cursor = "move";
                    actionMove(posX, posY);
                    return;
                }
                if (!over) {
                    return;
                }
                var canvasPos = getCoordsWithinElement(e);
                var hit = navCubeScene.pick({
                    canvasPos: canvasPos,
                    pickSurface: true
                });
                if (hit) {
                    if (hit.uv) {
                        self._navCubeCanvas.style.cursor = "pointer";
                        var areaId = self._cubeTextureCanvas.getArea(hit.uv);
                        if (areaId === lastAreaId) {
                            return;
                        }
                        if (lastAreaId >= 0) {
                            self._cubeTextureCanvas.setAreaHighlighted(lastAreaId, false);
                        }
                        if (areaId >= 0) {
                            self._cubeTextureCanvas.setAreaHighlighted(areaId, true);
                            self._repaint();
                            lastAreaId = areaId;
                        }
                    }
                } else {
                    self._navCubeCanvas.style.cursor = "default";
                    if (lastAreaId >= 0) {
                        self._cubeTextureCanvas.setAreaHighlighted(lastAreaId, false);
                        self._repaint();
                        lastAreaId = -1;
                    }
                }
            });

            var flyTo = (function () {
                var center = math.vec3();
                return function (dir, up, ok) {
                    var aabb = self._fitVisible ? viewer.scene.getAABB(viewer.scene.visibleObjectIds) : viewer.scene.aabb;
                    var diag = math.getAABB3Diag(aabb);
                    math.getAABB3Center(aabb, center);
                    var dist = Math.abs(diag / Math.tan(self._cameraFitFOV * math.DEGTORAD));
                    viewer.cameraControl.pivotPos = center;
                    if (self._cameraFly) {
                        viewer.cameraFlight.flyTo({
                            look: center,
                            eye: [center[0] - (dist * dir[0]), center[1] - (dist * dir[1]), center[2] - (dist * dir[2])],
                            up: up || [0, 1, 0],
                            orthoScale: diag * 1.1,
                            fitFOV: self._cameraFitFOV,
                            duration: self._cameraFlyDuration
                        }, ok);
                    } else {
                        viewer.cameraFlight.jumpTo({
                            look: center,
                            eye: [center[0] - (dist * dir[0]), center[1] - (dist * dir[1]), center[2] - (dist * dir[2])],
                            up: up || [0, 1, 0],
                            orthoScale: diag * 1.1,
                            fitFOV: self._cameraFitFOV
                        }, ok);
                    }
                };
            })();
        }

        this._onUpdated = viewer.localeService.on("updated", () => {
            this._cubeTextureCanvas.clear();
            this._repaint();
        });

        this.setVisible(cfg.visible);
        this.setCameraFitFOV(cfg.cameraFitFOV);
        this.setCameraFly(cfg.cameraFly);
        this.setCameraFlyDuration(cfg.cameraFlyDuration);
        this.setFitVisible(cfg.fitVisible);
        this.setSynchProjection(cfg.synchProjection);
    }

    send(name, value) {
        switch (name) {
            case "language":
                this._cubeTextureCanvas.clear();
                this._repaint(); // CubeTextureCanvas gets language from Viewer
                break;
        }
    }

    _repaint() {
        const image = this._cubeTextureCanvas.getImage();
        this._cubeMesh.material.diffuseMap.image = image;
        this._cubeMesh.material.emissiveMap.image = image;
    }

    /**
     * Sets if the NavCube is visible.
     *
     * @param {Boolean} visible Whether or not the NavCube is visible.
     */
    setVisible(visible = true) {
        if (!this._navCubeCanvas) {
            return;
        }
        this._cubeMesh.visible = visible;
        if (this._shadow) {
            this._shadow.visible = visible;
        }
        this._navCubeCanvas.style.visibility = visible ? "visible" : "hidden";
    }

    /**
     * Gets if the NavCube is visible.
     *
     * @return {Boolean} True when the NavCube is visible.
     */
    getVisible() {
        if (!this._navCubeCanvas) {
            return false;
        }
        return this._cubeMesh.visible;
    }


    /**
     * Sets whether the axis, corner and edge-aligned views will fit the
     * view to the entire {@link Scene} or just to visible object-{@link Entity}s.
     *
     * Entitys are visible objects when {@link Entity#isObject} and {@link Entity#visible} are both ````true````.
     *
     * @param {Boolean} fitVisible Set ````true```` to fit only visible object-Entitys.
     */
    setFitVisible(fitVisible = false) {
        this._fitVisible = fitVisible;
    }

    /**
     * Gets whether the axis, corner and edge-aligned views will fit the
     * view to the entire {@link Scene} or just to visible object-{@link Entity}s.
     *
     * Entitys are visible objects when {@link Entity#isObject} and {@link Entity#visible} are both ````true````.
     *
     * @return {Boolean} True when fitting only visible object-Entitys.
     */
    getFitVisible() {
        return this._fitVisible;
    }

    /**
     * Sets whether the {@link Camera} flies or jumps to each selected axis or diagonal.
     *
     * Default is ````true````, to fly.
     *
     * @param {Boolean} cameraFly Set ````true```` to fly, else ````false```` to jump.
     */
    setCameraFly(cameraFly = true) {
        this._cameraFly = cameraFly;
    }

    /**
     * Gets whether the {@link Camera} flies or jumps to each selected axis or diagonal.
     *
     * Default is ````true````, to fly.
     *
     * @returns {Boolean} Returns ````true```` to fly, else ````false```` to jump.
     */
    getCameraFly() {
        return this._cameraFly;
    }

    /**
     * Sets how much of the field-of-view, in degrees, that the {@link Scene} should
     * fill the canvas when flying or jumping the {@link Camera} to each selected axis or diagonal.
     *
     * Default value is ````45````.
     *
     * @param {Number} cameraFitFOV New FOV value.
     */
    setCameraFitFOV(cameraFitFOV = 45) {
        this._cameraFitFOV = cameraFitFOV;
    }

    /**
     * Gets how much of the field-of-view, in degrees, that the {@link Scene} should
     * fill the canvas when flying or jumping the {@link Camera} to each selected axis or diagonal.
     *
     * Default value is ````45````.
     *
     * @returns {Number} Current FOV value.
     */
    getCameraFitFOV() {
        return this._cameraFitFOV;
    }

    /**
     * When flying the {@link Camera} to each new axis or diagonal, sets how long, in seconds, that the Camera takes to get there.
     *
     * Default is ````0.5````.
     *
     * @param {Boolean} cameraFlyDuration Camera flight duration in seconds.
     */
    setCameraFlyDuration(cameraFlyDuration = 0.5) {
        this._cameraFlyDuration = cameraFlyDuration;
    }

    /**
     * When flying the {@link Camera} to each new axis or diagonal, gets how long, in seconds, that the Camera takes to get there.
     *
     * Default is ````0.5````.
     *
     * @returns {Boolean} Camera flight duration in seconds.
     */
    getCameraFlyDuration() {
        return this._cameraFlyDuration;
    }

    /**
     * Sets whether the NavCube switches between perspective and orthographic projections in synchrony with
     * the {@link Camera}. When ````false````, the NavCube will always be rendered with perspective projection.
     *
     * @param {Boolean} synchProjection Set ````true```` to keep NavCube projection synchronized with {@link Camera#projection}.
     */
    setSynchProjection(synchProjection = false) {
        this._synchProjection = synchProjection;
    }

    /**
     * Gets whether the NavCube switches between perspective and orthographic projections in synchrony with
     * the {@link Camera}. When ````false````, the NavCube will always be rendered with perspective projection.
     *
     * @return {Boolean} True when NavCube projection is synchronized with {@link Camera#projection}.
     */
    getSynchProjection() {
        return this._synchProjection;
    }

    /**
     * Sets whether the NavCube switches between project north and true north
     *
     * @param {Boolean} isProjectNorth Set ````true```` to use project north offset
     */
    setIsProjectNorth(isProjectNorth = false) {
        this._isProjectNorth = isProjectNorth;
    }

    /**
     * Gets whether the NavCube switches between project north and true north
     *
     * @return {Boolean} isProjectNorth when ````true```` - use project north offset
     */
    getIsProjectNorth() {
        return this._isProjectNorth;
    }

    /**
     * Sets the NavCube project north offset angle (used when {@link isProjectNorth} is ````true````
     *
     * @param {number} projectNorthOffsetAngle Set the vector offset for project north
     */
    setProjectNorthOffsetAngle(projectNorthOffsetAngle) {
        this._projectNorthOffsetAngle = projectNorthOffsetAngle;
    }

    /**
     * Gets the offset angle between project north and true north
     *
     * @return {number} projectNorthOffsetAngle
     */
    getProjectNorthOffsetAngle() {
        return this._projectNorthOffsetAngle;
    }

    /**
     * Destroys this NavCubePlugin.
     *
     * Does not destroy the canvas the NavCubePlugin was configured with.
     */
    destroy() {

        if (this._navCubeCanvas) {

            this.viewer.localeService.off(this._onUpdated);
            this.viewer.camera.off(this._onCameraMatrix);
            this.viewer.camera.off(this._onCameraWorldAxis);
            this.viewer.camera.perspective.off(this._onCameraFOV);
            this.viewer.camera.off(this._onCameraProjection);

            this._navCubeCanvas.removeEventListener("mouseenter", this._onMouseEnter);
            this._navCubeCanvas.removeEventListener("mouseleave", this._onMouseLeave);
            this._navCubeCanvas.removeEventListener("mousedown", this._onMouseDown);

            this._navCubeCanvas.removeEventListener("mousemove", this._onMouseMove);
            this._navCubeCanvas.removeEventListener("mouseup", this._onMouseUp);

            this._navCubeCanvas = null;
            this._cubeTextureCanvas.destroy();
            this._cubeTextureCanvas = null;

            this._onMouseEnter = null;
            this._onMouseLeave = null;
            this._onMouseDown = null;
            this._onMouseMove = null;
            this._onMouseUp = null;
        }

        this._navCubeScene.destroy();
        this._navCubeScene = null;
        this._cubeMesh = null;
        this._shadow = null;

        super.destroy();
    }
}

math.vec3();

/**
 * 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$1(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) => {
            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 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);
    }
}

/**
 * Renders a 3D plane within an {@link Overview} to indicate its {@link SectionPlane}'s current position and orientation.
 *
 * @private
 */
class Plane$1 {

    /** @private */
    constructor(overview, overviewScene, sectionPlane) {

        /**
         * The ID of this SectionPlanesOverviewPlane.
         *
         * @type {String}
         */
        this.id = sectionPlane.id;

        /**
         * The {@link SectionPlane} represented by this SectionPlanesOverviewPlane.
         *
         * @type {SectionPlane}
         */
        this._sectionPlane = sectionPlane;

        this._mesh = new Mesh(overviewScene, {
            id: sectionPlane.id,
            geometry: new ReadableGeometry(overviewScene, buildBoxGeometry({
                xSize: .5,
                ySize: .5,
                zSize: .001
            })),
            material: new PhongMaterial(overviewScene, {
                emissive: [1, 1, 1],
                diffuse: [0, 0, 0],
                backfaces: false
            }),
            edgeMaterial: new EdgeMaterial(overviewScene, {
                edgeColor: [0.0, 0.0, 0.0],
                edgeAlpha: 1.0,
                edgeWidth: 1
            }),
            highlightMaterial: new EmphasisMaterial(overviewScene, {
                fill: true,
                fillColor: [0.5, 1, 0.5],
                fillAlpha: 0.7,
                edges: true,
                edgeColor: [0.0, 0.0, 0.0],
                edgeAlpha: 1.0,
                edgeWidth: 1
            }),
            selectedMaterial: new EmphasisMaterial(overviewScene, {
                fill: true,
                fillColor: [0, 0, 1],
                fillAlpha: 0.7,
                edges: true,
                edgeColor: [1.0, 0.0, 0.0],
                edgeAlpha: 1.0,
                edgeWidth: 1
            }),
            highlighted: true,
            scale: [3, 3, 3],
            position: [0, 0, 0],
            rotation: [0, 0, 0],
            opacity: 0.3,
            edges: true
        });


        {
            const vec = math.vec3([0, 0, 0]);
            const pos2 = math.vec3();
            const zeroVec = math.vec3([0, 0, 1]);
            const quat = math.vec4(4);
            const pos3 = math.vec3();

            const update = () => {

                const origin = this._sectionPlane.scene.center;

                const negDir = [-this._sectionPlane.dir[0], -this._sectionPlane.dir[1], -this._sectionPlane.dir[2]];
                math.subVec3(origin, this._sectionPlane.pos, vec);
                const dist = -math.dotVec3(negDir, vec);

                math.normalizeVec3(negDir);
                math.mulVec3Scalar(negDir, dist, pos2);
                const quaternion = math.vec3PairToQuaternion(zeroVec, this._sectionPlane.dir, quat);

                pos3[0] = pos2[0] * 0.1;
                pos3[1] = pos2[1] * 0.1;
                pos3[2] = pos2[2] * 0.1;

                this._mesh.quaternion = quaternion;
                this._mesh.position = pos3;
            };

            this._onSectionPlanePos = this._sectionPlane.on("pos", update);
            this._onSectionPlaneDir = this._sectionPlane.on("dir", update);

            // update();
        }

        this._highlighted = false;
        this._selected = false;
    }

    /**
     * Sets if this SectionPlanesOverviewPlane is highlighted.
     *
     * @type {Boolean}
     * @private
     */
    setHighlighted(highlighted) {
        this._highlighted = !!highlighted;
        this._mesh.highlighted = this._highlighted;
        this._mesh.highlightMaterial.fillColor = highlighted ? [0, 0.7, 0] : [0, 0, 0];
        // this._selectedMesh.highlighted = true;
    }

    /**
     * Gets if this SectionPlanesOverviewPlane is highlighted.
     *
     * @type {Boolean}
     * @private
     */
    getHighlighted() {
        return this._highlighted;
    }

    /**
     * Sets if this SectionPlanesOverviewPlane is selected.
     *
     * @type {Boolean}
     * @private
     */
    setSelected(selected) {
        this._selected = !!selected;
        this._mesh.edgeMaterial.edgeWidth = selected ? 3 : 1;
        this._mesh.highlightMaterial.edgeWidth = selected ? 3 : 1;

    }

    /**
     * Gets if this SectionPlanesOverviewPlane is selected.
     *
     * @type {Boolean}
     * @private
     */
    getSelected() {
        return this._selected;
    }

    /** @private */
    destroy() {
        this._sectionPlane.off(this._onSectionPlanePos);
        this._sectionPlane.off(this._onSectionPlaneDir);
        this._mesh.destroy();
    }
}

/**
 * @desc An interactive 3D overview for navigating the {@link SectionPlane}s created by its {@link SectionPlanesPlugin}.
 *
 * * Located at {@link SectionPlanesPlugin#overview}.
 * * Renders the overview on a separate canvas at a corner of the {@link Viewer}'s {@link Scene} {@link Canvas}.
 * * The overview shows a 3D plane object for each {@link SectionPlane} in the {@link Scene}.
 * * Click a plane object in the overview to toggle the visibility of a 3D gizmo to edit the position and orientation of its {@link SectionPlane}.
 *
 * @private
 */
class Overview$1 {

    /**
     * @private
     */
    constructor(plugin, cfg) {

        if (!cfg.onHoverEnterPlane || !cfg.onHoverLeavePlane || !cfg.onClickedNothing || !cfg.onClickedPlane) {
            throw "Missing config(s): onHoverEnterPlane, onHoverLeavePlane, onClickedNothing || onClickedPlane";
        }

        /**
         * The {@link SectionPlanesPlugin} that owns this SectionPlanesOverview.
         *
         * @type {SectionPlanesPlugin}
         */
        this.plugin = plugin;

        this._viewer = plugin.viewer;

        this._onHoverEnterPlane = cfg.onHoverEnterPlane;
        this._onHoverLeavePlane = cfg.onHoverLeavePlane;
        this._onClickedNothing = cfg.onClickedNothing;
        this._onClickedPlane = cfg.onClickedPlane;
        this._visible = true;

        this._planes = {};

        //--------------------------------------------------------------------------------------------------------------
        // Init canvas
        //--------------------------------------------------------------------------------------------------------------

        this._canvas = cfg.overviewCanvas;

        //--------------------------------------------------------------------------------------------------------------
        // Init scene
        //--------------------------------------------------------------------------------------------------------------

        this._scene = new Scene(this._viewer, {
            canvasId: this._canvas.id,
            transparent: true
        });
        this._scene.clearLights();
        new DirLight(this._scene, {
            dir: [0.4, -0.4, 0.8],
            color: [0.8, 1.0, 1.0],
            intensity: 1.0,
            space: "view"
        });
        new DirLight(this._scene, {
            dir: [-0.8, -0.3, -0.4],
            color: [0.8, 0.8, 0.8],
            intensity: 1.0,
            space: "view"
        });
        new DirLight(this._scene, {
            dir: [0.8, -0.6, -0.8],
            color: [1.0, 1.0, 1.0],
            intensity: 1.0,
            space: "view"
        });

        this._scene.camera;
        this._scene.camera.perspective.fov = 70;

        this._zUp = false;

        //--------------------------------------------------------------------------------------------------------------
        // Synchronize overview scene camera with viewer camera
        //--------------------------------------------------------------------------------------------------------------

        {
            const camera = this._scene.camera;
            const matrix = math.rotationMat4c(-90 * math.DEGTORAD, 1, 0, 0);
            const eyeLookVec = math.vec3();
            const eyeLookVecOverview = math.vec3();
            const upOverview = math.vec3();

            this._synchCamera = () => {
                const eye = this._viewer.camera.eye;
                const look = this._viewer.camera.look;
                const up = this._viewer.camera.up;
                math.mulVec3Scalar(math.normalizeVec3(math.subVec3(eye, look, eyeLookVec)), 7);
                if (this._zUp) { // +Z up
                    math.transformVec3(matrix, eyeLookVec, eyeLookVecOverview);
                    math.transformVec3(matrix, up, upOverview);
                    camera.look = [0, 0, 0];
                    camera.eye = math.transformVec3(matrix, eyeLookVec, eyeLookVecOverview);
                    camera.up = math.transformPoint3(matrix, up, upOverview);
                } else { // +Y up
                    camera.look = [0, 0, 0];
                    camera.eye = eyeLookVec;
                    camera.up = up;
                }
            };
        }

        this._onViewerCameraMatrix = this._viewer.camera.on("matrix", this._synchCamera);

        this._onViewerCameraWorldAxis = this._viewer.camera.on("worldAxis", this._synchCamera);

        this._onViewerCameraFOV = this._viewer.camera.perspective.on("fov", (fov) => {
            this._scene.camera.perspective.fov = fov;
        });

        //--------------------------------------------------------------------------------------------------------------
        // Bind overview canvas events
        //--------------------------------------------------------------------------------------------------------------

        {
            var hoveredEntity = null;

            this._onInputMouseMove = this._scene.input.on("mousemove", (coords) => {
                const hit = this._scene.pick({
                    canvasPos: coords
                });
                if (hit) {
                    if (!hoveredEntity || hit.entity.id !== hoveredEntity.id) {
                        if (hoveredEntity) {
                            const plane = this._planes[hoveredEntity.id];
                            if (plane) {
                                this._onHoverLeavePlane(hoveredEntity.id);
                            }
                        }
                        hoveredEntity = hit.entity;
                        const plane = this._planes[hoveredEntity.id];
                        if (plane) {
                            this._onHoverEnterPlane(hoveredEntity.id);
                        }
                    }
                } else {
                    if (hoveredEntity) {
                        this._onHoverLeavePlane(hoveredEntity.id);
                        hoveredEntity = null;
                    }
                }
            });

            this._scene.canvas.canvas.addEventListener("mouseup", this._onCanvasMouseUp = () => {
                if (hoveredEntity) {
                    const plane = this._planes[hoveredEntity.id];
                    if (plane) {
                        this._onClickedPlane(hoveredEntity.id);
                    }
                } else {
                    this._onClickedNothing();
                }
            });

            this._scene.canvas.canvas.addEventListener("mouseout", this._onCanvasMouseOut = () => {
                if (hoveredEntity) {
                    this._onHoverLeavePlane(hoveredEntity.id);
                    hoveredEntity = null;
                }
            });
        }

        //--------------------------------------------------------------------------------------------------------------
        // Configure overview
        //--------------------------------------------------------------------------------------------------------------

        this.setVisible(cfg.overviewVisible);
    }

    /** Called by SectionPlanesPlugin#createSectionPlane()
     * @private
     */
    addSectionPlane(sectionPlane) {
        this._planes[sectionPlane.id] = new Plane$1(this, this._scene, sectionPlane);
    }

    /**  @private
     */
    setPlaneHighlighted(id, highlighted) {
        const plane = this._planes[id];
        if (plane) {
            plane.setHighlighted(highlighted);
        }
    }

    /**  @private
     */
    setPlaneSelected(id, selected) {
        const plane = this._planes[id];
        if (plane) {
            plane.setSelected(selected);
        }
    }

    /** @private
     */
    removeSectionPlane(sectionPlane) {
        const plane = this._planes[sectionPlane.id];
        if (plane) {
            plane.destroy();
            delete this._planes[sectionPlane.id];
        }
    }

    /**
     * Sets if this SectionPlanesOverview is visible.
     *
     * @param {Boolean} visible Whether or not this SectionPlanesOverview is visible.
     */
    setVisible(visible = true) {
        this._visible = visible;
        this._canvas.style.visibility = visible ? "visible" : "hidden";
    }

    /**
     * Gets if this SectionPlanesOverview is visible.
     *
     * @return {Boolean} True when this SectionPlanesOverview is visible.
     */
    getVisible() {
        return this._visible;
    }

    /**  @private
     */
    destroy() {
        this._viewer.camera.off(this._onViewerCameraMatrix);
        this._viewer.camera.off(this._onViewerCameraWorldAxis);
        this._viewer.camera.perspective.off(this._onViewerCameraFOV);

        this._scene.input.off(this._onInputMouseMove);
        this._scene.canvas.canvas.removeEventListener("mouseup", this._onCanvasMouseUp);
        this._scene.canvas.canvas.removeEventListener("mouseout", this._onCanvasMouseOut);
        this._scene.destroy();
    }
}

const tempAABB$1 = math.AABB3();
const tempVec3$1$1 = math.vec3();

/**
 * SectionPlanesPlugin is a {@link Viewer} plugin that manages {@link SectionPlane}s.
 *
 * [<img src="https://user-images.githubusercontent.com/83100/57724962-406e9a00-768c-11e9-9f1f-3d178a3ec11f.gif">](https://xeokit.github.io/xeokit-sdk/examples/index.html#gizmos_SectionPlanesPlugin)
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#gizmos_SectionPlanesPlugin)]
 *
 * ## Overview
 *
 * * Use the SectionPlanesPlugin to
 * create and edit {@link SectionPlane}s to slice portions off your models and reveal internal structures.
 * * As shown in the screen capture above, SectionPlanesPlugin shows an overview of all your SectionPlanes (on the right, in
 * this example).
 * * Click a plane in the overview to activate a 3D control with which you can interactively
 * reposition its SectionPlane in the main canvas.
 * * Use {@lin BCFViewpointsPlugin} to save and load SectionPlanes in BCF viewpoints.
 *
 * ## Usage
 *
 * In the example below, we'll use a {@link GLTFLoaderPlugin} to load a model, and a SectionPlanesPlugin
 * to slice it open with two {@link SectionPlane}s. We'll show the overview in the bottom right of the Viewer
 * canvas. Finally, we'll programmatically activate the 3D editing control, so that we can use it to interactively
 * reposition our second SectionPlane.
 *
 * ````JavaScript
 * import {Viewer, GLTFLoaderPlugin, SectionPlanesPlugin} from "xeokit-sdk.es.js";
 *
 * // Create a Viewer and arrange its Camera
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas"
 * });
 *
 * viewer.camera.eye = [-5.02, 2.22, 15.09];
 * viewer.camera.look = [4.97, 2.79, 9.89];
 * viewer.camera.up = [-0.05, 0.99, 0.02];
 *
 *
 * // Add a GLTFLoaderPlugin
 *
 * const gltfLoader = new GLTFLoaderPlugin(viewer);
 *
 * // Add a SectionPlanesPlugin, with overview visible
 *
 * const sectionPlanes = new SectionPlanesPlugin(viewer, {
 *     overviewCanvasID: "myOverviewCanvas",
 *     overviewVisible: true
 * });
 *
 * // Load a model
 *
 * const model = gltfLoader.load({
 *     id: "myModel",
 *     src: "./models/gltf/schependomlaan/scene.gltf"
 * });
 *
 * // Create a couple of section planes
 * // These will be shown in the overview
 *
 * sectionPlanes.createSectionPlane({
 *     id: "mySectionPlane",
 *     pos: [1.04, 1.95, 9.74],
 *     dir: [1.0, 0.0, 0.0]
 * });
 *
 * sectionPlanes.createSectionPlane({
 *     id: "mySectionPlane2",
 *     pos: [2.30, 4.46, 14.93],
 *     dir: [0.0, -0.09, -0.79]
 * });
 *
 * // Show the SectionPlanePlugin's 3D editing gizmo,
 * // to interactively reposition one of our SectionPlanes
 *
 * sectionPlanes.showControl("mySectionPlane2");
 *
 * const mySectionPlane2 = sectionPlanes.sectionPlanes["mySectionPlane2"];
 *
 * // Programmatically reposition one of our SectionPlanes
 * // This also updates its position as shown in the overview gizmo
 *
 * mySectionPlane2.pos = [11.0, 6.0, -12];
 * mySectionPlane2.dir = [0.4, 0.0, 0.5];
 * ````
 */
class SectionPlanesPlugin extends Plugin {

    /**
     * @constructor
     * @param {Viewer} viewer The Viewer.
     * @param {Object} cfg Plugin configuration.
     * @param {String} [cfg.id="SectionPlanes"] Optional ID for this plugin, so that we can find it within {@link Viewer#plugins}.
     * @param {String} [cfg.overviewCanvasId] ID of a canvas element to display the overview.
     * @param {String} [cfg.overviewVisible=true] Initial visibility of the overview canvas.
     */
    constructor(viewer, cfg = {}) {

        super("SectionPlanes", viewer);

        this._freeControls = [];
        this._sectionPlanes = viewer.scene.sectionPlanes;
        this._controls = {};
        this._shownControlId = null;

        if (cfg.overviewCanvasId !== null && cfg.overviewCanvasId !== undefined) {

            const overviewCanvas = document.getElementById(cfg.overviewCanvasId);

            if (!overviewCanvas) {
                this.warn("Can't find overview canvas: '" + cfg.overviewCanvasId + "' - will create plugin without overview");

            } else {

                this._overview = new Overview$1(this, {
                    overviewCanvas: overviewCanvas,
                    visible: cfg.overviewVisible,

                    onHoverEnterPlane: ((id) => {
                        this._overview.setPlaneHighlighted(id, true);
                    }),

                    onHoverLeavePlane: ((id) => {
                        this._overview.setPlaneHighlighted(id, false);
                    }),

                    onClickedPlane: ((id) => {
                        if (this.getShownControl() === id) {
                            this.hideControl();
                            return;
                        }
                        this.showControl(id);
                        const sectionPlane = this.sectionPlanes[id];
                        const sectionPlanePos = sectionPlane.pos;
                        tempAABB$1.set(this.viewer.scene.aabb);
                        math.getAABB3Center(tempAABB$1, tempVec3$1$1);
                        tempAABB$1[0] += sectionPlanePos[0] - tempVec3$1$1[0];
                        tempAABB$1[1] += sectionPlanePos[1] - tempVec3$1$1[1];
                        tempAABB$1[2] += sectionPlanePos[2] - tempVec3$1$1[2];
                        tempAABB$1[3] += sectionPlanePos[0] - tempVec3$1$1[0];
                        tempAABB$1[4] += sectionPlanePos[1] - tempVec3$1$1[1];
                        tempAABB$1[5] += sectionPlanePos[2] - tempVec3$1$1[2];
                        this.viewer.cameraFlight.flyTo({
                            aabb: tempAABB$1,
                            fitFOV: 65
                        });
                    }),

                    onClickedNothing: (() => {
                        this.hideControl();
                    })
                });
            }
        }

        this._onSceneSectionPlaneCreated = viewer.scene.on("sectionPlaneCreated", (sectionPlane) => {

            // SectionPlane created, either via SectionPlanesPlugin#createSectionPlane(), or by directly
            // instantiating a SectionPlane independently of SectionPlanesPlugin, which can be done
            // by BCFViewpointsPlugin#loadViewpoint().

            this._sectionPlaneCreated(sectionPlane);
        });
    }

    /**
     * Sets if the overview canvas is visible.
     *
     * @param {Boolean} visible Whether or not the overview canvas is visible.
     */
    setOverviewVisible(visible) {
        if (this._overview) {
            this._overview.setVisible(visible);
        }
    }

    /**
     * Gets if the overview canvas is visible.
     *
     * @return {Boolean} True when the overview canvas is visible.
     */
    getOverviewVisible() {
        if (this._overview) {
            return this._overview.getVisible();
        }
    }

    /**
     * Returns a map of the {@link SectionPlane}s created by this SectionPlanesPlugin.
     *
     * @returns {{String:SectionPlane}} A map containing the {@link SectionPlane}s, each mapped to its {@link SectionPlane#id}.
     */
    get sectionPlanes() {
        return this._sectionPlanes;
    }

    /**
     * Creates a {@link SectionPlane}.
     *
     * The {@link SectionPlane} will be registered by {@link SectionPlane#id} in {@link SectionPlanesPlugin#sectionPlanes}.
     *
     * @param {Object} params {@link SectionPlane} configuration.
     * @param {String} [params.id] Unique ID to assign to the {@link SectionPlane}. Must be unique among all components in the {@link Viewer}'s {@link Scene}. Auto-generated when omitted.
     * @param {Number[]} [params.pos=[0,0,0]] World-space position of the {@link SectionPlane}.
     * @param {Number[]} [params.dir=[0,0,-1]] World-space vector indicating the orientation of the {@link SectionPlane}.
     * @param {Boolean} [params.active=true] Whether the {@link SectionPlane} is initially active. Only clips while this is true.
     * @returns {SectionPlane} The new {@link SectionPlane}.
     */
    createSectionPlane(params = {}) {

        if (params.id !== undefined && params.id !== null && this.viewer.scene.components[params.id]) {
            this.error("Viewer component with this ID already exists: " + params.id);
            delete params.id;
        }

        // Note that SectionPlane constructor fires "sectionPlaneCreated" on the Scene,
        // which SectionPlanesPlugin handles and calls #_sectionPlaneCreated to create gizmo and add to overview canvas.

        const sectionPlane = new SectionPlane(this.viewer.scene, {
            id: params.id,
            pos: params.pos,
            dir: params.dir,
            active: true 
        });
        return sectionPlane;
    }

    _sectionPlaneCreated(sectionPlane) {
        const control = ((this._freeControls.length > 0)
                         ? this._freeControls.pop()
                         : (() => {
                             const scene = this.viewer.scene;
                             const planeRoot = (function() {
                                 const rootNode = new Node$1(scene, { isObject: false });

                                 rootNode.addChild(new Mesh(rootNode, { // plane
                                     geometry: new ReadableGeometry(rootNode, {
                                         primitive: "triangles",
                                         positions: [
                                             0.5, 0.5, 0.0, 0.5, -0.5, 0.0, // 0
                                             -0.5, -0.5, 0.0, -0.5, 0.5, 0.0, // 1
                                             0.5, 0.5, -0, 0.5, -0.5, -0, // 2
                                             -0.5, -0.5, -0, -0.5, 0.5, -0 // 3
                                         ],
                                         indices: [0, 1, 2, 2, 3, 0]
                                     }),
                                     material: new PhongMaterial(rootNode, {
                                         emissive: [0, 0.0, 0],
                                         diffuse: [0, 0, 0],
                                         backfaces: true
                                     }),
                                     opacity: 0.6,
                                     ghosted: true,
                                     ghostMaterial: new EmphasisMaterial(rootNode, {
                                         edges: false,
                                         filled: true,
                                         fillColor: [1, 1, 0],
                                         edgeColor: [0, 0, 0],
                                         fillAlpha: 0.1,
                                         backfaces: true
                                     }),
                                     pickable: false,
                                     collidable: true,
                                     clippable: false,
                                     visible: false,
                                     scale: [2.4, 2.4, 1],
                                     isObject: false
                                 }));

                                 rootNode.addChild(new Mesh(rootNode, { // Visible frame
                                     geometry: new ReadableGeometry(rootNode, buildTorusGeometry({
                                         center: [0, 0, 0],
                                         radius: 1.7,
                                         tube: 0.02,
                                         radialSegments: 4,
                                         tubeSegments: 4,
                                         arc: Math.PI * 2.0
                                     })),
                                     material: new PhongMaterial(rootNode, {
                                         emissive: [0, 0, 0],
                                         diffuse: [0, 0, 0],
                                         specular: [0, 0, 0],
                                         shininess: 0
                                     }),
                                     //highlighted: true,
                                     highlightMaterial: new EmphasisMaterial(rootNode, {
                                         edges: false,
                                         edgeColor: [0.0, 0.0, 0.0],
                                         filled: true,
                                         fillColor: [0.8, 0.8, 0.8],
                                         fillAlpha: 1.0
                                     }),
                                     pickable: false,
                                     collidable: false,
                                     clippable: false,
                                     visible: false,
                                     scale: [1, 1, .1],
                                     rotation: [0, 0, 45],
                                     isObject: false
                                 }));

                                 return {
                                     setPosition: (function() {
                                         const origin = math.vec3();
                                         const rtcPos = math.vec3();
                                         return function(p) {
                                             worldToRTCPos(p, origin, rtcPos);
                                             rootNode.origin = origin;
                                             rootNode.position = rtcPos;
                                         };
                                     })(),
                                     setQuaternion: q => { rootNode.quaternion = q; },
                                     setScale: s => { rootNode.scale = s; },
                                     setVisible: v => { rootNode.visible = v; }
                                 };
                             })();
                             let unbindSectionPlane = () => { };
                             const ctrl = new TransformControl(this.viewer);

                             let culled  = false;
                             let visible = false;
                             let handlers = null;
                             const updateVisible = () => {
                                 const vis = visible && (! culled);
                                 planeRoot.setVisible(vis);
                                 ctrl.setHandlers(vis && handlers);
                             };

                             return {
                                 _destroy: () => {
                                     unbindSectionPlane();
                                     ctrl.destroy();
                                 },
                                 setCulled:  c => { culled = c;  updateVisible(); },
                                 setVisible: v => { visible = v; updateVisible(); },
                                 _setSectionPlane: sectionPlane => {
                                     unbindSectionPlane();
                                     if (sectionPlane) {
                                         let ignoreNextSectionPlaneDirUpdate = false;
                                         handlers = {
                                             onPosition:   p => {
                                                 planeRoot.setPosition(p);
                                                 sectionPlane.pos = p;
                                             },
                                             onQuaternion: q => {
                                                 planeRoot.setQuaternion(q);
                                                 ignoreNextSectionPlaneDirUpdate = true;
                                                 sectionPlane.quaternion = q;
                                             },
                                             onScreenScale: s => {
                                                 planeRoot.setScale(s);
                                             }
                                         };

                                         const setPosFromSectionPlane = () => {
                                             planeRoot.setPosition(sectionPlane.pos);
                                             ctrl.setPosition(sectionPlane.pos);
                                         };
                                         const setDirFromSectionPlane = () => {
                                             planeRoot.setQuaternion(sectionPlane.quaternion);
                                             ctrl.setQuaternion(sectionPlane.quaternion);
                                         };
                                         setPosFromSectionPlane();
                                         setDirFromSectionPlane();
                                         const onSectionPlanePos = sectionPlane.on("pos", setPosFromSectionPlane);
                                         const onSectionPlaneDir = sectionPlane.on("dir", () => {
                                             if (!ignoreNextSectionPlaneDirUpdate) {
                                                 setDirFromSectionPlane();
                                             } else {
                                                 ignoreNextSectionPlaneDirUpdate = false;
                                             }
                                         });

                                         unbindSectionPlane = () => {
                                             sectionPlane.off(onSectionPlanePos);
                                             sectionPlane.off(onSectionPlaneDir);
                                             unbindSectionPlane = () => { };
                                         };
                                     }
                                 }
                             };
                         })());
        control._setSectionPlane(sectionPlane);
        control.setVisible(false);
        this._controls[sectionPlane.id] = control;
        if (this._overview) {
            this._overview.addSectionPlane(sectionPlane);
        }
        sectionPlane.once("destroyed", () => {
            this._sectionPlaneDestroyed(sectionPlane);
        });
    }

    /**
     * Inverts the direction of {@link SectionPlane#dir} on every existing SectionPlane.
     *
     * Inverts all SectionPlanes, including those that were not created with SectionPlanesPlugin.
     */
    flipSectionPlanes() {
        const sectionPlanes = this.viewer.scene.sectionPlanes;
        for (let id in sectionPlanes) {
            const sectionPlane = sectionPlanes[id];
            sectionPlane.flipDir();
        }
    }

    /**
     * Shows the 3D editing gizmo for a {@link SectionPlane}.
     *
     * @param {String} id ID of the {@link SectionPlane}.
     */
    showControl(id) {
        const control = this._controls[id];
        if (!control) {
            this.error("Control not found: " + id);
            return;
        }
        this.hideControl();
        control.setVisible(true);
        if (this._overview) {
            this._overview.setPlaneSelected(id, true);
        }
        this._shownControlId = id;
    }

    /**
     * Gets the ID of the {@link SectionPlane} that the 3D editing gizmo is shown for.
     *
     * Returns ````null```` when the editing gizmo is not shown.
     *
     * @returns {String} ID of the the {@link SectionPlane} that the 3D editing gizmo is shown for, if shown, else ````null````.
     */
    getShownControl() {
        return this._shownControlId;
    }

    /**
     * Hides the 3D {@link SectionPlane} editing gizmo if shown.
     */
    hideControl() {
        for (var id in this._controls) {
            if (this._controls.hasOwnProperty(id)) {
                this._controls[id].setVisible(false);
                if (this._overview) {
                    this._overview.setPlaneSelected(id, false);
                }
            }
        }
        this._shownControlId = null;
    }

    /**
     * Destroys a {@link SectionPlane} created by this SectionPlanesPlugin.
     *
     * @param {String} id ID of the {@link SectionPlane}.
     */
    destroySectionPlane(id) {
        var sectionPlane = this.viewer.scene.sectionPlanes[id];
        if (!sectionPlane) {
            this.error("SectionPlane not found: " + id);
            return;
        }
        this._sectionPlaneDestroyed(sectionPlane);
        sectionPlane.destroy();

        if (id === this._shownControlId) {
            this._shownControlId = null;
        }
    }

    _sectionPlaneDestroyed(sectionPlane) {
        if (this._overview) {
            this._overview.removeSectionPlane(sectionPlane);
        }
        const control = this._controls[sectionPlane.id];
        if (!control) {
            return;
        }
        control.setVisible(false);
        control._setSectionPlane(null);
        delete this._controls[sectionPlane.id];
        this._freeControls.push(control);
    }

    /**
     * Destroys all {@link SectionPlane}s created by this SectionPlanesPlugin.
     */
    clear() {
        const ids = Object.keys(this._sectionPlanes);
        for (var i = 0, len = ids.length; i < len; i++) {
            this.destroySectionPlane(ids[i]);
        }
    }

    /**
     * @private
     */
    send(name, value) {
        switch (name) {

            case "snapshotStarting": // Viewer#getSnapshot() about to take snapshot - hide controls
                for (let id in this._controls) {
                    if (this._controls.hasOwnProperty(id)) {
                        this._controls[id].setCulled(true);
                    }
                }
                break;

            case "snapshotFinished": // Viewer#getSnapshot() finished taking snapshot - show controls again
                for (let id in this._controls) {
                    if (this._controls.hasOwnProperty(id)) {
                        this._controls[id].setCulled(false);
                    }
                }
                break;

            case "clearSectionPlanes":
                this.clear();
                break;
        }
    }

    /**
     * Destroys this SectionPlanesPlugin.
     *
     * Also destroys each {@link SectionPlane} created by this SectionPlanesPlugin.
     *
     * Does not destroy the canvas the SectionPlanesPlugin was configured with.
     */
    destroy() {
        this.clear();
        if (this._overview) {
            this._overview.destroy();
        }
        this._destroyFreeControls();
        super.destroy();
    }

    _destroyFreeControls() {
        var control = this._freeControls.pop();
        while (control) {
            control._destroy();
            control = this._freeControls.pop();
        }
        this.viewer.scene.off(this._onSceneSectionPlaneCreated);
    }
}

math.vec3();
math.mat4();

math.AABB3();
math.vec3();

math.vec3();

/**
 * @desc A {@link TreeViewPlugin} render class.
 * 
 */
class RenderService {

  /**
  * @constructor
  *
  * @param {HTMLElement} treeViewContainer Container element for the tree which uses this render service.
  */
  constructor(treeViewContainer) {
    this._treeViewContainer = treeViewContainer;
  }

  /*
  * Creates the root node of the tree.
  * @return {HTMLElement} The root node of the tree.
  */
  createRootNode() {
    return document.createElement('ul')
  }

  /*
  * Creates node of the tree.
  * @param {Object} node The node to create.

  * @return {HTMLElement} The html element for the node.
  */
  createNodeElement(node, expandHandler, checkHandler, contextmenuHandler, titleClickHandler) {
    const nodeElement = document.createElement('li');
    nodeElement.id = node.nodeId;

    if (node.xrayed) {
        nodeElement.classList.add('xrayed-node');
    }
    
    if (node.children.length > 0) {
        const switchElement = document.createElement('a');
        switchElement.href = '#';
        switchElement.id = `switch-${node.nodeId}`;
        switchElement.textContent = '+';
        switchElement.classList.add('plus');
        if (expandHandler) switchElement.addEventListener('click', expandHandler);
        nodeElement.appendChild(switchElement);
    }
    
    const checkbox = document.createElement('input');
    checkbox.id = `checkbox-${node.nodeId}`;
    checkbox.type = "checkbox";
    checkbox.checked = node.checked;
    checkbox.style["pointer-events"] = "all";
    if (checkHandler) checkbox.addEventListener("change", checkHandler);
    nodeElement.appendChild(checkbox);
    
    const span = document.createElement('span');
    span.textContent = node.title;
    nodeElement.appendChild(span);

    if (contextmenuHandler) {
      addContextMenuListener(span, contextmenuHandler);
    }

    if (titleClickHandler) {
      span.onclick = titleClickHandler;
    }

    return nodeElement;
  }

  createDisabledNodeElement(rootName) {
    const li = document.createElement('li');

    const switchElement = document.createElement('a');
    switchElement.href = '#';
    switchElement.textContent = '!';
    switchElement.classList.add('warn');
    switchElement.classList.add('warning');
    li.appendChild(switchElement);
    
    const span = document.createElement('span');
    span.textContent = rootName;
    li.appendChild(span);

    return li;
  }

  addChildren(element, nodes) {
    const ul = document.createElement('ul');
    nodes.forEach((nodeElement) => {
      ul.appendChild(nodeElement);
    });

    element.parentElement.appendChild(ul);
  }

  expand(element, expandHandler, collapseHandler) {
    element.classList.remove('plus');
    element.classList.add('minus');
    element.textContent = '-';
    element.removeEventListener('click', expandHandler);
    element.addEventListener('click', collapseHandler);
  }

  collapse(element, expandHandler, collapseHandler) {
    if (!element) {
      return;
    }
    const parent = element.parentElement;
    if (!parent) {
        return;
    }
    const ul = parent.querySelector('ul');
    if (!ul) {
        return;
    }
    parent.removeChild(ul);
    element.classList.remove('minus');
    element.classList.add('plus');
    element.textContent = '+';
    element.removeEventListener('click', collapseHandler);
    element.addEventListener('click', expandHandler);
  }

  isExpanded(element) {
    const parentElement = element.parentElement;
    return parentElement.getElementsByTagName('li')[0] !== undefined;
  }

  getId(element) {
    const parentElement = element.parentElement;
    return parentElement.id;
  }

  getIdFromCheckbox(element) {
    return element.id.replace('checkbox-', '');
  }

  getSwitchElement(nodeId) {
    return this._treeViewContainer.querySelector(`[id="switch-${nodeId}"]`);
  }

  isChecked(element) {
    return element.checked;
  }

  setCheckbox(nodeId, checked, indeterminate = false) {
    const checkbox = this._treeViewContainer.querySelector(`[id="checkbox-${nodeId}"]`);
    if (checkbox) {
      if (checked !== checkbox.checked) {
        checkbox.checked = checked;
      }
      if (indeterminate !== checkbox.indeterminate) {
        checkbox.indeterminate = indeterminate;
        if (indeterminate) {
          checkbox.checked = false;
        }
      }
    }
  }

  setXRayed(nodeId, xrayed) {
    const treeNode = this._treeViewContainer.querySelector(`[id="${nodeId}"]`);
    if (treeNode) {
      if (xrayed) {
        treeNode.classList.add('xrayed-node');
      } else {
        treeNode.classList.remove('xrayed-node');
      }
    }
  }

  setHighlighted(nodeId, highlighted) { 
    const treeNode = this._treeViewContainer.querySelector(`[id="${nodeId}"]`);
    if (treeNode) {
      if (highlighted) {
        treeNode.scrollIntoView({block: "center"});
        treeNode.classList.add('highlighted-node');
      } else {
        treeNode.classList.remove('highlighted-node');
      }
    }
  }
}

const treeViews = [];

/**
 * @desc A {@link Viewer} plugin that provides an HTML tree view to navigate the IFC elements in models.
 * <br>
 *
 * <a href="https://xeokit.github.io/xeokit-sdk/examples/index.html#BIMOffline_XKT_WestRiverSideHospital" style="border: 1px solid black;"><img src="http://xeokit.io/img/docs/TreeViewPlugin/TreeViewPlugin.png"></a>
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#BIMOffline_XKT_WestRiverSideHospital)]
 *
 * ## Overview
 *
 * * A fast HTML tree view, with zero external dependencies, that works with huge numbers of objects.
 * * Each tree node has a checkbox to control the visibility of its object.
 * * Has three hierarchy modes: "containment", "types" and "storeys".
 * * Automatically contains all models (that have metadata) that are currently in the {@link Scene}.
 * * Sorts tree nodes by default - spatially, from top-to-bottom for ````IfcBuildingStorey```` nodes, and alphanumerically for other nodes.
 * * Allows custom CSS styling.
 * * Use {@link ContextMenu} to create a context menu for the tree nodes.
 *
 * ## Credits
 *
 * TreeViewPlugin is based on techniques described in [*Super Fast Tree View in JavaScript*](https://chrissmith.xyz/super-fast-tree-view-in-javascript/) by [Chris Smith](https://twitter.com/chris22smith).
 *
 * ## Usage
 *
 * In the example below, we'll add a TreeViewPlugin which, by default, will automatically show the structural
 * hierarchy of the IFC elements in each model we load.
 *
 * Then we'll use an {@link XKTLoaderPlugin} to load the Schependomlaan model from an
 * [.xkt file](https://github.com/xeokit/xeokit-sdk/tree/master/examples/models/xkt/schependomlaan).
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/navigation/#TreeViewPlugin_Containment)]
 *
 * ````javascript
 * import {Viewer, XKTLoaderPlugin, TreeViewPlugin} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *      canvasId: "myCanvas",
 *      transparent: true
 * });
 *
 * viewer.camera.eye = [-2.56, 8.38, 8.27];
 * viewer.camera.look = [13.44, 3.31, -14.83];
 * viewer.camera.up = [0.10, 0.98, -0.14];
 *
 * const treeView = new TreeViewPlugin(viewer, {
 *     containerElement: document.getElementById("myTreeViewContainer")
 * });
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const model = xktLoader.load({
 *     id: "myModel",
 *     src: "./models/xkt/Schependomlaan.xkt",
 *     edges: true
 * });
 * ````
 *
 * ## Manually Adding Models
 *
 * Instead of adding models automatically, we can control which models appear in our TreeViewPlugin by adding them manually.
 *
 * In the next example, we'll configure the TreeViewPlugin to not add models automatically. Then, once the model
 * has loaded, we'll add it manually using {@link TreeViewPlugin#addModel}.
 *
 * ````javascript
 * const treeView = new TreeViewPlugin(viewer, {
 *      containerElement: document.getElementById("myTreeViewContainer"),
 *      autoAddModels: false  // <<---------------- Don't auto-add models
 * });
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const model = xktLoader.load({
 *     id: "myModel",
 *     src: "./models/xkt/Schependomlaan.xkt",
 *     edges: true
 * });
 *
 * model.on("loaded", () => {
 *      treeView.addModel(model.id);
 * });
 * ````
 *
 * Adding models manually also allows us to set some options for the model. For example, the ````rootName```` option allows us to provide a custom name for
 * the root node, which is sometimes desirable when the model's "IfcProject" element's name is not suitable:
 *
 * ````javascript
 * model.on("loaded", () => {
 *      treeView.addModel(model.id, {
 *          rootName: "Schependomlaan Model"
 *      });
 * });
 * ````
 *
 * ## Initially Expanding the Hierarchy
 *
 * We can also configure TreeViewPlugin to initially expand each model's nodes to a given depth.
 *
 * Let's automatically expand the first three nodes from the root, for every model added:
 *
 * ````javascript
 * const treeView = new TreeViewPlugin(viewer, {
 *      containerElement: document.getElementById("myTreeViewContainer"),
 *      autoExpandDepth: 3
 * });
 * ````
 *
 * ## Showing a Node by ID
 *
 * We can show a given node using its ID. This causes the TreeViewPlugin to collapse, then expand and scroll the node into view, then highlight the node.
 *
 * See the documentation for the {@link TreeViewPlugin#showNode} method for more information, including how to define a custom highlighted appearance for the node using CSS.
 *
 * Let's make the TreeViewPlugin show the node corresponding to whatever object {@link Entity} that we pick:
 *
 * ````javascript
 * viewer.cameraControl.on("picked", function (e) {
 *     var objectId = e.entity.id;
 *     treeView.showNode(objectId);
 * });
 * ````
 *
 * This will de-highlight any node that was previously shown by this method.
 *
 * Note that this method only works if the picked {@link Entity} is an object that belongs to a model that's represented in the TreeViewPlugin.
 *
 * ## Customizing Appearance
 *
 * We can customize the appearance of our TreeViewPlugin by defining custom CSS for its HTML
 * elements. See our example's [source code](https://github.com/xeokit/xeokit-sdk/blob/master/examples/BIMOffline_XKT_Schependomlaan.html)
 * for an example of custom CSS rules.
 *
 * ## Model Hierarchies
 *
 * TreeViewPlugin has three hierarchies for organizing its nodes:
 *
 * * "containment" - organizes the tree nodes to indicate the containment hierarchy of the {@link MetaObject}s.
 * * "types" - groups nodes by their IFC types.
 * * "storeys" - groups nodes within their ````IfcBuildingStoreys````, and sub-groups them by their IFC types.
 *
 * <br>
 * The table below shows what the hierarchies look like:
 * <br>
 *
 * | 1. Containment Hierarchy | 2. Types Hierarchy | 3. Storeys Hierarchy |
 * |---|---|---|
 * | <img src="http://xeokit.io/img/docs/TreeViewPlugin/structureMode.png"> | <img src="http://xeokit.io/img/docs/TreeViewPlugin/typesMode.png"> | <img src="http://xeokit.io/img/docs/TreeViewPlugin/storeysMode.png"> |
 * <br>
 *
 * Let's create a TreeViewPlugin that groups nodes by their building stories and IFC types:
 *
 * ````javascript
 * const treeView = new TreeViewPlugin(viewer, {
 *      containerElement: document.getElementById("myTreeViewContainer"),
 *      hierarchy: "stories"
 * });
 * ````
 *
 * ## Sorting Nodes
 *
 * TreeViewPlugin sorts its tree nodes by default. For a "storeys" hierarchy, it orders ````IfcBuildingStorey```` nodes
 * spatially, with the node for the highest story at the top, down to the lowest at the bottom.
 *
 * For all the hierarchy types ("containment", "classes" and "storeys"), TreeViewPlugin sorts the other node types
 * alphanumerically on their titles.
 *
 * If for some reason you need to prevent sorting, create your TreeViewPlugin with the option disabled, like so:
 *
 * ````javascript
 * const treeView = new TreeViewPlugin(viewer, {
 *      containerElement: document.getElementById("myTreeViewContainer"),
 *      hierarchy: "stories",
 *      sortNodes: false // <<------ Disable node sorting
 * });
 * ````
 *
 * Note that, for all hierarchy modes, node sorting is only done for each model at the time that it is added to the TreeViewPlugin, and will not
 * update dynamically if we later transform the {@link Entity}s corresponding to the nodes.
 *
 * ## Pruning empty nodes
 *
 * Sometimes a model contains subtrees of objects that don't have any geometry. These are models whose
 * {@link MetaModel} contains trees of {@link MetaObject}s that don't have any {@link Entity}s in the {@link Scene}.
 *
 * For these models, the tree view would contain nodes that don't do anything in the Scene when we interact with them,
 * which is undesirable.
 *
 * By default, TreeViewPlugin will not create nodes for those objects. However, we can override that behaviour if we want
 * to have nodes for those objects (perhaps for debugging the model):
 *
 * ````javascript
 * const treeView = new TreeViewPlugin(viewer, {
 *      containerElement: document.getElementById("myTreeViewContainer"),
 *      hierarchy: "stories",
 *      pruneEmptyNodes: false // <<------ Create nodes for object subtrees without geometry
 * });
 * ````
 *
 * ## Context Menu
 *
 * TreeViewPlugin fires a "contextmenu" event whenever we right-click on a tree node.
 *
 * The event contains:
 *
 * * ````event```` - the original [contextmenu](https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event) [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent)
 * * ````viewer```` - the {@link Viewer}
 * * ````treeViewPlugin```` - the TreeViewPlugin
 * * ````treeViewNode```` - the {@link TreeViewNode} representing the tree node
 *<br><br>
 *
 * Let's use {@link ContextMenu} to show a simple context menu for the node we clicked.
 *
 * [[Run an example](https://xeokit.github.io/xeokit-sdk/examples/index.html#ContextMenu_Canvas_TreeViewPlugin_Custom)]
 *
 * ````javascript
 * import {ContextMenu} from "../src/extras/ContextMenu/ContextMenu.js";
 *
 * const treeViewContextMenu = new ContextMenu({
 *     items: [
 *         [
 *             [
 *                 {
 *                     title: "Hide",
 *                     doAction: function (context) {
 *                         context.treeViewPlugin.withNodeTree(context.treeViewNode, (treeViewNode) => {
 *                             if (treeViewNode.objectId) {
 *                                 const entity = context.viewer.scene.objects[treeViewNode.objectId];
 *                                 if (entity) {
 *                                     entity.visible = false;
 *                                 }
 *                             }
 *                         });
 *                     }
 *                 },
 *                 {
 *                     title: "Hide all",
 *                     doAction: function (context) {
 *                         context.viewer.scene.setObjectsVisible(context.viewer.scene.visibleObjectIds, false);
 *                     }
 *                 }
 *             ],
 *             [
 *                 {
 *                     title: "Show",
 *                     doAction: function (context) {
 *                         context.treeViewPlugin.withNodeTree(context.treeViewNode, (treeViewNode) => {
 *                             if (treeViewNode.objectId) {
 *                                 const entity = context.viewer.scene.objects[treeViewNode.objectId];
 *                                 if (entity) {
 *                                     entity.visible = true;
 *                                     entity.xrayed = false;
 *                                     entity.selected = false;
 *                                 }
 *                             }
 *                         });
 *                     }
 *                 },
 *                 {
 *                     title: "Show all",
 *                     doAction: function (context) {
 *                         const scene = context.viewer.scene;
 *                         scene.setObjectsVisible(scene.objectIds, true);
 *                         scene.setObjectsXRayed(scene.xrayedObjectIds, false);
 *                         scene.setObjectsSelected(scene.selectedObjectIds, false);
 *                     }
 *                 }
 *             ]
 *         ]
 *     ]
 * });
 *
 * treeView.on("contextmenu", (e) => {
 *
 *     const event = e.event;                           // MouseEvent
 *     const viewer = e.viewer;                         // Viewer
 *     const treeViewPlugin = e.treeViewPlugin;         // TreeViewPlugin
 *     const treeViewNode = e.treeViewNode;             // TreeViewNode
 *
 *     treeViewContextMenu.show(e.event.pageX, e.event.pageY);
 *
 *     treeViewContextMenu.context = {
 *         viewer: e.viewer,
 *         treeViewPlugin: e.treeViewPlugin,
 *         treeViewNode: e.treeViewNode
 *     };
 * });
 * ````
 *
 * ## Clicking Node Titles
 *
 * TreeViewPlugin fires a "nodeTitleClicked" event whenever we left-click on a tree node.
 *
 * Like the "contextmenu" event, this event contains:
 *
 * * ````event```` - the original [click](https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event) [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent)
 * * ````viewer```` - the {@link Viewer}
 * * ````treeViewPlugin```` - the TreeViewPlugin
 * * ````treeViewNode```` - the {@link TreeViewNode} representing the tree node
 *<br><br>
 *
 * Let's register a callback to isolate and fit-to-view the {@link Entity}(s) represented by the node. This callback is
 * going to X-ray all the other Entitys, fly the camera to fit the Entity(s) for the clicked node, then hide the other Entitys.
 *
 * [[Run an example](https://xeokit.github.io/xeokit-sdk/examples/index.html#ContextMenu_Canvas_TreeViewPlugin_Custom)]
 *
 * ````javascript
 * treeView.on("nodeTitleClicked", (e) => {
 *     const scene = viewer.scene;
 *     const objectIds = [];
 *     e.treeViewPlugin.withNodeTree(e.treeViewNode, (treeViewNode) => {
 *         if (treeViewNode.objectId) {
 *             objectIds.push(treeViewNode.objectId);
 *         }
 *     });
 *     scene.setObjectsXRayed(scene.objectIds, true);
 *     scene.setObjectsVisible(scene.objectIds, true);
 *     scene.setObjectsXRayed(objectIds, false);
 *     viewer.cameraFlight.flyTo({
 *         aabb: scene.getAABB(objectIds),
 *         duration: 0.5
 *     }, () => {
 *         setTimeout(function () {
 *             scene.setObjectsVisible(scene.xrayedObjectIds, false);
 *             scene.setObjectsXRayed(scene.xrayedObjectIds, false);
 *         }, 500);
 *     });
 * });
 * ````
 *
 * To make the cursor change to a pointer when we hover over the node titles, and also to make the titles change to blue, we'll also define this CSS for the ````<span>```` elements
 * that represent the titles of our TreeViewPlugin nodes:
 *
 * ````css
 * #treeViewContainer ul li span:hover {
 *      color: blue;
 *      cursor: pointer;
 * }
 * ````
 *
 * @class TreeViewPlugin
 */
class TreeViewPlugin extends Plugin {

    /**
     * @constructor
     *
     * @param {Viewer} viewer The Viewer.
     * @param {*} cfg Plugin configuration.
     * @param {String} [cfg.containerElementId]  ID of an existing HTML element to contain the TreeViewPlugin - either this or containerElement is mandatory. When both values are given, the element reference is always preferred to the ID.
     * @param {HTMLElement} cfg.containerElement DOM element to contain the TreeViewPlugin.
     * @param {Boolean} [cfg.autoAddModels=true] When ````true```` (default), will automatically add each model as it's created. Set this ````false```` if you want to manually add models using {@link TreeViewPlugin#addModel} instead.
     * @param {Number} [cfg.autoExpandDepth] Optional depth to which to initially expand the tree.
     * @param {String} [cfg.hierarchy="containment"] How to organize the tree nodes: "containment", "storeys" or "types". See the class documentation for details.
     * @param {Boolean} [cfg.sortNodes=true] When true, will sort the children of each node. For a "storeys" hierarchy, the
     * ````IfcBuildingStorey```` nodes will be ordered spatially, from the highest storey down to the lowest, on the
     * vertical World axis. For all hierarchy types, other node types will be ordered in the ascending alphanumeric order of their titles.
     * @param {Boolean} [cfg.pruneEmptyNodes=true] When true, will not contain nodes that don't have content in the {@link Scene}. These are nodes whose {@link MetaObject}s don't have {@link Entity}s.
     * @param {RenderService} [cfg.renderService] Optional {@link RenderService} to use. Defaults to the {@link TreeViewPlugin}'s default {@link RenderService}.
     * @param {Boolean} [cfg.showIndeterminate=false] When true, will show indeterminate state for checkboxes when some but not all child nodes are checked
     * @param {Boolean} [cfg.showProjectNode=false] When true, will show top level project node when hierarchy is set to "storeys"
     * @param {Function} [cfg.elevationSortFunction] Optional function to replace the default elevation sort function. The function should take two nodes and return -1, 0 or 1. 
     * @param {Function} [cfg.defaultSortFunction] Optional function to replace the default sort function. The function should take two nodes and return -1, 0 or 1.
     */
    constructor(viewer, cfg = {}) {

        super("TreeViewPlugin", viewer);

        /**
         * Contains messages for any errors found while last rebuilding this TreeView.
         * @type {String[]}
         */
        this.errors = [];

        /**
         * True if errors were found generating this TreeView.
         * @type {boolean}
         */
        this.valid = true;

        const containerElement = cfg.containerElement || document.getElementById(cfg.containerElementId);

        if (!(containerElement instanceof HTMLElement)) {
            this.error("Mandatory config expected: valid containerElementId or containerElement");
            return;
        }

        for (let i = 0; ; i++) {
            if (!treeViews[i]) {
                treeViews[i] = this;
                this._index = i;
                this._id = `tree-${i}`;
                break;
            }
        }


        this._containerElement = containerElement;
        this._metaModels = {};
        this._autoAddModels = (cfg.autoAddModels !== false);
        this._autoExpandDepth = (cfg.autoExpandDepth || 0);
        this._sortNodes = (cfg.sortNodes !== false);
        this._viewer = viewer;
        this._rootElement = null;
        this._muteSceneEvents = false;
        this._muteTreeEvents = false;
        this._rootNodes = [];
        this._objectNodes = {}; // Object ID -> Node
        this._nodeNodes = {}; // Node ID -> Node
        this._rootNames = {}; // Node ID -> Root name
        this._sortNodes = cfg.sortNodes;
        this._pruneEmptyNodes = cfg.pruneEmptyNodes;
        this._showListItemElementId = null;
        this._renderService = cfg.renderService || new RenderService(this._containerElement);
        this._showIndeterminate = cfg.showIndeterminate ?? false;
        this._showProjectNode = cfg.showProjectNode ?? false;
        this._elevationSortFunction = cfg.elevationSortFunction ?? undefined;
        this._defaultSortFunction = cfg.defaultSortFunction ?? undefined;

        if (!this._renderService) {
            throw new Error('TreeViewPlugin: no render service set');
        }

        this._containerElement.oncontextmenu = (e) => {
            e.preventDefault();
        };

        this._onObjectVisibility = this._viewer.scene.on("objectVisibility", (entity) => {
            if (this._muteSceneEvents) {
                return;
            }
            const objectId = entity.id;
            const node = this._objectNodes[objectId];
            if (!node) {
                return; // Not in this tree
            }
            const visible = entity.visible;
            const updated = (visible !== node.checked);
            if (!updated) {
                return;
            }
            this._muteTreeEvents = true;
            node.checked = visible;
            if (visible) {
                node.numVisibleEntities++;
            } else {
                node.numVisibleEntities--;
            }

            this._renderService.setCheckbox(node.nodeId, visible);

            let parent = node.parent;
            while (parent) {
                parent.checked = visible;
                if (visible) {
                    parent.numVisibleEntities++;
                } else {
                    parent.numVisibleEntities--;
                }
                const indeterminate = this._showIndeterminate 
                  && parent.numVisibleEntities > 0 
                  && parent.numVisibleEntities < parent.numEntities;
                parent.checked = parent.numVisibleEntities > 0;
                this._renderService.setCheckbox(parent.nodeId, parent.checked, indeterminate);

                parent = parent.parent;
            }

            this._muteTreeEvents = false;
        });

        this._onObjectXrayed = this._viewer.scene.on('objectXRayed', (entity) => {
            if (this._muteSceneEvents) {
                return;
            }
            const objectId = entity.id;
            const node = this._objectNodes[objectId];
            if (!node) {
                return; // Not in this tree
            }
            this._muteTreeEvents = true;
            const xrayed = entity.xrayed;
            const updated = (xrayed !== node.xrayed);
            if (!updated) {
                return;
            }
            node.xrayed = xrayed;

            this._renderService.setXRayed(node.nodeId, xrayed);
            this._muteTreeEvents = false;
        });

        this._switchExpandHandler = (event) => {
            event.preventDefault();
            event.stopPropagation();
            const switchElement = event.target;
            this._expandSwitchElement(switchElement);
        };

        this._switchCollapseHandler = (event) => {
            event.preventDefault();
            event.stopPropagation();
            const switchElement = event.target;
            this._collapseSwitchElement(switchElement);
        };

        this._checkboxChangeHandler = (event) => {
            if (this._muteTreeEvents) {
                return;
            }
            this._muteSceneEvents = true;
            const checkbox = event.target;
            const visible = this._renderService.isChecked(checkbox);
            const nodeId = this._renderService.getIdFromCheckbox(checkbox);

            const checkedNode = this._nodeNodes[nodeId];
            const objects = this._viewer.scene.objects;
            let numUpdated = 0;
            
            this._withNodeTree(checkedNode, (node) => {
                const objectId = node.objectId;
                const entity = objects[objectId];
                const isLeaf = (node.children.length === 0);
                node.numVisibleEntities = visible ? node.numEntities : 0;
                if (isLeaf && (visible !== node.checked)) {
                    numUpdated++;
                }
                node.checked = visible;

                this._renderService.setCheckbox(node.nodeId, visible);
                
                if (entity) {
                    entity.visible = visible;
                }
            });

            let parent = checkedNode.parent;
            while (parent) {
                
                if (visible) {
                    parent.numVisibleEntities += numUpdated;
                } else {
                    parent.numVisibleEntities -= numUpdated;
                }
                const indeterminate = this._showIndeterminate 
                  && parent.numVisibleEntities > 0 
                  && parent.numVisibleEntities < parent.numEntities;
                parent.checked = parent.numVisibleEntities > 0;
                this._renderService.setCheckbox(parent.nodeId, parent.checked, indeterminate);
                
                parent = parent.parent;
            }
            this._muteSceneEvents = false;
        };

        this._hierarchy = cfg.hierarchy || "containment";
        this._autoExpandDepth = cfg.autoExpandDepth || 0;

        if (this._autoAddModels) {
            const modelIds = Object.keys(this.viewer.metaScene.metaModels);
            for (let i = 0, len = modelIds.length; i < len; i++) {
                const modelId = modelIds[i];
                const metaModel = this.viewer.metaScene.metaModels[modelId];
                if (metaModel.finalized) {
                    this.addModel(modelId);
                }
            }
            this.viewer.scene.on("modelLoaded", (modelId) => {
                if (this.viewer.metaScene.metaModels[modelId]) {
                    this.addModel(modelId);
                }
            });
        }

        this.hierarchy = cfg.hierarchy;
    }

    /**
     * Sets how the nodes are organized within this tree view.
     *
     * Accepted values are:
     *
     * * "containment" - organizes the nodes to indicate the containment hierarchy of the IFC objects.
     * * "types" - groups the nodes within their IFC types.
     * * "storeys" - groups the nodes within ````IfcBuildingStoreys```` and sub-groups them by their IFC types.
     *
     * <br>
     * This can be updated dynamically.
     *
     * Default value is "containment".
     *
     * @type {String}
     */
    set hierarchy(hierarchy) {
        hierarchy = hierarchy || "containment";
        if (hierarchy !== "containment" && hierarchy !== "storeys" && hierarchy !== "types") {
            this.error("Unsupported value for `hierarchy' - defaulting to 'containment'");
            hierarchy = "containment";
        }
        if (this._hierarchy === hierarchy) {
            return;
        }
        this._hierarchy = hierarchy;
        this._createNodes();
    }

    /**
     * Gets how the nodes are organized within this tree view.
     *
     * @type {String}
     */
    get hierarchy() {
        return this._hierarchy;
    }

    /**
     * Adds a model to this tree view.
     *
     * The model will be automatically removed when destroyed.
     *
     * To automatically add each model as it's created, instead of manually calling this method each time,
     * provide a ````autoAddModels: true```` to the TreeViewPlugin constructor.
     *
     * @param {String} modelId ID of a model {@link Entity} in {@link Scene#models}.
     * @param {Object} [options] Options for model in the tree view.
     * @param {String} [options.rootName] Optional display name for the root node. Ordinary, for "containment"
     * and "storeys" hierarchy types, the tree would derive the root node name from the model's "IfcProject" element
     * name. This option allows to override that name when it is not suitable as a display name.
     */
    addModel(modelId, options = {}) {
        if (!this._containerElement) {
            return;
        }
        const model = this.viewer.scene.models[modelId];
        if (!model) {
            throw "Model not found: " + modelId;
        }
        const metaModel = this.viewer.metaScene.metaModels[modelId];
        if (!metaModel) {
            this.error("MetaModel not found: " + modelId);
            return;
        }
        if (this._metaModels[modelId]) {
            this.warn("Model already added: " + modelId);
            return;
        }
        this._metaModels[modelId] = metaModel;

        if (options && options.rootName) {
            this._rootNames[modelId] = options.rootName;
        }
        
        model.on("destroyed", () => {
            this.removeModel(model.id);
        });
        this._createNodes();
    }

    /**
     * Removes a model from this tree view.
     *
     * Does nothing if model not currently in tree view.
     *
     * @param {String} modelId ID of a model {@link Entity} in {@link Scene#models}.
     */
    removeModel(modelId) {
        if (!this._containerElement) {
            return;
        }
        const metaModel = this._metaModels[modelId];
        if (!metaModel) {
            return;
        }

        if (this._rootNames[modelId]) {
            delete this._rootNames[modelId];
        }

        delete this._metaModels[modelId];
        this._createNodes();
    }

    /**
     * Highlights the tree view node that represents the given object {@link Entity}.
     *
     * This causes the tree view to collapse, then expand to reveal the node, then highlight the node.
     *
     * If a node is previously highlighted, de-highlights that node and collapses the tree first.
     *
     * Note that if the TreeViewPlugin was configured with ````pruneEmptyNodes: true```` (default configuration), then the
     * node won't exist in the tree if it has no Entitys in the {@link Scene}. in that case, nothing will happen.
     *
     * Within the DOM, the node is represented by an ````<li>```` element. This method will add a ````.highlighted-node```` class to
     * the element to make it appear highlighted, removing that class when de-highlighting it again. See the CSS rules
     * in the TreeViewPlugin examples for an example of that class.
     *
     * @param {String} objectId ID of the {@link Entity}.
     */
    showNode(objectId) {
        this.unShowNode();

        const node = this._objectNodes[objectId];
        if (!node) {
            return; // Node may not exist for the given object if (this._pruneEmptyNodes == true)
        }

        this.collapse();

        const nodeId = node.nodeId;

        const switchElement = this._renderService.getSwitchElement(nodeId);
        if (switchElement) {
            this._expandSwitchElement(switchElement);
            switchElement.scrollIntoView();
            return true;
        }
        
        const path = [];
        path.unshift(node);
        let parent = node.parent;
        while (parent) {
            path.unshift(parent);
            parent = parent.parent;
        }

        for (let i = 0, len = path.length; i < len; i++) {
            const switchElement = this._renderService.getSwitchElement(path[i].nodeId);
            if (switchElement) {
                this._expandSwitchElement(switchElement);
            }
        }

        this._renderService.setHighlighted(nodeId, true);

        this._showListItemElementId = nodeId;
    }

    /**
     * De-highlights the node previously shown with {@link TreeViewPlugin#showNode}.
     *
     * Does nothing if no node is currently shown.
     *
     * If the node is currently scrolled into view, keeps the node in view.
     */
    unShowNode() {
        if (!this._showListItemElementId) {
            return;
        }

        this._renderService.setHighlighted(this._showListItemElementId, false);
        
        this._showListItemElementId = null;
    }

    /**
     * Expands the tree to the given depth.
     *
     * Collapses the tree first.
     *
     * @param {Number} depth Depth to expand to.
     */
    expandToDepth(depth) {
        this.collapse();
        const expand = (node, countDepth) => {
            if (countDepth === depth) {
                return;
            }

            const switchElement = this._renderService.getSwitchElement(node.nodeId);
            if (switchElement) {
                this._expandSwitchElement(switchElement);
                const childNodes = node.children;
                for (var i = 0, len = childNodes.length; i < len; i++) {
                    const childNode = childNodes[i];
                    expand(childNode, countDepth + 1);
                }
            }
        };
        for (let i = 0, len = this._rootNodes.length; i < len; i++) {
            const rootNode = this._rootNodes[i];
            expand(rootNode, 0);
        }
    }

    /**
     * Closes all the nodes in the tree.
     */
    collapse() {
        for (let i = 0, len = this._rootNodes.length; i < len; i++) {
            const rootNode = this._rootNodes[i];
            const nodeId = rootNode.nodeId;
            this._collapseNode(nodeId);
        }
    }

    /**
     * Iterates over a subtree of the tree view's {@link TreeViewNode}s, calling the given callback for each
     * node in depth-first pre-order.
     *
     * @param {TreeViewNode} node Root of the subtree.
     * @param {Function} callback Callback called at each {@link TreeViewNode}, with the TreeViewNode given as the argument.
     */
    withNodeTree(node, callback) {
        callback(node);
        const children = node.children;
        if (!children) {
            return;
        }
        for (let i = 0, len = children.length; i < len; i++) {
            this.withNodeTree(children[i], callback);
        }
    }

    /**
     * Destroys this TreeViewPlugin.
     */
    destroy() {
        if (!this._containerElement) {
            return;
        }
        this._metaModels = {};
        if (this._rootElement && !this._destroyed) {
            this._rootElement.parentNode.removeChild(this._rootElement);
            this._viewer.scene.off(this._onObjectVisibility);
            this._destroyed = true;
        }
        delete treeViews[this._index];
        super.destroy();
    }

    _createNodes() {
        if (this._rootElement) {
            this._rootElement.parentNode.removeChild(this._rootElement);
            this._rootElement = null;
        }

        this._rootNodes = [];
        this._objectNodes = {};
        this._nodeNodes = {};
        this._validate();
        if (this.valid || (this._hierarchy !== "storeys")) {
            this._createEnabledNodes();
        } else {
            this._createDisabledNodes();
        }
    }

    _validate() {
        this.errors = [];
        switch (this._hierarchy) {
            case "storeys":
                this.valid = this._validateMetaModelForStoreysHierarchy();
                break;
            case "types":
                this.valid = (this._rootNodes.length > 0);
                break;
            case "containment":
            default:
                this.valid = (this._rootNodes.length > 0);
                break;
        }
        return this.valid;
    }

    _validateMetaModelForStoreysHierarchy(level = 0, ctx, buildingNode) {
        // ctx = ctx || {
        //     foundIFCBuildingStoreys: false
        // };
        // const metaObjectType = metaObject.type;
        // const children = metaObject.children;
        // if (metaObjectType === "IfcBuilding") {
        //     buildingNode = true;
        // } else if (metaObjectType === "IfcBuildingStorey") {
        //     if (!buildingNode) {
        //         errors.push("Can't build storeys hierarchy: IfcBuildingStorey found without parent IfcBuilding");
        //         return false;
        //     }
        //     ctx.foundIFCBuildingStoreys = true;
        // }
        // if (children) {
        //     for (let i = 0, len = children.length; i < len; i++) {
        //         const childMetaObject = children[i];
        //         if (!this._validateMetaModelForStoreysHierarchy(childMetaObject, errors, level + 1, ctx, buildingNode)) {
        //             return false;
        //         }
        //     }
        // }
        // if (level === 0) {
        //     if (!ctx.foundIFCBuildingStoreys) {
        //         // errors.push("Can't build storeys hierarchy: no IfcBuildingStoreys found");
        //     }
        // }
        return true;
    }

    _createEnabledNodes() {
        if (this._pruneEmptyNodes) {
            this._findEmptyNodes();
        }
        switch (this._hierarchy) {
            case "storeys":
                this._createStoreysNodes();
                if (this._rootNodes.length === 0) {
                    this.error("Failed to build storeys hierarchy");
                }
                break;
            case "types":
                this._createTypesNodes();
                break;
            case "containment":
            default:
                this._createContainmentNodes();
        }
        if (this._sortNodes) {
            this._doSortNodes();
        }
        this._synchNodesToEntities();
        this._createTrees();
        this.expandToDepth(this._autoExpandDepth);
    }

    _createDisabledNodes() {

        const rootNode = this._renderService.createRootNode();
        this._rootElement = rootNode;
        this._containerElement.appendChild(rootNode);

        const rootMetaObjects = this._viewer.metaScene.rootMetaObjects;

        for (let objectId in rootMetaObjects) {
            const rootMetaObject = rootMetaObjects[objectId];
            const metaObjectType = rootMetaObject.type;
            const metaObjectName = rootMetaObject.name;
            const rootName = ((metaObjectName && metaObjectName !== ""
                && metaObjectName !== "Undefined"
                && metaObjectName !== "Default") ? metaObjectName : metaObjectType);

            const childNode = this._renderService.createDisabledNodeElement(rootName);
            rootNode.appendChild(childNode);
        }
    }


    _findEmptyNodes() {
        const rootMetaObjects = this._viewer.metaScene.rootMetaObjects;
        for (let objectId in rootMetaObjects) {
            this._findEmptyNodes2(rootMetaObjects[objectId]);
        }
    }

    _findEmptyNodes2(metaObject, countEntities = 0) {
        const viewer = this.viewer;
        const scene = viewer.scene;
        const children = metaObject.children;
        const objectId = metaObject.id;
        const entity = scene.objects[objectId];
        metaObject._countEntities = 0;
        if (entity) {
            metaObject._countEntities++;
        }
        if (children) {
            for (let i = 0, len = children.length; i < len; i++) {
                const childMetaObject = children[i];
                childMetaObject._countEntities = this._findEmptyNodes2(childMetaObject);
                metaObject._countEntities += childMetaObject._countEntities;
            }
        }
        return metaObject._countEntities;
    }

    _createStoreysNodes() {
        const rootMetaObjects = this._viewer.metaScene.rootMetaObjects;
        for (let id in rootMetaObjects) {
            this._createStoreysNodes2(rootMetaObjects[id], null, null, null, null);
        }
    }

    _createStoreysNodes2(metaObject, projectNode, buildingNode, storeyNode, typeNodes) {
        if (this._pruneEmptyNodes && (metaObject._countEntities === 0)) {
            return;
        }
        const metaObjectType = metaObject.type;
        const metaObjectName = metaObject.name;
        const children = metaObject.children;
        const objectId = metaObject.id;

        if (this._showProjectNode && metaObjectType === 'IfcProject') {
            projectNode = {
                nodeId: `${this._id}-${objectId}`,
                objectId,
                title: (metaObject.metaModels.length === 0) ? metaObjectName : this._rootNames[metaObject.metaModels[0].id] || ((metaObjectName && metaObjectName !== "" && metaObjectName !== "Undefined" && metaObjectName !== "Default") ? metaObjectName : metaObjectType),
                type: metaObjectType,
                parent: null,
                numEntities: 0,
                numVisibleEntities: 0,
                checked: false,
                xrayed: false,
                children: [],
            };
            this._rootNodes.push(projectNode);
            this._objectNodes[projectNode.objectId] = projectNode;
            this._nodeNodes[projectNode.nodeId] = projectNode;
        } else if (metaObjectType === "IfcBuilding") {
            buildingNode = {
                nodeId: `${this._id}-${objectId}`,
                objectId: objectId,
                title: (metaObject.metaModels.length === 0) ? metaObjectName : this._rootNames[metaObject.metaModels[0].id] || ((metaObjectName && metaObjectName !== "" && metaObjectName !== "Undefined" && metaObjectName !== "Default") ? metaObjectName : metaObjectType),
                type: metaObjectType,
                parent: projectNode,
                numEntities: 0,
                numVisibleEntities: 0,
                checked: false,
                xrayed: false,
                children: []
            };
            if (projectNode) {
                projectNode.children.push(buildingNode);
            } else {
                this._rootNodes.push(buildingNode);
            }
            this._objectNodes[buildingNode.objectId] = buildingNode;
            this._nodeNodes[buildingNode.nodeId] = buildingNode;
        } else if (metaObjectType === "IfcBuildingStorey") {
            if (!buildingNode) {
                this.error("Failed to build storeys hierarchy for model - model does not have an IfcBuilding object, or is not an IFC model");
                return;
            }
            storeyNode = {
                nodeId: `${this._id}-${objectId}`,
                objectId: objectId,
                title: (metaObjectName && metaObjectName !== "" && metaObjectName !== "Undefined" && metaObjectName !== "Default") ? metaObjectName : metaObjectType,
                type: metaObjectType,
                parent: buildingNode,
                numEntities: 0,
                numVisibleEntities: 0,
                checked: false,
                xrayed: false,
                children: []
            };
            buildingNode.children.push(storeyNode);
            this._objectNodes[storeyNode.objectId] = storeyNode;
            this._nodeNodes[storeyNode.nodeId] = storeyNode;
            typeNodes = {};
        } else {
            if (storeyNode) {
                const objects = this._viewer.scene.objects;
                const object = objects[objectId];
                if (object) {
                    typeNodes = typeNodes || {};
                    let typeNode = typeNodes[metaObjectType];
                    if (!typeNode) {
                        typeNode = {
                            nodeId: `${this._id}-${storeyNode.objectId}-${metaObjectType}`,
                            objectId: `${storeyNode.objectId}-${metaObjectType}`,
                            title: metaObjectType,
                            type: metaObjectType,
                            parent: storeyNode,
                            numEntities: 0,
                            numVisibleEntities: 0,
                            checked: false,
                            xrayed: false,
                            children: []
                        };
                        storeyNode.children.push(typeNode);
                        this._objectNodes[typeNode.objectId] = typeNode;
                        this._nodeNodes[typeNode.nodeId] = typeNode;
                        typeNodes[metaObjectType] = typeNode;
                    }
                    const node = {
                        nodeId: `${this._id}-${objectId}`,
                        objectId: objectId,
                        title: (metaObjectName && metaObjectName !== "" && metaObjectName !== "Undefined" && metaObjectName !== "Default") ? metaObjectName : metaObjectType,
                        type: metaObjectType,
                        parent: typeNode,
                        numEntities: 0,
                        numVisibleEntities: 0,
                        checked: false,
                        xrayed: false,
                        children: []
                    };
                    typeNode.children.push(node);
                    this._objectNodes[node.objectId] = node;
                    this._nodeNodes[node.nodeId] = node;
                }
            }
        }
        if (children) {
            for (let i = 0, len = children.length; i < len; i++) {
                const childMetaObject = children[i];
                this._createStoreysNodes2(childMetaObject, projectNode, buildingNode, storeyNode, typeNodes);
            }
        }
    }

    _createTypesNodes() {
        const rootMetaObjects = this._viewer.metaScene.rootMetaObjects;
        for (let id in rootMetaObjects) {
            this._createTypesNodes2(rootMetaObjects[id], null, null);
        }
    }

    _createTypesNodes2(metaObject, rootNode, typeNodes) {
        if (this._pruneEmptyNodes && (metaObject._countEntities === 0)) {
            return;
        }
        const metaObjectType = metaObject.type;
        const metaObjectName = metaObject.name;
        const children = metaObject.children;
        const objectId = metaObject.id;
        if (!metaObject.parent) {
            rootNode = {
                nodeId: `${this._id}-${objectId}`,
                objectId: objectId,
                title: metaObject.metaModels.length === 0 ? metaObjectName : this._rootNames[metaObject.metaModels[0].id] || ((metaObjectName && metaObjectName !== "" && metaObjectName !== "Undefined" && metaObjectName !== "Default") ? metaObjectName : metaObjectType),
                type: metaObjectType,
                parent: null,
                numEntities: 0,
                numVisibleEntities: 0,
                checked: false,
                xrayed: false,
                children: []
            };
            this._rootNodes.push(rootNode);
            this._objectNodes[rootNode.objectId] = rootNode;
            this._nodeNodes[rootNode.nodeId] = rootNode;
            typeNodes = {};
        } else {
            if (rootNode) {
                const objects = this._viewer.scene.objects;
                const object = objects[objectId];
                if (object) {
                    let typeNode = typeNodes[metaObjectType];
                    if (!typeNode) {
                        typeNode = {
                            nodeId: `${this._id}-${rootNode.objectId}-${metaObjectType}`,
                            objectId: `${rootNode.objectId}-${metaObjectType}`,
                            title: metaObjectType,
                            type: metaObjectType,
                            parent: rootNode,
                            numEntities: 0,
                            numVisibleEntities: 0,
                            checked: false,
                            xrayed: false,
                            children: []
                        };
                        rootNode.children.push(typeNode);
                        this._objectNodes[typeNode.objectId] = typeNode;
                        this._nodeNodes[typeNode.nodeId] = typeNode;
                        typeNodes[metaObjectType] = typeNode;
                    }
                    const node = {
                        nodeId: `${this._id}-${objectId}`,
                        objectId: objectId,
                        title: (metaObjectName && metaObjectName !== "" && metaObjectName !== "Default") ? metaObjectName : metaObjectType,
                        type: metaObjectType,
                        parent: typeNode,
                        numEntities: 0,
                        numVisibleEntities: 0,
                        checked: false,
                        xrayed: false,
                        children: []
                    };
                    typeNode.children.push(node);
                    this._objectNodes[node.objectId] = node;
                    this._nodeNodes[node.nodeId] = node;
                }
            }
        }
        if (children) {
            for (let i = 0, len = children.length; i < len; i++) {
                const childMetaObject = children[i];
                this._createTypesNodes2(childMetaObject, rootNode, typeNodes);
            }
        }
    }

    _createContainmentNodes() {
        const rootMetaObjects = this._viewer.metaScene.rootMetaObjects;
        for (let id in rootMetaObjects) {
            this._createContainmentNodes2(rootMetaObjects[id], null);
        }
    }

    _createContainmentNodes2(metaObject, parent) {
        if (this._pruneEmptyNodes && (metaObject._countEntities === 0)) {
            return;
        }
        const metaObjectType = metaObject.type;
        const metaObjectName = metaObject.name || metaObjectType;
        const children = metaObject.children;
        const objectId = metaObject.id;
        const node = {
            nodeId: `${this._id}-${objectId}`,
            objectId: objectId,
            title: (!parent) ? metaObject.metaModels.length === 0 ? metaObjectName : (this._rootNames[metaObject.metaModels[0].id] || metaObjectName) : (metaObjectName && metaObjectName !== "" && metaObjectName !== "Undefined" && metaObjectName !== "Default") ? metaObjectName : metaObjectType,
            type: metaObjectType,
            parent: parent,
            numEntities: 0,
            numVisibleEntities: 0,
            checked: false,
            xrayed: false,
            children: []
        };
        if (parent) {
            parent.children.push(node);
        } else {
            this._rootNodes.push(node);
        }
        this._objectNodes[node.objectId] = node;
        this._nodeNodes[node.nodeId] = node;
        if (children) {
            for (let i = 0, len = children.length; i < len; i++) {
                const childMetaObject = children[i];
                this._createContainmentNodes2(childMetaObject, node);
            }
        }
    }

    _doSortNodes() {
        for (let i = 0, len = this._rootNodes.length; i < len; i++) {
            const rootNode = this._rootNodes[i];
            this._sortChildren(rootNode);
        }
    }

    _sortChildren(node) {
        const children = node.children;
        if (!children || children.length === 0) {
            return;
        }
        const firstChild = children[0];
        if ((this._hierarchy === "storeys" || this._hierarchy === "containment") &&  firstChild.type === "IfcBuildingStorey") {
            if (this._elevationSortFunction) children.sort(this._elevationSortFunction);
            else children.sort(this._getSpatialSortFunc());
        } else {
            if (this._defaultSortFunction) children.sort(this._defaultSortFunction);
            else children.sort(this._alphaSortFunc);
        }
        for (let i = 0, len = children.length; i < len; i++) {
            const node = children[i];
            this._sortChildren(node);
        }
    }

    _getSpatialSortFunc() {
        const viewer = this.viewer;
        const scene = viewer.scene;
        const camera = scene.camera;
        return this._spatialSortFunc || (this._spatialSortFunc = (storey1, storey2) => {
            if (camera.xUp) ; else if (camera.yUp) ; else ;
            const metaScene = this.viewer.metaScene;
            const storey1MetaObject = metaScene.metaObjects[storey1.objectId];
            const storey2MetaObject = metaScene.metaObjects[storey2.objectId];

            if (storey1MetaObject && (storey1MetaObject.attributes && storey1MetaObject.attributes.elevation !== undefined) &&
                storey2MetaObject && (storey2MetaObject.attributes && storey2MetaObject.attributes.elevation !== undefined)) {
                const elevation1 = Number.parseFloat(storey1MetaObject.attributes.elevation);
                const elevation2 = Number.parseFloat(storey2MetaObject.attributes.elevation);
                if (elevation1 > elevation2) {
                    return -1;
                }
                if (elevation1 < elevation2) {
                    return 1;
                }
                return 0;
            } else {
                return 0;
            }
        });
    }

    _alphaSortFunc(node1, node2) {
        const title1 = node1.title.toUpperCase(); // FIXME: Should be case sensitive?
        const title2 = node2.title.toUpperCase();
        if (title1 < title2) {
            return -1;
        }
        if (title1 > title2) {
            return 1;
        }
        return 0;
    }

    _synchNodesToEntities() {
        const objectIds = Object.keys(this.viewer.metaScene.metaObjects);
        const metaObjects = this._viewer.metaScene.metaObjects;
        const objects = this._viewer.scene.objects;
        for (let i = 0, len = objectIds.length; i < len; i++) {
            const objectId = objectIds[i];
            const metaObject = metaObjects[objectId];
            if (metaObject) {
                const node = this._objectNodes[objectId];
                if (node) {
                    const entity = objects[objectId];
                    if (entity) {
                        const visible = entity.visible;
                        node.numEntities = 1;
                        node.xrayed = entity.xrayed;
                        if (visible) {
                            node.numVisibleEntities = 1;
                            node.checked = true;
                        } else {
                            node.numVisibleEntities = 0;
                            node.checked = false;
                        }
                        let parent = node.parent; // Synch parents
                        while (parent) {
                            parent.numEntities++;
                            if (visible) {
                                parent.numVisibleEntities++;
                                parent.checked = true;
                            }
                            parent = parent.parent;
                        }
                    }
                }
            }
        }
    }

    _withNodeTree(node, callback) {
        callback(node);
        const children = node.children;
        if (!children) {
            return;
        }
        for (let i = 0, len = children.length; i < len; i++) {
            this._withNodeTree(children[i], callback);
        }
    }

    _createTrees() {
        if (this._rootNodes.length === 0) {
            return;
        }
        const rootNodeElements = this._rootNodes.map((rootNode) => {
            return this._createNodeElement(rootNode);
        });

        const rootNode = this._renderService.createRootNode();
        rootNodeElements.forEach((nodeElement) => {
            rootNode.appendChild(nodeElement);
        });

        this._containerElement.appendChild(rootNode);
        this._rootElement = rootNode;
    }

    _createNodeElement(node) {
        const contextmenuHandler = (event) =>{
                this.fire("contextmenu", {
                event: event,
                viewer: this._viewer,
                treeViewPlugin: this,
                treeViewNode: node
            });
            event.preventDefault();
        };
        const onclickHandler = (event) => {
            this.fire("nodeTitleClicked", {
                event: event,
                viewer: this._viewer,
                treeViewPlugin: this,
                treeViewNode: node
            });
            event.preventDefault();
        };

        return this._renderService.createNodeElement(node, this._switchExpandHandler, this._checkboxChangeHandler, contextmenuHandler, onclickHandler);
    }

    _expandSwitchElement(switchElement) {
        const expanded = this._renderService.isExpanded(switchElement); 
        if (expanded) {
            return;
        }

        const nodeId = this._renderService.getId(switchElement);

        const nodeElements = this._nodeNodes[nodeId].children.map((node) => {
            return this._createNodeElement(node);
        });

        this._renderService.addChildren(switchElement, nodeElements);

        this._renderService.expand(switchElement, this._switchExpandHandler, this._switchCollapseHandler);
    }

    _collapseNode(nodeId) {
        const switchElement = this._renderService.getSwitchElement(nodeId);
        this._collapseSwitchElement(switchElement);
    }

    _collapseSwitchElement(switchElement) {
        this._renderService.collapse(switchElement, this._switchExpandHandler, this._switchCollapseHandler);
    }
}

/**
 * Default data access strategy for {@link XKTLoaderPlugin}.
 */
class XKTDefaultDataSource {

    constructor(cfg = {}) {
        this.cacheBuster = (cfg.cacheBuster !== false);
    }

    _cacheBusterURL(url) {
        if (!this.cacheBuster) {
            return url;
        }
        const timestamp = new Date().getTime();
        if (url.indexOf('?') > -1) {
            return url + '&_=' + timestamp;
        } else {
            return url + '?_=' + timestamp;
        }
    }

    /**
     * Gets manifest JSON.
     *
     * @param {String|Number} manifestSrc Identifies the manifest JSON asset.
     * @param {Function} ok Fired on successful loading of the manifest JSON asset.
     * @param {Function} error Fired on error while loading the manifest JSON asset.
     */
    getManifest(manifestSrc, ok, error) {
        utils.loadJSON(this._cacheBusterURL(manifestSrc),
            (json) => {
                ok(json);
            },
            function (errMsg) {
                error(errMsg);
            });
    }

    /**
     * Gets metamodel JSON.
     *
     * @param {String|Number} metaModelSrc Identifies the metamodel JSON asset.
     * @param {Function} ok Fired on successful loading of the metamodel JSON asset.
     * @param {Function} error Fired on error while loading the metamodel JSON asset.
     */
    getMetaModel(metaModelSrc, ok, error) {
        utils.loadJSON(this._cacheBusterURL(metaModelSrc),
            (json) => {
                ok(json);
            },
            function (errMsg) {
                error(errMsg);
            });
    }

    /**
     * Gets the contents of the given ````.xkt```` file in an arraybuffer.
     *
     * @param {String|Number} src Path or ID of an ````.xkt```` file.
     * @param {Function} ok Callback fired on success, argument is the ````.xkt```` file in an arraybuffer.
     * @param {Function} error Callback fired on error.
     */
    getXKT(src, ok, error) {
        var defaultCallback = () => {
        };
        ok = ok || defaultCallback;
        error = error || defaultCallback;
        const dataUriRegex = /^data:(.*?)(;base64)?,(.*)$/;
        const dataUriRegexResult = src.match(dataUriRegex);
        if (dataUriRegexResult) { // Safari can't handle data URIs through XMLHttpRequest
            const isBase64 = !!dataUriRegexResult[2];
            var data = dataUriRegexResult[3];
            data = window.decodeURIComponent(data);
            if (isBase64) {
                data = window.atob(data);
            }
            try {
                const buffer = new ArrayBuffer(data.length);
                const view = new Uint8Array(buffer);
                for (var i = 0; i < data.length; i++) {
                    view[i] = data.charCodeAt(i);
                }
                ok(buffer);
            } catch (errMsg) {
                error(errMsg);
            }
        } else {
            const request = new XMLHttpRequest();
            request.open('GET', this._cacheBusterURL(src), true);
            request.responseType = 'arraybuffer';
            request.onreadystatechange = function () {
                if (request.readyState === 4) {
                    if (request.status === 200) {
                        ok(request.response);
                    } else {
                        error('getXKT error : ' + request.response);
                    }
                }
            };
            request.send(null);
        }
    }
}

/*! pako 2.1.0 https://github.com/nodeca/pako @license (MIT AND Zlib) */
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).pako={});}(undefined,(function(t){function e(t){let e=t.length;for(;--e>=0;)t[e]=0;}const a=256,i=286,n=30,s=15,r=new Uint8Array([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0]),o=new Uint8Array([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13]),l=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7]),h=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),d=new Array(576);e(d);const _=new Array(60);e(_);const f=new Array(512);e(f);const c=new Array(256);e(c);const u=new Array(29);e(u);const w=new Array(n);function m(t,e,a,i,n){this.static_tree=t,this.extra_bits=e,this.extra_base=a,this.elems=i,this.max_length=n,this.has_stree=t&&t.length;}let b,g,p;function k(t,e){this.dyn_tree=t,this.max_code=0,this.stat_desc=e;}e(w);const v=t=>t<256?f[t]:f[256+(t>>>7)],y=(t,e)=>{t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255;},x=(t,e,a)=>{t.bi_valid>16-a?(t.bi_buf|=e<<t.bi_valid&65535,y(t,t.bi_buf),t.bi_buf=e>>16-t.bi_valid,t.bi_valid+=a-16):(t.bi_buf|=e<<t.bi_valid&65535,t.bi_valid+=a);},z=(t,e,a)=>{x(t,a[2*e],a[2*e+1]);},A=(t,e)=>{let a=0;do{a|=1&t,t>>>=1,a<<=1;}while(--e>0);return a>>>1},E=(t,e,a)=>{const i=new Array(16);let n,r,o=0;for(n=1;n<=s;n++)o=o+a[n-1]<<1,i[n]=o;for(r=0;r<=e;r++){let e=t[2*r+1];0!==e&&(t[2*r]=A(i[e]++,e));}},R=t=>{let e;for(e=0;e<i;e++)t.dyn_ltree[2*e]=0;for(e=0;e<n;e++)t.dyn_dtree[2*e]=0;for(e=0;e<19;e++)t.bl_tree[2*e]=0;t.dyn_ltree[512]=1,t.opt_len=t.static_len=0,t.sym_next=t.matches=0;},Z=t=>{t.bi_valid>8?y(t,t.bi_buf):t.bi_valid>0&&(t.pending_buf[t.pending++]=t.bi_buf),t.bi_buf=0,t.bi_valid=0;},U=(t,e,a,i)=>{const n=2*e,s=2*a;return t[n]<t[s]||t[n]===t[s]&&i[e]<=i[a]},S=(t,e,a)=>{const i=t.heap[a];let n=a<<1;for(;n<=t.heap_len&&(n<t.heap_len&&U(e,t.heap[n+1],t.heap[n],t.depth)&&n++,!U(e,i,t.heap[n],t.depth));)t.heap[a]=t.heap[n],a=n,n<<=1;t.heap[a]=i;},D=(t,e,i)=>{let n,s,l,h,d=0;if(0!==t.sym_next)do{n=255&t.pending_buf[t.sym_buf+d++],n+=(255&t.pending_buf[t.sym_buf+d++])<<8,s=t.pending_buf[t.sym_buf+d++],0===n?z(t,s,e):(l=c[s],z(t,l+a+1,e),h=r[l],0!==h&&(s-=u[l],x(t,s,h)),n--,l=v(n),z(t,l,i),h=o[l],0!==h&&(n-=w[l],x(t,n,h)));}while(d<t.sym_next);z(t,256,e);},T=(t,e)=>{const a=e.dyn_tree,i=e.stat_desc.static_tree,n=e.stat_desc.has_stree,r=e.stat_desc.elems;let o,l,h,d=-1;for(t.heap_len=0,t.heap_max=573,o=0;o<r;o++)0!==a[2*o]?(t.heap[++t.heap_len]=d=o,t.depth[o]=0):a[2*o+1]=0;for(;t.heap_len<2;)h=t.heap[++t.heap_len]=d<2?++d:0,a[2*h]=1,t.depth[h]=0,t.opt_len--,n&&(t.static_len-=i[2*h+1]);for(e.max_code=d,o=t.heap_len>>1;o>=1;o--)S(t,a,o);h=r;do{o=t.heap[1],t.heap[1]=t.heap[t.heap_len--],S(t,a,1),l=t.heap[1],t.heap[--t.heap_max]=o,t.heap[--t.heap_max]=l,a[2*h]=a[2*o]+a[2*l],t.depth[h]=(t.depth[o]>=t.depth[l]?t.depth[o]:t.depth[l])+1,a[2*o+1]=a[2*l+1]=h,t.heap[1]=h++,S(t,a,1);}while(t.heap_len>=2);t.heap[--t.heap_max]=t.heap[1],((t,e)=>{const a=e.dyn_tree,i=e.max_code,n=e.stat_desc.static_tree,r=e.stat_desc.has_stree,o=e.stat_desc.extra_bits,l=e.stat_desc.extra_base,h=e.stat_desc.max_length;let d,_,f,c,u,w,m=0;for(c=0;c<=s;c++)t.bl_count[c]=0;for(a[2*t.heap[t.heap_max]+1]=0,d=t.heap_max+1;d<573;d++)_=t.heap[d],c=a[2*a[2*_+1]+1]+1,c>h&&(c=h,m++),a[2*_+1]=c,_>i||(t.bl_count[c]++,u=0,_>=l&&(u=o[_-l]),w=a[2*_],t.opt_len+=w*(c+u),r&&(t.static_len+=w*(n[2*_+1]+u)));if(0!==m){do{for(c=h-1;0===t.bl_count[c];)c--;t.bl_count[c]--,t.bl_count[c+1]+=2,t.bl_count[h]--,m-=2;}while(m>0);for(c=h;0!==c;c--)for(_=t.bl_count[c];0!==_;)f=t.heap[--d],f>i||(a[2*f+1]!==c&&(t.opt_len+=(c-a[2*f+1])*a[2*f],a[2*f+1]=c),_--);}})(t,e),E(a,d,t.bl_count);},O=(t,e,a)=>{let i,n,s=-1,r=e[1],o=0,l=7,h=4;for(0===r&&(l=138,h=3),e[2*(a+1)+1]=65535,i=0;i<=a;i++)n=r,r=e[2*(i+1)+1],++o<l&&n===r||(o<h?t.bl_tree[2*n]+=o:0!==n?(n!==s&&t.bl_tree[2*n]++,t.bl_tree[32]++):o<=10?t.bl_tree[34]++:t.bl_tree[36]++,o=0,s=n,0===r?(l=138,h=3):n===r?(l=6,h=3):(l=7,h=4));},I=(t,e,a)=>{let i,n,s=-1,r=e[1],o=0,l=7,h=4;for(0===r&&(l=138,h=3),i=0;i<=a;i++)if(n=r,r=e[2*(i+1)+1],!(++o<l&&n===r)){if(o<h)do{z(t,n,t.bl_tree);}while(0!=--o);else 0!==n?(n!==s&&(z(t,n,t.bl_tree),o--),z(t,16,t.bl_tree),x(t,o-3,2)):o<=10?(z(t,17,t.bl_tree),x(t,o-3,3)):(z(t,18,t.bl_tree),x(t,o-11,7));o=0,s=n,0===r?(l=138,h=3):n===r?(l=6,h=3):(l=7,h=4);}};let F=false;const L=(t,e,a,i)=>{x(t,0+(i?1:0),3),Z(t),y(t,a),y(t,~a),a&&t.pending_buf.set(t.window.subarray(e,e+a),t.pending),t.pending+=a;};var N=(t,e,i,n)=>{let s,r,o=0;t.level>0?(2===t.strm.data_type&&(t.strm.data_type=(t=>{let e,i=4093624447;for(e=0;e<=31;e++,i>>>=1)if(1&i&&0!==t.dyn_ltree[2*e])return 0;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return 1;for(e=32;e<a;e++)if(0!==t.dyn_ltree[2*e])return 1;return 0})(t)),T(t,t.l_desc),T(t,t.d_desc),o=(t=>{let e;for(O(t,t.dyn_ltree,t.l_desc.max_code),O(t,t.dyn_dtree,t.d_desc.max_code),T(t,t.bl_desc),e=18;e>=3&&0===t.bl_tree[2*h[e]+1];e--);return t.opt_len+=3*(e+1)+5+5+4,e})(t),s=t.opt_len+3+7>>>3,r=t.static_len+3+7>>>3,r<=s&&(s=r)):s=r=i+5,i+4<=s&&-1!==e?L(t,e,i,n):4===t.strategy||r===s?(x(t,2+(n?1:0),3),D(t,d,_)):(x(t,4+(n?1:0),3),((t,e,a,i)=>{let n;for(x(t,e-257,5),x(t,a-1,5),x(t,i-4,4),n=0;n<i;n++)x(t,t.bl_tree[2*h[n]+1],3);I(t,t.dyn_ltree,e-1),I(t,t.dyn_dtree,a-1);})(t,t.l_desc.max_code+1,t.d_desc.max_code+1,o+1),D(t,t.dyn_ltree,t.dyn_dtree)),R(t),n&&Z(t);},B={_tr_init:t=>{F||((()=>{let t,e,a,h,k;const v=new Array(16);for(a=0,h=0;h<28;h++)for(u[h]=a,t=0;t<1<<r[h];t++)c[a++]=h;for(c[a-1]=h,k=0,h=0;h<16;h++)for(w[h]=k,t=0;t<1<<o[h];t++)f[k++]=h;for(k>>=7;h<n;h++)for(w[h]=k<<7,t=0;t<1<<o[h]-7;t++)f[256+k++]=h;for(e=0;e<=s;e++)v[e]=0;for(t=0;t<=143;)d[2*t+1]=8,t++,v[8]++;for(;t<=255;)d[2*t+1]=9,t++,v[9]++;for(;t<=279;)d[2*t+1]=7,t++,v[7]++;for(;t<=287;)d[2*t+1]=8,t++,v[8]++;for(E(d,287,v),t=0;t<n;t++)_[2*t+1]=5,_[2*t]=A(t,5);b=new m(d,r,257,i,s),g=new m(_,o,0,n,s),p=new m(new Array(0),l,0,19,7);})(),F=true),t.l_desc=new k(t.dyn_ltree,b),t.d_desc=new k(t.dyn_dtree,g),t.bl_desc=new k(t.bl_tree,p),t.bi_buf=0,t.bi_valid=0,R(t);},_tr_stored_block:L,_tr_flush_block:N,_tr_tally:(t,e,i)=>(t.pending_buf[t.sym_buf+t.sym_next++]=e,t.pending_buf[t.sym_buf+t.sym_next++]=e>>8,t.pending_buf[t.sym_buf+t.sym_next++]=i,0===e?t.dyn_ltree[2*i]++:(t.matches++,e--,t.dyn_ltree[2*(c[i]+a+1)]++,t.dyn_dtree[2*v(e)]++),t.sym_next===t.sym_end),_tr_align:t=>{x(t,2,3),z(t,256,d),(t=>{16===t.bi_valid?(y(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):t.bi_valid>=8&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8);})(t);}};var C=(t,e,a,i)=>{let n=65535&t|0,s=t>>>16&65535|0,r=0;for(;0!==a;){r=a>2e3?2e3:a,a-=r;do{n=n+e[i++]|0,s=s+n|0;}while(--r);n%=65521,s%=65521;}return n|s<<16|0};const M=new Uint32Array((()=>{let t,e=[];for(var a=0;a<256;a++){t=a;for(var i=0;i<8;i++)t=1&t?3988292384^t>>>1:t>>>1;e[a]=t;}return e})());var H=(t,e,a,i)=>{const n=M,s=i+a;t^=-1;for(let a=i;a<s;a++)t=t>>>8^n[255&(t^e[a])];return -1^t},j={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"},K={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8};const{_tr_init:P,_tr_stored_block:Y,_tr_flush_block:G,_tr_tally:X,_tr_align:W}=B,{Z_NO_FLUSH:q,Z_PARTIAL_FLUSH:J,Z_FULL_FLUSH:Q,Z_FINISH:V,Z_BLOCK:$,Z_OK:tt,Z_STREAM_END:et,Z_STREAM_ERROR:at,Z_DATA_ERROR:it,Z_BUF_ERROR:nt,Z_DEFAULT_COMPRESSION:st,Z_FILTERED:rt,Z_HUFFMAN_ONLY:ot,Z_RLE:lt,Z_FIXED:ht,Z_DEFAULT_STRATEGY:dt,Z_UNKNOWN:_t,Z_DEFLATED:ft}=K,ct=258,ut=262,wt=42,mt=113,bt=666,gt=(t,e)=>(t.msg=j[e],e),pt=t=>2*t-(t>4?9:0),kt=t=>{let e=t.length;for(;--e>=0;)t[e]=0;},vt=t=>{let e,a,i,n=t.w_size;e=t.hash_size,i=e;do{a=t.head[--i],t.head[i]=a>=n?a-n:0;}while(--e);e=n,i=e;do{a=t.prev[--i],t.prev[i]=a>=n?a-n:0;}while(--e)};let yt=(t,e,a)=>(e<<t.hash_shift^a)&t.hash_mask;const xt=t=>{const e=t.state;let a=e.pending;a>t.avail_out&&(a=t.avail_out),0!==a&&(t.output.set(e.pending_buf.subarray(e.pending_out,e.pending_out+a),t.next_out),t.next_out+=a,e.pending_out+=a,t.total_out+=a,t.avail_out-=a,e.pending-=a,0===e.pending&&(e.pending_out=0));},zt=(t,e)=>{G(t,t.block_start>=0?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,xt(t.strm);},At=(t,e)=>{t.pending_buf[t.pending++]=e;},Et=(t,e)=>{t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e;},Rt=(t,e,a,i)=>{let n=t.avail_in;return n>i&&(n=i),0===n?0:(t.avail_in-=n,e.set(t.input.subarray(t.next_in,t.next_in+n),a),1===t.state.wrap?t.adler=C(t.adler,e,n,a):2===t.state.wrap&&(t.adler=H(t.adler,e,n,a)),t.next_in+=n,t.total_in+=n,n)},Zt=(t,e)=>{let a,i,n=t.max_chain_length,s=t.strstart,r=t.prev_length,o=t.nice_match;const l=t.strstart>t.w_size-ut?t.strstart-(t.w_size-ut):0,h=t.window,d=t.w_mask,_=t.prev,f=t.strstart+ct;let c=h[s+r-1],u=h[s+r];t.prev_length>=t.good_match&&(n>>=2),o>t.lookahead&&(o=t.lookahead);do{if(a=e,h[a+r]===u&&h[a+r-1]===c&&h[a]===h[s]&&h[++a]===h[s+1]){s+=2,a++;do{}while(h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&s<f);if(i=ct-(f-s),s=f-ct,i>r){if(t.match_start=e,r=i,i>=o)break;c=h[s+r-1],u=h[s+r];}}}while((e=_[e&d])>l&&0!=--n);return r<=t.lookahead?r:t.lookahead},Ut=t=>{const e=t.w_size;let a,i,n;do{if(i=t.window_size-t.lookahead-t.strstart,t.strstart>=e+(e-ut)&&(t.window.set(t.window.subarray(e,e+e-i),0),t.match_start-=e,t.strstart-=e,t.block_start-=e,t.insert>t.strstart&&(t.insert=t.strstart),vt(t),i+=e),0===t.strm.avail_in)break;if(a=Rt(t.strm,t.window,t.strstart+t.lookahead,i),t.lookahead+=a,t.lookahead+t.insert>=3)for(n=t.strstart-t.insert,t.ins_h=t.window[n],t.ins_h=yt(t,t.ins_h,t.window[n+1]);t.insert&&(t.ins_h=yt(t,t.ins_h,t.window[n+3-1]),t.prev[n&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=n,n++,t.insert--,!(t.lookahead+t.insert<3)););}while(t.lookahead<ut&&0!==t.strm.avail_in)},St=(t,e)=>{let a,i,n,s=t.pending_buf_size-5>t.w_size?t.w_size:t.pending_buf_size-5,r=0,o=t.strm.avail_in;do{if(a=65535,n=t.bi_valid+42>>3,t.strm.avail_out<n)break;if(n=t.strm.avail_out-n,i=t.strstart-t.block_start,a>i+t.strm.avail_in&&(a=i+t.strm.avail_in),a>n&&(a=n),a<s&&(0===a&&e!==V||e===q||a!==i+t.strm.avail_in))break;r=e===V&&a===i+t.strm.avail_in?1:0,Y(t,0,0,r),t.pending_buf[t.pending-4]=a,t.pending_buf[t.pending-3]=a>>8,t.pending_buf[t.pending-2]=~a,t.pending_buf[t.pending-1]=~a>>8,xt(t.strm),i&&(i>a&&(i=a),t.strm.output.set(t.window.subarray(t.block_start,t.block_start+i),t.strm.next_out),t.strm.next_out+=i,t.strm.avail_out-=i,t.strm.total_out+=i,t.block_start+=i,a-=i),a&&(Rt(t.strm,t.strm.output,t.strm.next_out,a),t.strm.next_out+=a,t.strm.avail_out-=a,t.strm.total_out+=a);}while(0===r);return o-=t.strm.avail_in,o&&(o>=t.w_size?(t.matches=2,t.window.set(t.strm.input.subarray(t.strm.next_in-t.w_size,t.strm.next_in),0),t.strstart=t.w_size,t.insert=t.strstart):(t.window_size-t.strstart<=o&&(t.strstart-=t.w_size,t.window.set(t.window.subarray(t.w_size,t.w_size+t.strstart),0),t.matches<2&&t.matches++,t.insert>t.strstart&&(t.insert=t.strstart)),t.window.set(t.strm.input.subarray(t.strm.next_in-o,t.strm.next_in),t.strstart),t.strstart+=o,t.insert+=o>t.w_size-t.insert?t.w_size-t.insert:o),t.block_start=t.strstart),t.high_water<t.strstart&&(t.high_water=t.strstart),r?4:e!==q&&e!==V&&0===t.strm.avail_in&&t.strstart===t.block_start?2:(n=t.window_size-t.strstart,t.strm.avail_in>n&&t.block_start>=t.w_size&&(t.block_start-=t.w_size,t.strstart-=t.w_size,t.window.set(t.window.subarray(t.w_size,t.w_size+t.strstart),0),t.matches<2&&t.matches++,n+=t.w_size,t.insert>t.strstart&&(t.insert=t.strstart)),n>t.strm.avail_in&&(n=t.strm.avail_in),n&&(Rt(t.strm,t.window,t.strstart,n),t.strstart+=n,t.insert+=n>t.w_size-t.insert?t.w_size-t.insert:n),t.high_water<t.strstart&&(t.high_water=t.strstart),n=t.bi_valid+42>>3,n=t.pending_buf_size-n>65535?65535:t.pending_buf_size-n,s=n>t.w_size?t.w_size:n,i=t.strstart-t.block_start,(i>=s||(i||e===V)&&e!==q&&0===t.strm.avail_in&&i<=n)&&(a=i>n?n:i,r=e===V&&0===t.strm.avail_in&&a===i?1:0,Y(t,t.block_start,a,r),t.block_start+=a,xt(t.strm)),r?3:1)},Dt=(t,e)=>{let a,i;for(;;){if(t.lookahead<ut){if(Ut(t),t.lookahead<ut&&e===q)return 1;if(0===t.lookahead)break}if(a=0,t.lookahead>=3&&(t.ins_h=yt(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),0!==a&&t.strstart-a<=t.w_size-ut&&(t.match_length=Zt(t,a)),t.match_length>=3)if(i=X(t,t.strstart-t.match_start,t.match_length-3),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=3){t.match_length--;do{t.strstart++,t.ins_h=yt(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart;}while(0!=--t.match_length);t.strstart++;}else t.strstart+=t.match_length,t.match_length=0,t.ins_h=t.window[t.strstart],t.ins_h=yt(t,t.ins_h,t.window[t.strstart+1]);else i=X(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++;if(i&&(zt(t,false),0===t.strm.avail_out))return 1}return t.insert=t.strstart<2?t.strstart:2,e===V?(zt(t,true),0===t.strm.avail_out?3:4):t.sym_next&&(zt(t,false),0===t.strm.avail_out)?1:2},Tt=(t,e)=>{let a,i,n;for(;;){if(t.lookahead<ut){if(Ut(t),t.lookahead<ut&&e===q)return 1;if(0===t.lookahead)break}if(a=0,t.lookahead>=3&&(t.ins_h=yt(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),t.prev_length=t.match_length,t.prev_match=t.match_start,t.match_length=2,0!==a&&t.prev_length<t.max_lazy_match&&t.strstart-a<=t.w_size-ut&&(t.match_length=Zt(t,a),t.match_length<=5&&(t.strategy===rt||3===t.match_length&&t.strstart-t.match_start>4096)&&(t.match_length=2)),t.prev_length>=3&&t.match_length<=t.prev_length){n=t.strstart+t.lookahead-3,i=X(t,t.strstart-1-t.prev_match,t.prev_length-3),t.lookahead-=t.prev_length-1,t.prev_length-=2;do{++t.strstart<=n&&(t.ins_h=yt(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart);}while(0!=--t.prev_length);if(t.match_available=0,t.match_length=2,t.strstart++,i&&(zt(t,false),0===t.strm.avail_out))return 1}else if(t.match_available){if(i=X(t,0,t.window[t.strstart-1]),i&&zt(t,false),t.strstart++,t.lookahead--,0===t.strm.avail_out)return 1}else t.match_available=1,t.strstart++,t.lookahead--;}return t.match_available&&(i=X(t,0,t.window[t.strstart-1]),t.match_available=0),t.insert=t.strstart<2?t.strstart:2,e===V?(zt(t,true),0===t.strm.avail_out?3:4):t.sym_next&&(zt(t,false),0===t.strm.avail_out)?1:2};function Ot(t,e,a,i,n){this.good_length=t,this.max_lazy=e,this.nice_length=a,this.max_chain=i,this.func=n;}const It=[new Ot(0,0,0,0,St),new Ot(4,4,8,4,Dt),new Ot(4,5,16,8,Dt),new Ot(4,6,32,32,Dt),new Ot(4,4,16,16,Tt),new Ot(8,16,32,32,Tt),new Ot(8,16,128,128,Tt),new Ot(8,32,128,256,Tt),new Ot(32,128,258,1024,Tt),new Ot(32,258,258,4096,Tt)];function Ft(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=ft,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new Uint16Array(1146),this.dyn_dtree=new Uint16Array(122),this.bl_tree=new Uint16Array(78),kt(this.dyn_ltree),kt(this.dyn_dtree),kt(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new Uint16Array(16),this.heap=new Uint16Array(573),kt(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new Uint16Array(573),kt(this.depth),this.sym_buf=0,this.lit_bufsize=0,this.sym_next=0,this.sym_end=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0;}const Lt=t=>{if(!t)return 1;const e=t.state;return !e||e.strm!==t||e.status!==wt&&57!==e.status&&69!==e.status&&73!==e.status&&91!==e.status&&103!==e.status&&e.status!==mt&&e.status!==bt?1:0},Nt=t=>{if(Lt(t))return gt(t,at);t.total_in=t.total_out=0,t.data_type=_t;const e=t.state;return e.pending=0,e.pending_out=0,e.wrap<0&&(e.wrap=-e.wrap),e.status=2===e.wrap?57:e.wrap?wt:mt,t.adler=2===e.wrap?0:1,e.last_flush=-2,P(e),tt},Bt=t=>{const e=Nt(t);var a;return e===tt&&((a=t.state).window_size=2*a.w_size,kt(a.head),a.max_lazy_match=It[a.level].max_lazy,a.good_match=It[a.level].good_length,a.nice_match=It[a.level].nice_length,a.max_chain_length=It[a.level].max_chain,a.strstart=0,a.block_start=0,a.lookahead=0,a.insert=0,a.match_length=a.prev_length=2,a.match_available=0,a.ins_h=0),e},Ct=(t,e,a,i,n,s)=>{if(!t)return at;let r=1;if(e===st&&(e=6),i<0?(r=0,i=-i):i>15&&(r=2,i-=16),n<1||n>9||a!==ft||i<8||i>15||e<0||e>9||s<0||s>ht||8===i&&1!==r)return gt(t,at);8===i&&(i=9);const o=new Ft;return t.state=o,o.strm=t,o.status=wt,o.wrap=r,o.gzhead=null,o.w_bits=i,o.w_size=1<<o.w_bits,o.w_mask=o.w_size-1,o.hash_bits=n+7,o.hash_size=1<<o.hash_bits,o.hash_mask=o.hash_size-1,o.hash_shift=~~((o.hash_bits+3-1)/3),o.window=new Uint8Array(2*o.w_size),o.head=new Uint16Array(o.hash_size),o.prev=new Uint16Array(o.w_size),o.lit_bufsize=1<<n+6,o.pending_buf_size=4*o.lit_bufsize,o.pending_buf=new Uint8Array(o.pending_buf_size),o.sym_buf=o.lit_bufsize,o.sym_end=3*(o.lit_bufsize-1),o.level=e,o.strategy=s,o.method=a,Bt(t)};var Mt={deflateInit:(t,e)=>Ct(t,e,ft,15,8,dt),deflateInit2:Ct,deflateReset:Bt,deflateResetKeep:Nt,deflateSetHeader:(t,e)=>Lt(t)||2!==t.state.wrap?at:(t.state.gzhead=e,tt),deflate:(t,e)=>{if(Lt(t)||e>$||e<0)return t?gt(t,at):at;const a=t.state;if(!t.output||0!==t.avail_in&&!t.input||a.status===bt&&e!==V)return gt(t,0===t.avail_out?nt:at);const i=a.last_flush;if(a.last_flush=e,0!==a.pending){if(xt(t),0===t.avail_out)return a.last_flush=-1,tt}else if(0===t.avail_in&&pt(e)<=pt(i)&&e!==V)return gt(t,nt);if(a.status===bt&&0!==t.avail_in)return gt(t,nt);if(a.status===wt&&0===a.wrap&&(a.status=mt),a.status===wt){let e=ft+(a.w_bits-8<<4)<<8,i=-1;if(i=a.strategy>=ot||a.level<2?0:a.level<6?1:6===a.level?2:3,e|=i<<6,0!==a.strstart&&(e|=32),e+=31-e%31,Et(a,e),0!==a.strstart&&(Et(a,t.adler>>>16),Et(a,65535&t.adler)),t.adler=1,a.status=mt,xt(t),0!==a.pending)return a.last_flush=-1,tt}if(57===a.status)if(t.adler=0,At(a,31),At(a,139),At(a,8),a.gzhead)At(a,(a.gzhead.text?1:0)+(a.gzhead.hcrc?2:0)+(a.gzhead.extra?4:0)+(a.gzhead.name?8:0)+(a.gzhead.comment?16:0)),At(a,255&a.gzhead.time),At(a,a.gzhead.time>>8&255),At(a,a.gzhead.time>>16&255),At(a,a.gzhead.time>>24&255),At(a,9===a.level?2:a.strategy>=ot||a.level<2?4:0),At(a,255&a.gzhead.os),a.gzhead.extra&&a.gzhead.extra.length&&(At(a,255&a.gzhead.extra.length),At(a,a.gzhead.extra.length>>8&255)),a.gzhead.hcrc&&(t.adler=H(t.adler,a.pending_buf,a.pending,0)),a.gzindex=0,a.status=69;else if(At(a,0),At(a,0),At(a,0),At(a,0),At(a,0),At(a,9===a.level?2:a.strategy>=ot||a.level<2?4:0),At(a,3),a.status=mt,xt(t),0!==a.pending)return a.last_flush=-1,tt;if(69===a.status){if(a.gzhead.extra){let e=a.pending,i=(65535&a.gzhead.extra.length)-a.gzindex;for(;a.pending+i>a.pending_buf_size;){let n=a.pending_buf_size-a.pending;if(a.pending_buf.set(a.gzhead.extra.subarray(a.gzindex,a.gzindex+n),a.pending),a.pending=a.pending_buf_size,a.gzhead.hcrc&&a.pending>e&&(t.adler=H(t.adler,a.pending_buf,a.pending-e,e)),a.gzindex+=n,xt(t),0!==a.pending)return a.last_flush=-1,tt;e=0,i-=n;}let n=new Uint8Array(a.gzhead.extra);a.pending_buf.set(n.subarray(a.gzindex,a.gzindex+i),a.pending),a.pending+=i,a.gzhead.hcrc&&a.pending>e&&(t.adler=H(t.adler,a.pending_buf,a.pending-e,e)),a.gzindex=0;}a.status=73;}if(73===a.status){if(a.gzhead.name){let e,i=a.pending;do{if(a.pending===a.pending_buf_size){if(a.gzhead.hcrc&&a.pending>i&&(t.adler=H(t.adler,a.pending_buf,a.pending-i,i)),xt(t),0!==a.pending)return a.last_flush=-1,tt;i=0;}e=a.gzindex<a.gzhead.name.length?255&a.gzhead.name.charCodeAt(a.gzindex++):0,At(a,e);}while(0!==e);a.gzhead.hcrc&&a.pending>i&&(t.adler=H(t.adler,a.pending_buf,a.pending-i,i)),a.gzindex=0;}a.status=91;}if(91===a.status){if(a.gzhead.comment){let e,i=a.pending;do{if(a.pending===a.pending_buf_size){if(a.gzhead.hcrc&&a.pending>i&&(t.adler=H(t.adler,a.pending_buf,a.pending-i,i)),xt(t),0!==a.pending)return a.last_flush=-1,tt;i=0;}e=a.gzindex<a.gzhead.comment.length?255&a.gzhead.comment.charCodeAt(a.gzindex++):0,At(a,e);}while(0!==e);a.gzhead.hcrc&&a.pending>i&&(t.adler=H(t.adler,a.pending_buf,a.pending-i,i));}a.status=103;}if(103===a.status){if(a.gzhead.hcrc){if(a.pending+2>a.pending_buf_size&&(xt(t),0!==a.pending))return a.last_flush=-1,tt;At(a,255&t.adler),At(a,t.adler>>8&255),t.adler=0;}if(a.status=mt,xt(t),0!==a.pending)return a.last_flush=-1,tt}if(0!==t.avail_in||0!==a.lookahead||e!==q&&a.status!==bt){let i=0===a.level?St(a,e):a.strategy===ot?((t,e)=>{let a;for(;;){if(0===t.lookahead&&(Ut(t),0===t.lookahead)){if(e===q)return 1;break}if(t.match_length=0,a=X(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,a&&(zt(t,false),0===t.strm.avail_out))return 1}return t.insert=0,e===V?(zt(t,true),0===t.strm.avail_out?3:4):t.sym_next&&(zt(t,false),0===t.strm.avail_out)?1:2})(a,e):a.strategy===lt?((t,e)=>{let a,i,n,s;const r=t.window;for(;;){if(t.lookahead<=ct){if(Ut(t),t.lookahead<=ct&&e===q)return 1;if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=3&&t.strstart>0&&(n=t.strstart-1,i=r[n],i===r[++n]&&i===r[++n]&&i===r[++n])){s=t.strstart+ct;do{}while(i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&n<s);t.match_length=ct-(s-n),t.match_length>t.lookahead&&(t.match_length=t.lookahead);}if(t.match_length>=3?(a=X(t,1,t.match_length-3),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(a=X(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),a&&(zt(t,false),0===t.strm.avail_out))return 1}return t.insert=0,e===V?(zt(t,true),0===t.strm.avail_out?3:4):t.sym_next&&(zt(t,false),0===t.strm.avail_out)?1:2})(a,e):It[a.level].func(a,e);if(3!==i&&4!==i||(a.status=bt),1===i||3===i)return 0===t.avail_out&&(a.last_flush=-1),tt;if(2===i&&(e===J?W(a):e!==$&&(Y(a,0,0,false),e===Q&&(kt(a.head),0===a.lookahead&&(a.strstart=0,a.block_start=0,a.insert=0))),xt(t),0===t.avail_out))return a.last_flush=-1,tt}return e!==V?tt:a.wrap<=0?et:(2===a.wrap?(At(a,255&t.adler),At(a,t.adler>>8&255),At(a,t.adler>>16&255),At(a,t.adler>>24&255),At(a,255&t.total_in),At(a,t.total_in>>8&255),At(a,t.total_in>>16&255),At(a,t.total_in>>24&255)):(Et(a,t.adler>>>16),Et(a,65535&t.adler)),xt(t),a.wrap>0&&(a.wrap=-a.wrap),0!==a.pending?tt:et)},deflateEnd:t=>{if(Lt(t))return at;const e=t.state.status;return t.state=null,e===mt?gt(t,it):tt},deflateSetDictionary:(t,e)=>{let a=e.length;if(Lt(t))return at;const i=t.state,n=i.wrap;if(2===n||1===n&&i.status!==wt||i.lookahead)return at;if(1===n&&(t.adler=C(t.adler,e,a,0)),i.wrap=0,a>=i.w_size){0===n&&(kt(i.head),i.strstart=0,i.block_start=0,i.insert=0);let t=new Uint8Array(i.w_size);t.set(e.subarray(a-i.w_size,a),0),e=t,a=i.w_size;}const s=t.avail_in,r=t.next_in,o=t.input;for(t.avail_in=a,t.next_in=0,t.input=e,Ut(i);i.lookahead>=3;){let t=i.strstart,e=i.lookahead-2;do{i.ins_h=yt(i,i.ins_h,i.window[t+3-1]),i.prev[t&i.w_mask]=i.head[i.ins_h],i.head[i.ins_h]=t,t++;}while(--e);i.strstart=t,i.lookahead=2,Ut(i);}return i.strstart+=i.lookahead,i.block_start=i.strstart,i.insert=i.lookahead,i.lookahead=0,i.match_length=i.prev_length=2,i.match_available=0,t.next_in=r,t.input=o,t.avail_in=s,i.wrap=n,tt},deflateInfo:"pako deflate (from Nodeca project)"};const Ht=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var jt=function(t){const e=Array.prototype.slice.call(arguments,1);for(;e.length;){const a=e.shift();if(a){if("object"!=typeof a)throw new TypeError(a+"must be non-object");for(const e in a)Ht(a,e)&&(t[e]=a[e]);}}return t},Kt=t=>{let e=0;for(let a=0,i=t.length;a<i;a++)e+=t[a].length;const a=new Uint8Array(e);for(let e=0,i=0,n=t.length;e<n;e++){let n=t[e];a.set(n,i),i+=n.length;}return a};let Pt=true;try{String.fromCharCode.apply(null,new Uint8Array(1));}catch(t){Pt=false;}const Yt=new Uint8Array(256);for(let t=0;t<256;t++)Yt[t]=t>=252?6:t>=248?5:t>=240?4:t>=224?3:t>=192?2:1;Yt[254]=Yt[254]=1;var Gt=t=>{if("function"==typeof TextEncoder&&TextEncoder.prototype.encode)return (new TextEncoder).encode(t);let e,a,i,n,s,r=t.length,o=0;for(n=0;n<r;n++)a=t.charCodeAt(n),55296==(64512&a)&&n+1<r&&(i=t.charCodeAt(n+1),56320==(64512&i)&&(a=65536+(a-55296<<10)+(i-56320),n++)),o+=a<128?1:a<2048?2:a<65536?3:4;for(e=new Uint8Array(o),s=0,n=0;s<o;n++)a=t.charCodeAt(n),55296==(64512&a)&&n+1<r&&(i=t.charCodeAt(n+1),56320==(64512&i)&&(a=65536+(a-55296<<10)+(i-56320),n++)),a<128?e[s++]=a:a<2048?(e[s++]=192|a>>>6,e[s++]=128|63&a):a<65536?(e[s++]=224|a>>>12,e[s++]=128|a>>>6&63,e[s++]=128|63&a):(e[s++]=240|a>>>18,e[s++]=128|a>>>12&63,e[s++]=128|a>>>6&63,e[s++]=128|63&a);return e},Xt=(t,e)=>{const a=e||t.length;if("function"==typeof TextDecoder&&TextDecoder.prototype.decode)return (new TextDecoder).decode(t.subarray(0,e));let i,n;const s=new Array(2*a);for(n=0,i=0;i<a;){let e=t[i++];if(e<128){s[n++]=e;continue}let r=Yt[e];if(r>4)s[n++]=65533,i+=r-1;else {for(e&=2===r?31:3===r?15:7;r>1&&i<a;)e=e<<6|63&t[i++],r--;r>1?s[n++]=65533:e<65536?s[n++]=e:(e-=65536,s[n++]=55296|e>>10&1023,s[n++]=56320|1023&e);}}return ((t,e)=>{if(e<65534&&t.subarray&&Pt)return String.fromCharCode.apply(null,t.length===e?t:t.subarray(0,e));let a="";for(let i=0;i<e;i++)a+=String.fromCharCode(t[i]);return a})(s,n)},Wt=(t,e)=>{(e=e||t.length)>t.length&&(e=t.length);let a=e-1;for(;a>=0&&128==(192&t[a]);)a--;return a<0||0===a?e:a+Yt[t[a]]>e?a:e};var qt=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0;};const Jt=Object.prototype.toString,{Z_NO_FLUSH:Qt,Z_SYNC_FLUSH:Vt,Z_FULL_FLUSH:$t,Z_FINISH:te,Z_OK:ee,Z_STREAM_END:ae,Z_DEFAULT_COMPRESSION:ie,Z_DEFAULT_STRATEGY:ne,Z_DEFLATED:se}=K;function re(t){this.options=jt({level:ie,method:se,chunkSize:16384,windowBits:15,memLevel:8,strategy:ne},t||{});let e=this.options;e.raw&&e.windowBits>0?e.windowBits=-e.windowBits:e.gzip&&e.windowBits>0&&e.windowBits<16&&(e.windowBits+=16),this.err=0,this.msg="",this.ended=false,this.chunks=[],this.strm=new qt,this.strm.avail_out=0;let a=Mt.deflateInit2(this.strm,e.level,e.method,e.windowBits,e.memLevel,e.strategy);if(a!==ee)throw new Error(j[a]);if(e.header&&Mt.deflateSetHeader(this.strm,e.header),e.dictionary){let t;if(t="string"==typeof e.dictionary?Gt(e.dictionary):"[object ArrayBuffer]"===Jt.call(e.dictionary)?new Uint8Array(e.dictionary):e.dictionary,a=Mt.deflateSetDictionary(this.strm,t),a!==ee)throw new Error(j[a]);this._dict_set=true;}}function oe(t,e){const a=new re(e);if(a.push(t,true),a.err)throw a.msg||j[a.err];return a.result}re.prototype.push=function(t,e){const a=this.strm,i=this.options.chunkSize;let n,s;if(this.ended)return false;for(s=e===~~e?e:true===e?te:Qt,"string"==typeof t?a.input=Gt(t):"[object ArrayBuffer]"===Jt.call(t)?a.input=new Uint8Array(t):a.input=t,a.next_in=0,a.avail_in=a.input.length;;)if(0===a.avail_out&&(a.output=new Uint8Array(i),a.next_out=0,a.avail_out=i),(s===Vt||s===$t)&&a.avail_out<=6)this.onData(a.output.subarray(0,a.next_out)),a.avail_out=0;else {if(n=Mt.deflate(a,s),n===ae)return a.next_out>0&&this.onData(a.output.subarray(0,a.next_out)),n=Mt.deflateEnd(this.strm),this.onEnd(n),this.ended=true,n===ee;if(0!==a.avail_out){if(s>0&&a.next_out>0)this.onData(a.output.subarray(0,a.next_out)),a.avail_out=0;else if(0===a.avail_in)break}else this.onData(a.output);}return true},re.prototype.onData=function(t){this.chunks.push(t);},re.prototype.onEnd=function(t){t===ee&&(this.result=Kt(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg;};var le={Deflate:re,deflate:oe,deflateRaw:function(t,e){return (e=e||{}).raw=true,oe(t,e)},gzip:function(t,e){return (e=e||{}).gzip=true,oe(t,e)}};const he=16209;var de=function(t,e){let a,i,n,s,r,o,l,h,d,_,f,c,u,w,m,b,g,p,k,v,y,x,z,A;const E=t.state;a=t.next_in,z=t.input,i=a+(t.avail_in-5),n=t.next_out,A=t.output,s=n-(e-t.avail_out),r=n+(t.avail_out-257),o=E.dmax,l=E.wsize,h=E.whave,d=E.wnext,_=E.window,f=E.hold,c=E.bits,u=E.lencode,w=E.distcode,m=(1<<E.lenbits)-1,b=(1<<E.distbits)-1;t:do{c<15&&(f+=z[a++]<<c,c+=8,f+=z[a++]<<c,c+=8),g=u[f&m];e:for(;;){if(p=g>>>24,f>>>=p,c-=p,p=g>>>16&255,0===p)A[n++]=65535&g;else {if(!(16&p)){if(0==(64&p)){g=u[(65535&g)+(f&(1<<p)-1)];continue e}if(32&p){E.mode=16191;break t}t.msg="invalid literal/length code",E.mode=he;break t}k=65535&g,p&=15,p&&(c<p&&(f+=z[a++]<<c,c+=8),k+=f&(1<<p)-1,f>>>=p,c-=p),c<15&&(f+=z[a++]<<c,c+=8,f+=z[a++]<<c,c+=8),g=w[f&b];a:for(;;){if(p=g>>>24,f>>>=p,c-=p,p=g>>>16&255,!(16&p)){if(0==(64&p)){g=w[(65535&g)+(f&(1<<p)-1)];continue a}t.msg="invalid distance code",E.mode=he;break t}if(v=65535&g,p&=15,c<p&&(f+=z[a++]<<c,c+=8,c<p&&(f+=z[a++]<<c,c+=8)),v+=f&(1<<p)-1,v>o){t.msg="invalid distance too far back",E.mode=he;break t}if(f>>>=p,c-=p,p=n-s,v>p){if(p=v-p,p>h&&E.sane){t.msg="invalid distance too far back",E.mode=he;break t}if(y=0,x=_,0===d){if(y+=l-p,p<k){k-=p;do{A[n++]=_[y++];}while(--p);y=n-v,x=A;}}else if(d<p){if(y+=l+d-p,p-=d,p<k){k-=p;do{A[n++]=_[y++];}while(--p);if(y=0,d<k){p=d,k-=p;do{A[n++]=_[y++];}while(--p);y=n-v,x=A;}}}else if(y+=d-p,p<k){k-=p;do{A[n++]=_[y++];}while(--p);y=n-v,x=A;}for(;k>2;)A[n++]=x[y++],A[n++]=x[y++],A[n++]=x[y++],k-=3;k&&(A[n++]=x[y++],k>1&&(A[n++]=x[y++]));}else {y=n-v;do{A[n++]=A[y++],A[n++]=A[y++],A[n++]=A[y++],k-=3;}while(k>2);k&&(A[n++]=A[y++],k>1&&(A[n++]=A[y++]));}break}}break}}while(a<i&&n<r);k=c>>3,a-=k,c-=k<<3,f&=(1<<c)-1,t.next_in=a,t.next_out=n,t.avail_in=a<i?i-a+5:5-(a-i),t.avail_out=n<r?r-n+257:257-(n-r),E.hold=f,E.bits=c;};const _e=15,fe=new Uint16Array([3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0]),ce=new Uint8Array([16,16,16,16,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,20,20,20,21,21,21,21,16,72,78]),ue=new Uint16Array([1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0]),we=new Uint8Array([16,16,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,64,64]);var me=(t,e,a,i,n,s,r,o)=>{const l=o.bits;let h,d,_,f,c,u,w=0,m=0,b=0,g=0,p=0,k=0,v=0,y=0,x=0,z=0,A=null;const E=new Uint16Array(16),R=new Uint16Array(16);let Z,U,S,D=null;for(w=0;w<=_e;w++)E[w]=0;for(m=0;m<i;m++)E[e[a+m]]++;for(p=l,g=_e;g>=1&&0===E[g];g--);if(p>g&&(p=g),0===g)return n[s++]=20971520,n[s++]=20971520,o.bits=1,0;for(b=1;b<g&&0===E[b];b++);for(p<b&&(p=b),y=1,w=1;w<=_e;w++)if(y<<=1,y-=E[w],y<0)return -1;if(y>0&&(0===t||1!==g))return -1;for(R[1]=0,w=1;w<_e;w++)R[w+1]=R[w]+E[w];for(m=0;m<i;m++)0!==e[a+m]&&(r[R[e[a+m]]++]=m);if(0===t?(A=D=r,u=20):1===t?(A=fe,D=ce,u=257):(A=ue,D=we,u=0),z=0,m=0,w=b,c=s,k=p,v=0,_=-1,x=1<<p,f=x-1,1===t&&x>852||2===t&&x>592)return 1;for(;;){Z=w-v,r[m]+1<u?(U=0,S=r[m]):r[m]>=u?(U=D[r[m]-u],S=A[r[m]-u]):(U=96,S=0),h=1<<w-v,d=1<<k,b=d;do{d-=h,n[c+(z>>v)+d]=Z<<24|U<<16|S|0;}while(0!==d);for(h=1<<w-1;z&h;)h>>=1;if(0!==h?(z&=h-1,z+=h):z=0,m++,0==--E[w]){if(w===g)break;w=e[a+r[m]];}if(w>p&&(z&f)!==_){for(0===v&&(v=p),c+=b,k=w-v,y=1<<k;k+v<g&&(y-=E[k+v],!(y<=0));)k++,y<<=1;if(x+=1<<k,1===t&&x>852||2===t&&x>592)return 1;_=z&f,n[_]=p<<24|k<<16|c-s|0;}}return 0!==z&&(n[c+z]=w-v<<24|64<<16|0),o.bits=p,0};const{Z_FINISH:be,Z_BLOCK:ge,Z_TREES:pe,Z_OK:ke,Z_STREAM_END:ve,Z_NEED_DICT:ye,Z_STREAM_ERROR:xe,Z_DATA_ERROR:ze,Z_MEM_ERROR:Ae,Z_BUF_ERROR:Ee,Z_DEFLATED:Re}=K,Ze=16180,Ue=16190,Se=16191,De=16192,Te=16194,Oe=16199,Ie=16200,Fe=16206,Le=16209,Ne=t=>(t>>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24);function Be(){this.strm=null,this.mode=0,this.last=false,this.wrap=0,this.havedict=false,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new Uint16Array(320),this.work=new Uint16Array(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0;}const Ce=t=>{if(!t)return 1;const e=t.state;return !e||e.strm!==t||e.mode<Ze||e.mode>16211?1:0},Me=t=>{if(Ce(t))return xe;const e=t.state;return t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=Ze,e.last=0,e.havedict=0,e.flags=-1,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new Int32Array(852),e.distcode=e.distdyn=new Int32Array(592),e.sane=1,e.back=-1,ke},He=t=>{if(Ce(t))return xe;const e=t.state;return e.wsize=0,e.whave=0,e.wnext=0,Me(t)},je=(t,e)=>{let a;if(Ce(t))return xe;const i=t.state;return e<0?(a=0,e=-e):(a=5+(e>>4),e<48&&(e&=15)),e&&(e<8||e>15)?xe:(null!==i.window&&i.wbits!==e&&(i.window=null),i.wrap=a,i.wbits=e,He(t))},Ke=(t,e)=>{if(!t)return xe;const a=new Be;t.state=a,a.strm=t,a.window=null,a.mode=Ze;const i=je(t,e);return i!==ke&&(t.state=null),i};let Pe,Ye,Ge=true;const Xe=t=>{if(Ge){Pe=new Int32Array(512),Ye=new Int32Array(32);let e=0;for(;e<144;)t.lens[e++]=8;for(;e<256;)t.lens[e++]=9;for(;e<280;)t.lens[e++]=7;for(;e<288;)t.lens[e++]=8;for(me(1,t.lens,0,288,Pe,0,t.work,{bits:9}),e=0;e<32;)t.lens[e++]=5;me(2,t.lens,0,32,Ye,0,t.work,{bits:5}),Ge=false;}t.lencode=Pe,t.lenbits=9,t.distcode=Ye,t.distbits=5;},We=(t,e,a,i)=>{let n;const s=t.state;return null===s.window&&(s.wsize=1<<s.wbits,s.wnext=0,s.whave=0,s.window=new Uint8Array(s.wsize)),i>=s.wsize?(s.window.set(e.subarray(a-s.wsize,a),0),s.wnext=0,s.whave=s.wsize):(n=s.wsize-s.wnext,n>i&&(n=i),s.window.set(e.subarray(a-i,a-i+n),s.wnext),(i-=n)?(s.window.set(e.subarray(a-i,a),0),s.wnext=i,s.whave=s.wsize):(s.wnext+=n,s.wnext===s.wsize&&(s.wnext=0),s.whave<s.wsize&&(s.whave+=n))),0};var qe={inflateReset:He,inflateReset2:je,inflateResetKeep:Me,inflateInit:t=>Ke(t,15),inflateInit2:Ke,inflate:(t,e)=>{let a,i,n,s,r,o,l,h,d,_,f,c,u,w,m,b,g,p,k,v,y,x,z=0;const A=new Uint8Array(4);let E,R;const Z=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]);if(Ce(t)||!t.output||!t.input&&0!==t.avail_in)return xe;a=t.state,a.mode===Se&&(a.mode=De),r=t.next_out,n=t.output,l=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,h=a.hold,d=a.bits,_=o,f=l,x=ke;t:for(;;)switch(a.mode){case Ze:if(0===a.wrap){a.mode=De;break}for(;d<16;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}if(2&a.wrap&&35615===h){0===a.wbits&&(a.wbits=15),a.check=0,A[0]=255&h,A[1]=h>>>8&255,a.check=H(a.check,A,2,0),h=0,d=0,a.mode=16181;break}if(a.head&&(a.head.done=false),!(1&a.wrap)||(((255&h)<<8)+(h>>8))%31){t.msg="incorrect header check",a.mode=Le;break}if((15&h)!==Re){t.msg="unknown compression method",a.mode=Le;break}if(h>>>=4,d-=4,y=8+(15&h),0===a.wbits&&(a.wbits=y),y>15||y>a.wbits){t.msg="invalid window size",a.mode=Le;break}a.dmax=1<<a.wbits,a.flags=0,t.adler=a.check=1,a.mode=512&h?16189:Se,h=0,d=0;break;case 16181:for(;d<16;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}if(a.flags=h,(255&a.flags)!==Re){t.msg="unknown compression method",a.mode=Le;break}if(57344&a.flags){t.msg="unknown header flags set",a.mode=Le;break}a.head&&(a.head.text=h>>8&1),512&a.flags&&4&a.wrap&&(A[0]=255&h,A[1]=h>>>8&255,a.check=H(a.check,A,2,0)),h=0,d=0,a.mode=16182;case 16182:for(;d<32;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}a.head&&(a.head.time=h),512&a.flags&&4&a.wrap&&(A[0]=255&h,A[1]=h>>>8&255,A[2]=h>>>16&255,A[3]=h>>>24&255,a.check=H(a.check,A,4,0)),h=0,d=0,a.mode=16183;case 16183:for(;d<16;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}a.head&&(a.head.xflags=255&h,a.head.os=h>>8),512&a.flags&&4&a.wrap&&(A[0]=255&h,A[1]=h>>>8&255,a.check=H(a.check,A,2,0)),h=0,d=0,a.mode=16184;case 16184:if(1024&a.flags){for(;d<16;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}a.length=h,a.head&&(a.head.extra_len=h),512&a.flags&&4&a.wrap&&(A[0]=255&h,A[1]=h>>>8&255,a.check=H(a.check,A,2,0)),h=0,d=0;}else a.head&&(a.head.extra=null);a.mode=16185;case 16185:if(1024&a.flags&&(c=a.length,c>o&&(c=o),c&&(a.head&&(y=a.head.extra_len-a.length,a.head.extra||(a.head.extra=new Uint8Array(a.head.extra_len)),a.head.extra.set(i.subarray(s,s+c),y)),512&a.flags&&4&a.wrap&&(a.check=H(a.check,i,c,s)),o-=c,s+=c,a.length-=c),a.length))break t;a.length=0,a.mode=16186;case 16186:if(2048&a.flags){if(0===o)break t;c=0;do{y=i[s+c++],a.head&&y&&a.length<65536&&(a.head.name+=String.fromCharCode(y));}while(y&&c<o);if(512&a.flags&&4&a.wrap&&(a.check=H(a.check,i,c,s)),o-=c,s+=c,y)break t}else a.head&&(a.head.name=null);a.length=0,a.mode=16187;case 16187:if(4096&a.flags){if(0===o)break t;c=0;do{y=i[s+c++],a.head&&y&&a.length<65536&&(a.head.comment+=String.fromCharCode(y));}while(y&&c<o);if(512&a.flags&&4&a.wrap&&(a.check=H(a.check,i,c,s)),o-=c,s+=c,y)break t}else a.head&&(a.head.comment=null);a.mode=16188;case 16188:if(512&a.flags){for(;d<16;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}if(4&a.wrap&&h!==(65535&a.check)){t.msg="header crc mismatch",a.mode=Le;break}h=0,d=0;}a.head&&(a.head.hcrc=a.flags>>9&1,a.head.done=true),t.adler=a.check=0,a.mode=Se;break;case 16189:for(;d<32;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}t.adler=a.check=Ne(h),h=0,d=0,a.mode=Ue;case Ue:if(0===a.havedict)return t.next_out=r,t.avail_out=l,t.next_in=s,t.avail_in=o,a.hold=h,a.bits=d,ye;t.adler=a.check=1,a.mode=Se;case Se:if(e===ge||e===pe)break t;case De:if(a.last){h>>>=7&d,d-=7&d,a.mode=Fe;break}for(;d<3;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}switch(a.last=1&h,h>>>=1,d-=1,3&h){case 0:a.mode=16193;break;case 1:if(Xe(a),a.mode=Oe,e===pe){h>>>=2,d-=2;break t}break;case 2:a.mode=16196;break;case 3:t.msg="invalid block type",a.mode=Le;}h>>>=2,d-=2;break;case 16193:for(h>>>=7&d,d-=7&d;d<32;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}if((65535&h)!=(h>>>16^65535)){t.msg="invalid stored block lengths",a.mode=Le;break}if(a.length=65535&h,h=0,d=0,a.mode=Te,e===pe)break t;case Te:a.mode=16195;case 16195:if(c=a.length,c){if(c>o&&(c=o),c>l&&(c=l),0===c)break t;n.set(i.subarray(s,s+c),r),o-=c,s+=c,l-=c,r+=c,a.length-=c;break}a.mode=Se;break;case 16196:for(;d<14;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}if(a.nlen=257+(31&h),h>>>=5,d-=5,a.ndist=1+(31&h),h>>>=5,d-=5,a.ncode=4+(15&h),h>>>=4,d-=4,a.nlen>286||a.ndist>30){t.msg="too many length or distance symbols",a.mode=Le;break}a.have=0,a.mode=16197;case 16197:for(;a.have<a.ncode;){for(;d<3;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}a.lens[Z[a.have++]]=7&h,h>>>=3,d-=3;}for(;a.have<19;)a.lens[Z[a.have++]]=0;if(a.lencode=a.lendyn,a.lenbits=7,E={bits:a.lenbits},x=me(0,a.lens,0,19,a.lencode,0,a.work,E),a.lenbits=E.bits,x){t.msg="invalid code lengths set",a.mode=Le;break}a.have=0,a.mode=16198;case 16198:for(;a.have<a.nlen+a.ndist;){for(;z=a.lencode[h&(1<<a.lenbits)-1],m=z>>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}if(g<16)h>>>=m,d-=m,a.lens[a.have++]=g;else {if(16===g){for(R=m+2;d<R;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}if(h>>>=m,d-=m,0===a.have){t.msg="invalid bit length repeat",a.mode=Le;break}y=a.lens[a.have-1],c=3+(3&h),h>>>=2,d-=2;}else if(17===g){for(R=m+3;d<R;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}h>>>=m,d-=m,y=0,c=3+(7&h),h>>>=3,d-=3;}else {for(R=m+7;d<R;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}h>>>=m,d-=m,y=0,c=11+(127&h),h>>>=7,d-=7;}if(a.have+c>a.nlen+a.ndist){t.msg="invalid bit length repeat",a.mode=Le;break}for(;c--;)a.lens[a.have++]=y;}}if(a.mode===Le)break;if(0===a.lens[256]){t.msg="invalid code -- missing end-of-block",a.mode=Le;break}if(a.lenbits=9,E={bits:a.lenbits},x=me(1,a.lens,0,a.nlen,a.lencode,0,a.work,E),a.lenbits=E.bits,x){t.msg="invalid literal/lengths set",a.mode=Le;break}if(a.distbits=6,a.distcode=a.distdyn,E={bits:a.distbits},x=me(2,a.lens,a.nlen,a.ndist,a.distcode,0,a.work,E),a.distbits=E.bits,x){t.msg="invalid distances set",a.mode=Le;break}if(a.mode=Oe,e===pe)break t;case Oe:a.mode=Ie;case Ie:if(o>=6&&l>=258){t.next_out=r,t.avail_out=l,t.next_in=s,t.avail_in=o,a.hold=h,a.bits=d,de(t,f),r=t.next_out,n=t.output,l=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,h=a.hold,d=a.bits,a.mode===Se&&(a.back=-1);break}for(a.back=0;z=a.lencode[h&(1<<a.lenbits)-1],m=z>>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}if(b&&0==(240&b)){for(p=m,k=b,v=g;z=a.lencode[v+((h&(1<<p+k)-1)>>p)],m=z>>>24,b=z>>>16&255,g=65535&z,!(p+m<=d);){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}h>>>=p,d-=p,a.back+=p;}if(h>>>=m,d-=m,a.back+=m,a.length=g,0===b){a.mode=16205;break}if(32&b){a.back=-1,a.mode=Se;break}if(64&b){t.msg="invalid literal/length code",a.mode=Le;break}a.extra=15&b,a.mode=16201;case 16201:if(a.extra){for(R=a.extra;d<R;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}a.length+=h&(1<<a.extra)-1,h>>>=a.extra,d-=a.extra,a.back+=a.extra;}a.was=a.length,a.mode=16202;case 16202:for(;z=a.distcode[h&(1<<a.distbits)-1],m=z>>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}if(0==(240&b)){for(p=m,k=b,v=g;z=a.distcode[v+((h&(1<<p+k)-1)>>p)],m=z>>>24,b=z>>>16&255,g=65535&z,!(p+m<=d);){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}h>>>=p,d-=p,a.back+=p;}if(h>>>=m,d-=m,a.back+=m,64&b){t.msg="invalid distance code",a.mode=Le;break}a.offset=g,a.extra=15&b,a.mode=16203;case 16203:if(a.extra){for(R=a.extra;d<R;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}a.offset+=h&(1<<a.extra)-1,h>>>=a.extra,d-=a.extra,a.back+=a.extra;}if(a.offset>a.dmax){t.msg="invalid distance too far back",a.mode=Le;break}a.mode=16204;case 16204:if(0===l)break t;if(c=f-l,a.offset>c){if(c=a.offset-c,c>a.whave&&a.sane){t.msg="invalid distance too far back",a.mode=Le;break}c>a.wnext?(c-=a.wnext,u=a.wsize-c):u=a.wnext-c,c>a.length&&(c=a.length),w=a.window;}else w=n,u=r-a.offset,c=a.length;c>l&&(c=l),l-=c,a.length-=c;do{n[r++]=w[u++];}while(--c);0===a.length&&(a.mode=Ie);break;case 16205:if(0===l)break t;n[r++]=a.length,l--,a.mode=Ie;break;case Fe:if(a.wrap){for(;d<32;){if(0===o)break t;o--,h|=i[s++]<<d,d+=8;}if(f-=l,t.total_out+=f,a.total+=f,4&a.wrap&&f&&(t.adler=a.check=a.flags?H(a.check,n,f,r-f):C(a.check,n,f,r-f)),f=l,4&a.wrap&&(a.flags?h:Ne(h))!==a.check){t.msg="incorrect data check",a.mode=Le;break}h=0,d=0;}a.mode=16207;case 16207:if(a.wrap&&a.flags){for(;d<32;){if(0===o)break t;o--,h+=i[s++]<<d,d+=8;}if(4&a.wrap&&h!==(4294967295&a.total)){t.msg="incorrect length check",a.mode=Le;break}h=0,d=0;}a.mode=16208;case 16208:x=ve;break t;case Le:x=ze;break t;case 16210:return Ae;default:return xe}return t.next_out=r,t.avail_out=l,t.next_in=s,t.avail_in=o,a.hold=h,a.bits=d,(a.wsize||f!==t.avail_out&&a.mode<Le&&(a.mode<Fe||e!==be))&&We(t,t.output,t.next_out,f-t.avail_out),_-=t.avail_in,f-=t.avail_out,t.total_in+=_,t.total_out+=f,a.total+=f,4&a.wrap&&f&&(t.adler=a.check=a.flags?H(a.check,n,f,t.next_out-f):C(a.check,n,f,t.next_out-f)),t.data_type=a.bits+(a.last?64:0)+(a.mode===Se?128:0)+(a.mode===Oe||a.mode===Te?256:0),(0===_&&0===f||e===be)&&x===ke&&(x=Ee),x},inflateEnd:t=>{if(Ce(t))return xe;let e=t.state;return e.window&&(e.window=null),t.state=null,ke},inflateGetHeader:(t,e)=>{if(Ce(t))return xe;const a=t.state;return 0==(2&a.wrap)?xe:(a.head=e,e.done=false,ke)},inflateSetDictionary:(t,e)=>{const a=e.length;let i,n,s;return Ce(t)?xe:(i=t.state,0!==i.wrap&&i.mode!==Ue?xe:i.mode===Ue&&(n=1,n=C(n,e,a,0),n!==i.check)?ze:(s=We(t,e,a,a),s?(i.mode=16210,Ae):(i.havedict=1,ke)))},inflateInfo:"pako inflate (from Nodeca project)"};var Je=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=false;};const Qe=Object.prototype.toString,{Z_NO_FLUSH:Ve,Z_FINISH:$e,Z_OK:ta,Z_STREAM_END:ea,Z_NEED_DICT:aa,Z_STREAM_ERROR:ia,Z_DATA_ERROR:na,Z_MEM_ERROR:sa}=K;function ra(t){this.options=jt({chunkSize:65536,windowBits:15,to:""},t||{});const e=this.options;e.raw&&e.windowBits>=0&&e.windowBits<16&&(e.windowBits=-e.windowBits,0===e.windowBits&&(e.windowBits=-15)),!(e.windowBits>=0&&e.windowBits<16)||t&&t.windowBits||(e.windowBits+=32),e.windowBits>15&&e.windowBits<48&&0==(15&e.windowBits)&&(e.windowBits|=15),this.err=0,this.msg="",this.ended=false,this.chunks=[],this.strm=new qt,this.strm.avail_out=0;let a=qe.inflateInit2(this.strm,e.windowBits);if(a!==ta)throw new Error(j[a]);if(this.header=new Je,qe.inflateGetHeader(this.strm,this.header),e.dictionary&&("string"==typeof e.dictionary?e.dictionary=Gt(e.dictionary):"[object ArrayBuffer]"===Qe.call(e.dictionary)&&(e.dictionary=new Uint8Array(e.dictionary)),e.raw&&(a=qe.inflateSetDictionary(this.strm,e.dictionary),a!==ta)))throw new Error(j[a])}function oa(t,e){const a=new ra(e);if(a.push(t),a.err)throw a.msg||j[a.err];return a.result}ra.prototype.push=function(t,e){const a=this.strm,i=this.options.chunkSize,n=this.options.dictionary;let s,r,o;if(this.ended)return false;for(r=e===~~e?e:true===e?$e:Ve,"[object ArrayBuffer]"===Qe.call(t)?a.input=new Uint8Array(t):a.input=t,a.next_in=0,a.avail_in=a.input.length;;){for(0===a.avail_out&&(a.output=new Uint8Array(i),a.next_out=0,a.avail_out=i),s=qe.inflate(a,r),s===aa&&n&&(s=qe.inflateSetDictionary(a,n),s===ta?s=qe.inflate(a,r):s===na&&(s=aa));a.avail_in>0&&s===ea&&a.state.wrap>0&&0!==t[a.next_in];)qe.inflateReset(a),s=qe.inflate(a,r);switch(s){case ia:case na:case aa:case sa:return this.onEnd(s),this.ended=true,false}if(o=a.avail_out,a.next_out&&(0===a.avail_out||s===ea))if("string"===this.options.to){let t=Wt(a.output,a.next_out),e=a.next_out-t,n=Xt(a.output,t);a.next_out=e,a.avail_out=i-e,e&&a.output.set(a.output.subarray(t,t+e),0),this.onData(n);}else this.onData(a.output.length===a.next_out?a.output:a.output.subarray(0,a.next_out));if(s!==ta||0!==o){if(s===ea)return s=qe.inflateEnd(this.strm),this.onEnd(s),this.ended=true,true;if(0===a.avail_in)break}}return true},ra.prototype.onData=function(t){this.chunks.push(t);},ra.prototype.onEnd=function(t){t===ta&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=Kt(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg;};var la={Inflate:ra,inflate:oa,inflateRaw:function(t,e){return (e=e||{}).raw=true,oa(t,e)},ungzip:oa};const{Deflate:ha,deflate:da,deflateRaw:_a,gzip:fa}=le,{Inflate:ca,inflate:ua,inflateRaw:wa,ungzip:ma}=la;var ba=ha,ga=da,pa=_a,ka=fa,va=ca,ya=ua,xa=wa,za=ma,Aa=K,Ea={Deflate:ba,deflate:ga,deflateRaw:pa,gzip:ka,Inflate:va,inflate:ya,inflateRaw:xa,ungzip:za,constants:Aa};t.Deflate=ba,t.Inflate=va,t.constants=Aa,t.default=Ea,t.deflate=ga,t.deflateRaw=pa,t.gzip=ka,t.inflate=ya,t.inflateRaw=xa,t.ungzip=za,Object.defineProperty(t,"__esModule",{value:true});}));

var p = /*#__PURE__*/Object.freeze({
    __proto__: null
});

/*

 Parser for .XKT Format V1

.XKT specifications: https://github.com/xeokit/xeokit-sdk/wiki/XKT-Format

 DEPRECATED

 */

let pako$a = window.pako || p;
if (!pako$a.inflate) {  // See https://github.com/nodeca/pako/issues/97
    pako$a = pako$a.default;
}

const decompressColor$b = (function () {
    const color2 = new Float32Array(3);
    return function (color) {
        color2[0] = color[0] / 255.0;
        color2[1] = color[1] / 255.0;
        color2[2] = color[2] / 255.0;
        return color2;
    };
})();

function extract$a(elements) {
    return {
        positions: elements[0],
        normals: elements[1],
        indices: elements[2],
        edgeIndices: elements[3],
        meshPositions: elements[4],
        meshIndices: elements[5],
        meshEdgesIndices: elements[6],
        meshColors: elements[7],
        entityIDs: elements[8],
        entityMeshes: elements[9],
        entityIsObjects: elements[10],
        positionsDecodeMatrix: elements[11]
    };
}

function inflate$a(deflatedData) {
    return {
        positions: new Uint16Array(pako$a.inflate(deflatedData.positions).buffer),
        normals: new Int8Array(pako$a.inflate(deflatedData.normals).buffer),
        indices: new Uint32Array(pako$a.inflate(deflatedData.indices).buffer),
        edgeIndices: new Uint32Array(pako$a.inflate(deflatedData.edgeIndices).buffer),
        meshPositions: new Uint32Array(pako$a.inflate(deflatedData.meshPositions).buffer),
        meshIndices: new Uint32Array(pako$a.inflate(deflatedData.meshIndices).buffer),
        meshEdgesIndices: new Uint32Array(pako$a.inflate(deflatedData.meshEdgesIndices).buffer),
        meshColors: new Uint8Array(pako$a.inflate(deflatedData.meshColors).buffer),
        entityIDs: pako$a.inflate(deflatedData.entityIDs, {to: 'string'}),
        entityMeshes: new Uint32Array(pako$a.inflate(deflatedData.entityMeshes).buffer),
        entityIsObjects: new Uint8Array(pako$a.inflate(deflatedData.entityIsObjects).buffer),
        positionsDecodeMatrix: new Float32Array(pako$a.inflate(deflatedData.positionsDecodeMatrix).buffer)
    };
}

function load$b(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx) {

    manifestCtx.getNextId();

    sceneModel.positionsCompression = "precompressed";
    sceneModel.normalsCompression = "precompressed";

    const positions = inflatedData.positions;
    const normals = inflatedData.normals;
    const indices = inflatedData.indices;
    const edgeIndices = inflatedData.edgeIndices;
    const meshPositions = inflatedData.meshPositions;
    const meshIndices = inflatedData.meshIndices;
    const meshEdgesIndices = inflatedData.meshEdgesIndices;
    const meshColors = inflatedData.meshColors;
    const entityIDs = JSON.parse(inflatedData.entityIDs);
    const entityMeshes = inflatedData.entityMeshes;
    const entityIsObjects = inflatedData.entityIsObjects;
    const numMeshes = meshPositions.length;
    const numEntities = entityMeshes.length;

    for (let i = 0; i < numEntities; i++) {

        const xktEntityId = entityIDs [i];
        const entityId = options.globalizeObjectIds ? math.globalizeObjectId(sceneModel.id, xktEntityId) : xktEntityId;
        const metaObject = viewer.metaScene.metaObjects[entityId];
        const entityDefaults = {};
        const meshDefaults = {};

        if (metaObject) {

            if (options.excludeTypesMap && metaObject.type && options.excludeTypesMap[metaObject.type]) {
                continue;
            }

            if (options.includeTypesMap && metaObject.type && (!options.includeTypesMap[metaObject.type])) {
                continue;
            }

            const props = options.objectDefaults ? options.objectDefaults[metaObject.type] || options.objectDefaults["DEFAULT"] : null;

            if (props) {
                if (props.visible === false) {
                    entityDefaults.visible = false;
                }
                if (props.pickable === false) {
                    entityDefaults.pickable = false;
                }
                if (props.colorize) {
                    meshDefaults.color = props.colorize;
                }
                if (props.opacity !== undefined && props.opacity !== null) {
                    meshDefaults.opacity = props.opacity;
                }
            }
        } else {
            if (options.excludeUnclassifiedObjects) {
                continue;
            }
        }

        const lastEntity = (i === numEntities - 1);
        const meshIds = [];

        for (let j = entityMeshes [i], jlen = lastEntity ? entityMeshes.length : entityMeshes [i + 1]; j < jlen; j++) {

            const lastMesh = (j === (numMeshes - 1));
            const meshId = entityId + ".mesh." + j;

            const color = decompressColor$b(meshColors.subarray((j * 4), (j * 4) + 3));
            const opacity = meshColors[(j * 4) + 3] / 255.0;

            sceneModel.createMesh(utils.apply(meshDefaults, {
                id: meshId,
                primitive: "triangles",
                positionsCompressed: positions.subarray(meshPositions [j], lastMesh ? positions.length : meshPositions [j + 1]),
                normalsCompressed: normals.subarray(meshPositions [j], lastMesh ? positions.length : meshPositions [j + 1]),
                indices: indices.subarray(meshIndices [j], lastMesh ? indices.length : meshIndices [j + 1]),
                edgeIndices: edgeIndices.subarray(meshEdgesIndices [j], lastMesh ? edgeIndices.length : meshEdgesIndices [j + 1]),
                positionsDecodeMatrix: inflatedData.positionsDecodeMatrix,
                color: color,
                opacity: opacity
            }));

            meshIds.push(meshId);
        }

        sceneModel.createEntity(utils.apply(entityDefaults, {
            id: entityId,
            isObject: (entityIsObjects [i] === 1),
            meshIds: meshIds
        }));
    }
}

/** @private */
const ParserV1 = {
    version: 1,
    parse: function (viewer, options, elements, sceneModel, metaModel, manifestCtx) {
        const deflatedData = extract$a(elements);
        const inflatedData = inflate$a(deflatedData);
        load$b(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx);
    }
};

/*

Parser for .XKT Format V2

DEPRECATED

.XKT specifications: https://github.com/xeokit/xeokit-sdk/wiki/XKT-Format

 */

let pako$9 = window.pako || p;
if (!pako$9.inflate) {  // See https://github.com/nodeca/pako/issues/97
    pako$9 = pako$9.default;
}

function extract$9(elements) {
    return {

        positions: elements[0],
        normals: elements[1],
        indices: elements[2],
        edgeIndices: elements[3],

        meshPositions: elements[4],
        meshIndices: elements[5],
        meshEdgesIndices: elements[6],
        meshColors: elements[7],

        entityIDs: elements[8],
        entityMeshes: elements[9],
        entityIsObjects: elements[10],

        positionsDecodeMatrix: elements[11],

        entityMeshIds: elements[12],
        entityMatrices: elements[13],
        entityUsesInstancing: elements[14]
    };
}

function inflate$9(deflatedData) {
    return {
        positions: new Uint16Array(pako$9.inflate(deflatedData.positions).buffer),
        normals: new Int8Array(pako$9.inflate(deflatedData.normals).buffer),
        indices: new Uint32Array(pako$9.inflate(deflatedData.indices).buffer),
        edgeIndices: new Uint32Array(pako$9.inflate(deflatedData.edgeIndices).buffer),

        meshPositions: new Uint32Array(pako$9.inflate(deflatedData.meshPositions).buffer),
        meshIndices: new Uint32Array(pako$9.inflate(deflatedData.meshIndices).buffer),
        meshEdgesIndices: new Uint32Array(pako$9.inflate(deflatedData.meshEdgesIndices).buffer),
        meshColors: new Uint8Array(pako$9.inflate(deflatedData.meshColors).buffer),

        entityIDs: pako$9.inflate(deflatedData.entityIDs, {to: 'string'}),
        entityMeshes: new Uint32Array(pako$9.inflate(deflatedData.entityMeshes).buffer),
        entityIsObjects: new Uint8Array(pako$9.inflate(deflatedData.entityIsObjects).buffer),

        positionsDecodeMatrix: new Float32Array(pako$9.inflate(deflatedData.positionsDecodeMatrix).buffer),

        entityMeshIds: new Uint32Array(pako$9.inflate(deflatedData.entityMeshIds).buffer),
        entityMatrices: new Float32Array(pako$9.inflate(deflatedData.entityMatrices).buffer),
        entityUsesInstancing: new Uint8Array(pako$9.inflate(deflatedData.entityUsesInstancing).buffer)
    };
}

const decompressColor$a = (function () {
    const color2 = new Float32Array(3);
    return function (color) {
        color2[0] = color[0] / 255.0;
        color2[1] = color[1] / 255.0;
        color2[2] = color[2] / 255.0;
        return color2;
    };
})();

function load$a(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx) {

    const modelPartId = manifestCtx.getNextId();

    sceneModel.positionsCompression = "precompressed";
    sceneModel.normalsCompression = "precompressed";

    const positions = inflatedData.positions;
    const normals = inflatedData.normals;
    const indices = inflatedData.indices;
    const edgeIndices = inflatedData.edgeIndices;
    const meshPositions = inflatedData.meshPositions;
    const meshIndices = inflatedData.meshIndices;
    const meshEdgesIndices = inflatedData.meshEdgesIndices;
    const meshColors = inflatedData.meshColors;
    const entityIDs = JSON.parse(inflatedData.entityIDs);
    const entityMeshes = inflatedData.entityMeshes;
    const entityIsObjects = inflatedData.entityIsObjects;
    const entityMeshIds = inflatedData.entityMeshIds;
    const entityMatrices = inflatedData.entityMatrices;
    const entityUsesInstancing = inflatedData.entityUsesInstancing;

    const numMeshes = meshPositions.length;
    const numEntities = entityMeshes.length;

    const alreadyCreatedGeometries = {};

    for (let i = 0; i < numEntities; i++) {

        const xktEntityId = entityIDs [i];
        const entityId = options.globalizeObjectIds ? math.globalizeObjectId(sceneModel.id, xktEntityId) : xktEntityId;
        const metaObject = viewer.metaScene.metaObjects[entityId];
        const entityDefaults = {};
        const meshDefaults = {};
        const entityMatrix = entityMatrices.subarray((i * 16), (i * 16) + 16);

        if (metaObject) {
            if (options.excludeTypesMap && metaObject.type && options.excludeTypesMap[metaObject.type]) {
                continue;
            }
            if (options.includeTypesMap && metaObject.type && (!options.includeTypesMap[metaObject.type])) {
                continue;
            }
            const props = options.objectDefaults ? options.objectDefaults[metaObject.type] || options.objectDefaults["DEFAULT"] : null;
            if (props) {
                if (props.visible === false) {
                    entityDefaults.visible = false;
                }
                if (props.pickable === false) {
                    entityDefaults.pickable = false;
                }
                if (props.colorize) {
                    meshDefaults.color = props.colorize;
                }
                if (props.opacity !== undefined && props.opacity !== null) {
                    meshDefaults.opacity = props.opacity;
                }
            }
        } else {
            if (options.excludeUnclassifiedObjects) {
                continue;
            }
        }

        const lastEntity = (i === numEntities - 1);

        const meshIds = [];

        for (let j = entityMeshes [i], jlen = lastEntity ? entityMeshIds.length : entityMeshes [i + 1]; j < jlen; j++) {

            const jj = entityMeshIds [j];

            const lastMesh = (jj === (numMeshes - 1));
            const meshId = manifestCtx.getNextId();

            const color = decompressColor$a(meshColors.subarray((jj * 4), (jj * 4) + 3));
            const opacity = meshColors[(jj * 4) + 3] / 255.0;

            const tmpPositions = positions.subarray(meshPositions [jj], lastMesh ? positions.length : meshPositions [jj + 1]);
            const tmpNormals = normals.subarray(meshPositions [jj], lastMesh ? positions.length : meshPositions [jj + 1]);
            const tmpIndices = indices.subarray(meshIndices [jj], lastMesh ? indices.length : meshIndices [jj + 1]);
            const tmpEdgeIndices = edgeIndices.subarray(meshEdgesIndices [jj], lastMesh ? edgeIndices.length : meshEdgesIndices [jj + 1]);

            if (entityUsesInstancing [i] === 1) {

                const geometryId = `${modelPartId}.geometry.${meshId}.${jj}`;

                if (!(geometryId in alreadyCreatedGeometries)) {

                    sceneModel.createGeometry({
                        id: geometryId,
                        positionsCompressed: tmpPositions,
                        normalsCompressed: tmpNormals,
                        indices: tmpIndices,
                        edgeIndices: tmpEdgeIndices,
                        primitive: "triangles",
                        positionsDecodeMatrix: inflatedData.positionsDecodeMatrix,
                    });

                    alreadyCreatedGeometries [geometryId] = true;
                }

                sceneModel.createMesh(utils.apply(meshDefaults, {
                    id: meshId,
                    color: color,
                    opacity: opacity,
                    matrix: entityMatrix,
                    geometryId,
                }));

                meshIds.push(meshId);

            } else {

                sceneModel.createMesh(utils.apply(meshDefaults, {
                    id: meshId,
                    primitive: "triangles",
                    positionsCompressed: tmpPositions,
                    normalsCompressed: tmpNormals,
                    indices: tmpIndices,
                    edgeIndices: tmpEdgeIndices,
                    positionsDecodeMatrix: inflatedData.positionsDecodeMatrix,
                    color: color,
                    opacity: opacity
                }));

                meshIds.push(meshId);
            }
        }

        if (meshIds.length) {

            sceneModel.createEntity(utils.apply(entityDefaults, {
                id: entityId,
                isObject: (entityIsObjects [i] === 1),
                meshIds: meshIds
            }));
        }
    }
}

/** @private */
const ParserV2 = {
    version: 2,
    parse: function (viewer, options, elements, sceneModel, metaModel, manifestCtx) {
        const deflatedData = extract$9(elements);
        const inflatedData = inflate$9(deflatedData);
        load$a(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx);
    }
};

/*

Parser for .XKT Format V3

.XKT specifications: https://github.com/xeokit/xeokit-sdk/wiki/XKT-Format

 */

let pako$8 = window.pako || p;
if (!pako$8.inflate) {  // See https://github.com/nodeca/pako/issues/97
    pako$8 = pako$8.default;
}

function extract$8(elements) {
    return {
        positions: elements[0],
        normals: elements[1],
        indices: elements[2],
        edgeIndices: elements[3],
        meshPositions: elements[4],
        meshIndices: elements[5],
        meshEdgesIndices: elements[6],
        meshColors: elements[7],
        entityIDs: elements[8],
        entityMeshes: elements[9],
        entityIsObjects: elements[10],
        instancedPositionsDecodeMatrix: elements[11],
        batchedPositionsDecodeMatrix: elements[12],
        entityMeshIds: elements[13],
        entityMatrices: elements[14],
        entityUsesInstancing: elements[15]
    };
}

function inflate$8(deflatedData) {
    return {
        positions: new Uint16Array(pako$8.inflate(deflatedData.positions).buffer),
        normals: new Int8Array(pako$8.inflate(deflatedData.normals).buffer),
        indices: new Uint32Array(pako$8.inflate(deflatedData.indices).buffer),
        edgeIndices: new Uint32Array(pako$8.inflate(deflatedData.edgeIndices).buffer),
        meshPositions: new Uint32Array(pako$8.inflate(deflatedData.meshPositions).buffer),
        meshIndices: new Uint32Array(pako$8.inflate(deflatedData.meshIndices).buffer),
        meshEdgesIndices: new Uint32Array(pako$8.inflate(deflatedData.meshEdgesIndices).buffer),
        meshColors: new Uint8Array(pako$8.inflate(deflatedData.meshColors).buffer),
        entityIDs: pako$8.inflate(deflatedData.entityIDs, {to: 'string'}),
        entityMeshes: new Uint32Array(pako$8.inflate(deflatedData.entityMeshes).buffer),
        entityIsObjects: new Uint8Array(pako$8.inflate(deflatedData.entityIsObjects).buffer),
        instancedPositionsDecodeMatrix: new Float32Array(pako$8.inflate(deflatedData.instancedPositionsDecodeMatrix).buffer),
        batchedPositionsDecodeMatrix: new Float32Array(pako$8.inflate(deflatedData.batchedPositionsDecodeMatrix).buffer),
        entityMeshIds: new Uint32Array(pako$8.inflate(deflatedData.entityMeshIds).buffer),
        entityMatrices: new Float32Array(pako$8.inflate(deflatedData.entityMatrices).buffer),
        entityUsesInstancing: new Uint8Array(pako$8.inflate(deflatedData.entityUsesInstancing).buffer)
    };
}

const decompressColor$9 = (function () {
    const color2 = new Float32Array(3);
    return function (color) {
        color2[0] = color[0] / 255.0;
        color2[1] = color[1] / 255.0;
        color2[2] = color[2] / 255.0;
        return color2;
    };
})();

function load$9(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx) {

    const modelPartId = manifestCtx.getNextId();

    sceneModel.positionsCompression = "precompressed";
    sceneModel.normalsCompression = "precompressed";

    const positions = inflatedData.positions;
    const normals = inflatedData.normals;
    const indices = inflatedData.indices;
    const edgeIndices = inflatedData.edgeIndices;
    const meshPositions = inflatedData.meshPositions;
    const meshIndices = inflatedData.meshIndices;
    const meshEdgesIndices = inflatedData.meshEdgesIndices;
    const meshColors = inflatedData.meshColors;
    const entityIDs = JSON.parse(inflatedData.entityIDs);
    const entityMeshes = inflatedData.entityMeshes;
    const entityIsObjects = inflatedData.entityIsObjects;
    const entityMeshIds = inflatedData.entityMeshIds;
    const entityMatrices = inflatedData.entityMatrices;
    const entityUsesInstancing = inflatedData.entityUsesInstancing;

    const numMeshes = meshPositions.length;
    const numEntities = entityMeshes.length;

    const _alreadyCreatedGeometries = {};

    for (let i = 0; i < numEntities; i++) {

        const xktEntityId = entityIDs [i];
        const entityId = options.globalizeObjectIds ? math.globalizeObjectId(sceneModel.id, xktEntityId) : xktEntityId;
        const metaObject = viewer.metaScene.metaObjects[entityId];
        const entityDefaults = {};
        const meshDefaults = {};
        const entityMatrix = entityMatrices.subarray((i * 16), (i * 16) + 16);

        if (metaObject) {

            if (options.excludeTypesMap && metaObject.type && options.excludeTypesMap[metaObject.type]) {
                continue;
            }

            if (options.includeTypesMap && metaObject.type && (!options.includeTypesMap[metaObject.type])) {
                continue;
            }

            const props = options.objectDefaults ? options.objectDefaults[metaObject.type] || options.objectDefaults["DEFAULT"] : null;

            if (props) {
                if (props.visible === false) {
                    entityDefaults.visible = false;
                }
                if (props.pickable === false) {
                    entityDefaults.pickable = false;
                }
                if (props.colorize) {
                    meshDefaults.color = props.colorize;
                }
                if (props.opacity !== undefined && props.opacity !== null) {
                    meshDefaults.opacity = props.opacity;
                }
            }
        } else {
            if (options.excludeUnclassifiedObjects) {
                continue;
            }
        }

        const lastEntity = (i === numEntities - 1);

        const meshIds = [];

        for (let j = entityMeshes [i], jlen = lastEntity ? entityMeshIds.length : entityMeshes [i + 1]; j < jlen; j++) {
            var jj = entityMeshIds [j];

            const lastMesh = (jj === (numMeshes - 1));
            const meshId = `${modelPartId}.${entityId}.mesh.${jj}`;

            const color = decompressColor$9(meshColors.subarray((jj * 4), (jj * 4) + 3));
            const opacity = meshColors[(jj * 4) + 3] / 255.0;

            var tmpPositions = positions.subarray(meshPositions [jj], lastMesh ? positions.length : meshPositions [jj + 1]);
            var tmpNormals = normals.subarray(meshPositions [jj], lastMesh ? positions.length : meshPositions [jj + 1]);
            var tmpIndices = indices.subarray(meshIndices [jj], lastMesh ? indices.length : meshIndices [jj + 1]);
            var tmpEdgeIndices = edgeIndices.subarray(meshEdgesIndices [jj], lastMesh ? edgeIndices.length : meshEdgesIndices [jj + 1]);

            if (entityUsesInstancing [i] === 1) {

                const geometryId = `${modelPartId}.geometry.${meshId}.${jj}`;

                if (!(geometryId in _alreadyCreatedGeometries)) {

                    sceneModel.createGeometry({
                        id: geometryId,
                        positionsCompressed: tmpPositions,
                        normalsCompressed: tmpNormals,
                        indices: tmpIndices,
                        edgeIndices: tmpEdgeIndices,
                        primitive: "triangles",
                        positionsDecodeMatrix: inflatedData.instancedPositionsDecodeMatrix
                    });

                    _alreadyCreatedGeometries [geometryId] = true;
                }

                sceneModel.createMesh(utils.apply(meshDefaults, {
                    id: meshId,
                    color: color,
                    opacity: opacity,
                    matrix: entityMatrix,
                    geometryId,
                }));

                meshIds.push(meshId);

            } else {

                sceneModel.createMesh(utils.apply(meshDefaults, {
                    id: meshId,
                    primitive: "triangles",
                    positionsCompressed: tmpPositions,
                    normalsCompressed: tmpNormals,
                    indices: tmpIndices,
                    edgeIndices: tmpEdgeIndices,
                    positionsDecodeMatrix: inflatedData.batchedPositionsDecodeMatrix,
                    color: color,
                    opacity: opacity
                }));

                meshIds.push(meshId);
            }
        }

        if (meshIds.length) {
            sceneModel.createEntity(utils.apply(entityDefaults, {
                id: entityId,
                isObject: (entityIsObjects [i] === 1),
                meshIds: meshIds
            }));
        }
    }
}

/** @private */
const ParserV3 = {
    version: 3,
    parse: function (viewer, options, elements, sceneModel, metaModel, manifestCtx) {
        const deflatedData = extract$8(elements);
        const inflatedData = inflate$8(deflatedData);
        load$9(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx);
    }
};

/*

Parser for .XKT Format V4

.XKT specifications: https://github.com/xeokit/xeokit-sdk/wiki/XKT-Format

 */

let pako$7 = window.pako || p;
if (!pako$7.inflate) {  // See https://github.com/nodeca/pako/issues/97
    pako$7 = pako$7.default;
}

function extract$7(elements) {
    return {
        positions: elements[0],
        normals: elements[1],
        indices: elements[2],
        edgeIndices: elements[3],
        decodeMatrices: elements[4],
        matrices: elements[5],
        eachPrimitivePositionsAndNormalsPortion: elements[6],
        eachPrimitiveIndicesPortion: elements[7],
        eachPrimitiveEdgeIndicesPortion: elements[8],
        eachPrimitiveDecodeMatricesPortion: elements[9],
        eachPrimitiveColor: elements[10],
        primitiveInstances: elements[11],
        eachEntityId: elements[12],
        eachEntityPrimitiveInstancesPortion: elements[13],
        eachEntityMatricesPortion: elements[14],
        eachEntityMatrix: elements[15]
    };
}

function inflate$7(deflatedData) {
    return {
        positions: new Uint16Array(pako$7.inflate(deflatedData.positions).buffer),
        normals: new Int8Array(pako$7.inflate(deflatedData.normals).buffer),
        indices: new Uint32Array(pako$7.inflate(deflatedData.indices).buffer),
        edgeIndices: new Uint32Array(pako$7.inflate(deflatedData.edgeIndices).buffer),
        decodeMatrices: new Float32Array(pako$7.inflate(deflatedData.decodeMatrices).buffer),
        matrices: new Float32Array(pako$7.inflate(deflatedData.matrices).buffer),
        eachPrimitivePositionsAndNormalsPortion: new Uint32Array(pako$7.inflate(deflatedData.eachPrimitivePositionsAndNormalsPortion).buffer),
        eachPrimitiveIndicesPortion: new Uint32Array(pako$7.inflate(deflatedData.eachPrimitiveIndicesPortion).buffer),
        eachPrimitiveEdgeIndicesPortion: new Uint32Array(pako$7.inflate(deflatedData.eachPrimitiveEdgeIndicesPortion).buffer),
        eachPrimitiveDecodeMatricesPortion: new Uint32Array(pako$7.inflate(deflatedData.eachPrimitiveDecodeMatricesPortion).buffer),
        eachPrimitiveColor: new Uint8Array(pako$7.inflate(deflatedData.eachPrimitiveColor).buffer),
        primitiveInstances: new Uint32Array(pako$7.inflate(deflatedData.primitiveInstances).buffer),
        eachEntityId: pako$7.inflate(deflatedData.eachEntityId, {to: 'string'}),
        eachEntityPrimitiveInstancesPortion: new Uint32Array(pako$7.inflate(deflatedData.eachEntityPrimitiveInstancesPortion).buffer),
        eachEntityMatricesPortion: new Uint32Array(pako$7.inflate(deflatedData.eachEntityMatricesPortion).buffer)
    };
}

const decompressColor$8 = (function () {
    const color2 = new Float32Array(3);
    return function (color) {
        color2[0] = color[0] / 255.0;
        color2[1] = color[1] / 255.0;
        color2[2] = color[2] / 255.0;
        return color2;
    };
})();

function load$8(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx) {

    const modelPartId = manifestCtx.getNextId();

    sceneModel.positionsCompression = "precompressed";
    sceneModel.normalsCompression = "precompressed";

    const positions = inflatedData.positions;
    const normals = inflatedData.normals;
    const indices = inflatedData.indices;
    const edgeIndices = inflatedData.edgeIndices;
    const decodeMatrices = inflatedData.decodeMatrices;
    const matrices = inflatedData.matrices;

    const eachPrimitivePositionsAndNormalsPortion = inflatedData.eachPrimitivePositionsAndNormalsPortion;
    const eachPrimitiveIndicesPortion = inflatedData.eachPrimitiveIndicesPortion;
    const eachPrimitiveEdgeIndicesPortion = inflatedData.eachPrimitiveEdgeIndicesPortion;
    const eachPrimitiveDecodeMatricesPortion = inflatedData.eachPrimitiveDecodeMatricesPortion;
    const eachPrimitiveColor = inflatedData.eachPrimitiveColor;

    const primitiveInstances = inflatedData.primitiveInstances;

    const eachEntityId = JSON.parse(inflatedData.eachEntityId);
    const eachEntityPrimitiveInstancesPortion = inflatedData.eachEntityPrimitiveInstancesPortion;
    const eachEntityMatricesPortion = inflatedData.eachEntityMatricesPortion;

    const numPrimitives = eachPrimitivePositionsAndNormalsPortion.length;
    const numPrimitiveInstances = primitiveInstances.length;
    const primitiveInstanceCounts = new Uint8Array(numPrimitives); // For each mesh, how many times it is instanced
    const orderedPrimitiveIndexes = new Uint32Array(numPrimitives); // For each mesh, its index sorted into runs that share the same decode matrix

    const numEntities = eachEntityId.length;

    // Get lookup that orders primitives into runs that share the same decode matrices;
    // this is used to create meshes in batches that use the same decode matrix

    for (let primitiveIndex = 0; primitiveIndex < numPrimitives; primitiveIndex++) {
        orderedPrimitiveIndexes[primitiveIndex] = primitiveIndex;
    }

    orderedPrimitiveIndexes.sort((i1, i2) => {
        if (eachPrimitiveDecodeMatricesPortion[i1] < eachPrimitiveDecodeMatricesPortion[i2]) {
            return -1;
        }
        if (eachPrimitiveDecodeMatricesPortion[i1] > eachPrimitiveDecodeMatricesPortion[i2]) {
            return 1;
        }
        return 0;
    });

    // Count instances of each primitive

    for (let primitiveInstanceIndex = 0; primitiveInstanceIndex < numPrimitiveInstances; primitiveInstanceIndex++) {
        const primitiveIndex = primitiveInstances[primitiveInstanceIndex];
        primitiveInstanceCounts[primitiveIndex]++;
    }

    // Map batched primitives to the entities that will use them

    const batchedPrimitiveEntityIndexes = {};

    for (let entityIndex = 0; entityIndex < numEntities; entityIndex++) {

        const lastEntityIndex = (numEntities - 1);
        const atLastEntity = (entityIndex === lastEntityIndex);
        const firstEntityPrimitiveInstanceIndex = eachEntityPrimitiveInstancesPortion [entityIndex];
        const lastEntityPrimitiveInstanceIndex = atLastEntity ? eachEntityPrimitiveInstancesPortion[lastEntityIndex] : eachEntityPrimitiveInstancesPortion[entityIndex + 1];

        for (let primitiveInstancesIndex = firstEntityPrimitiveInstanceIndex; primitiveInstancesIndex < lastEntityPrimitiveInstanceIndex; primitiveInstancesIndex++) {

            const primitiveIndex = primitiveInstances[primitiveInstancesIndex];
            const primitiveInstanceCount = primitiveInstanceCounts[primitiveIndex];
            const isInstancedPrimitive = (primitiveInstanceCount > 1);

            if (!isInstancedPrimitive) {
                batchedPrimitiveEntityIndexes[primitiveIndex] = entityIndex;
            }
        }
    }

    // Create 1) geometries for instanced primitives, and 2) meshes for batched primitives.  We create all the
    // batched meshes now, before we create entities, because we're creating the batched meshes in runs that share
    // the same decode matrices. Each run of meshes with the same decode matrix will end up in the same
    // BatchingLayer; the SceneModel#createMesh() method starts a new BatchingLayer each time the decode
    // matrix has changed since the last invocation of that method, hence why we need to order batched meshes
    // in runs like this.

    for (let primitiveIndex = 0; primitiveIndex < numPrimitives; primitiveIndex++) {

        const orderedPrimitiveIndex = orderedPrimitiveIndexes[primitiveIndex];

        const atLastPrimitive = (orderedPrimitiveIndex === (numPrimitives - 1));

        const primitiveInstanceCount = primitiveInstanceCounts[orderedPrimitiveIndex];
        const isInstancedPrimitive = (primitiveInstanceCount > 1);

        const color = decompressColor$8(eachPrimitiveColor.subarray((orderedPrimitiveIndex * 4), (orderedPrimitiveIndex * 4) + 3));
        const opacity = eachPrimitiveColor[(orderedPrimitiveIndex * 4) + 3] / 255.0;

        const primitivePositions = positions.subarray(eachPrimitivePositionsAndNormalsPortion [orderedPrimitiveIndex], atLastPrimitive ? positions.length : eachPrimitivePositionsAndNormalsPortion [orderedPrimitiveIndex + 1]);
        const primitiveNormals = normals.subarray(eachPrimitivePositionsAndNormalsPortion [orderedPrimitiveIndex], atLastPrimitive ? normals.length : eachPrimitivePositionsAndNormalsPortion [orderedPrimitiveIndex + 1]);
        const primitiveIndices = indices.subarray(eachPrimitiveIndicesPortion [orderedPrimitiveIndex], atLastPrimitive ? indices.length : eachPrimitiveIndicesPortion [orderedPrimitiveIndex + 1]);
        const primitiveEdgeIndices = edgeIndices.subarray(eachPrimitiveEdgeIndicesPortion [orderedPrimitiveIndex], atLastPrimitive ? edgeIndices.length : eachPrimitiveEdgeIndicesPortion [orderedPrimitiveIndex + 1]);
        const primitiveDecodeMatrix = decodeMatrices.subarray(eachPrimitiveDecodeMatricesPortion [orderedPrimitiveIndex], eachPrimitiveDecodeMatricesPortion [orderedPrimitiveIndex] + 16);

        if (isInstancedPrimitive) {

            // Primitive instanced by more than one entity, and has positions in Model-space

           const geometryId = `${modelPartId}-geometry.${orderedPrimitiveIndex}`; // These IDs are local to the SceneModel

            sceneModel.createGeometry({
                id: geometryId,
                primitive: "triangles",
                positionsCompressed: primitivePositions,
                normalsCompressed: primitiveNormals,
                indices: primitiveIndices,
                edgeIndices: primitiveEdgeIndices,
                positionsDecodeMatrix: primitiveDecodeMatrix
            });

        } else {

            // Primitive is used only by one entity, and has positions pre-transformed into World-space

            const meshId = `${modelPartId}-${orderedPrimitiveIndex}`;

            const entityIndex = batchedPrimitiveEntityIndexes[orderedPrimitiveIndex];
            eachEntityId[entityIndex];

            const meshDefaults = {}; // TODO: get from lookup from entity IDs

            sceneModel.createMesh(utils.apply(meshDefaults, {
                id: meshId,
                primitive: "triangles",
                positionsCompressed: primitivePositions,
                normalsCompressed: primitiveNormals,
                indices: primitiveIndices,
                edgeIndices: primitiveEdgeIndices,
                positionsDecodeMatrix: primitiveDecodeMatrix,
                color: color,
                opacity: opacity
            }));
        }
    }

    let countInstances = 0;

    for (let entityIndex = 0; entityIndex < numEntities; entityIndex++) {

        const lastEntityIndex = (numEntities - 1);
        const atLastEntity = (entityIndex === lastEntityIndex);
        const entityId = eachEntityId[entityIndex];
        const firstEntityPrimitiveInstanceIndex = eachEntityPrimitiveInstancesPortion [entityIndex];
        const lastEntityPrimitiveInstanceIndex = atLastEntity ? eachEntityPrimitiveInstancesPortion[lastEntityIndex] : eachEntityPrimitiveInstancesPortion[entityIndex + 1];

        const meshIds = [];

        for (let primitiveInstancesIndex = firstEntityPrimitiveInstanceIndex; primitiveInstancesIndex < lastEntityPrimitiveInstanceIndex; primitiveInstancesIndex++) {

            const primitiveIndex = primitiveInstances[primitiveInstancesIndex];
            const primitiveInstanceCount = primitiveInstanceCounts[primitiveIndex];
            const isInstancedPrimitive = (primitiveInstanceCount > 1);

            if (isInstancedPrimitive) {

                const meshDefaults = {}; // TODO: get from lookup from entity IDs

                const meshId = `${modelPartId}-instance.${countInstances++}`;
                const geometryId = `${modelPartId}-geometry.${primitiveIndex}`; // These IDs are local to the SceneModel

                const matricesIndex = (eachEntityMatricesPortion [entityIndex]) * 16;
                const matrix = matrices.subarray(matricesIndex, matricesIndex + 16);

                sceneModel.createMesh(utils.apply(meshDefaults, {
                    id: meshId,
                    geometryId: geometryId,
                    matrix: matrix
                }));

                meshIds.push(meshId);

            } else {
                meshIds.push(primitiveIndex);
            }
        }

        if (meshIds.length > 0) {

            const entityDefaults = {}; // TODO: get from lookup from entity IDs

            sceneModel.createEntity(utils.apply(entityDefaults, {
                id: entityId,
                isObject: true, ///////////////// TODO: If metaobject exists
                meshIds: meshIds
            }));
        }
    }
}

/** @private */
const ParserV4 = {
    version: 4,
    parse: function (viewer, options, elements, sceneModel, metaModel, manifestCtx) {
        const deflatedData = extract$7(elements);
        const inflatedData = inflate$7(deflatedData);
        load$8(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx);
    }
};

/*

 Parser for .XKT Format V5

.XKT specifications: https://github.com/xeokit/xeokit-sdk/wiki/XKT-Format

 */

let pako$6 = window.pako || p;
if (!pako$6.inflate) {  // See https://github.com/nodeca/pako/issues/97
    pako$6 = pako$6.default;
}

function extract$6(elements) {
    return {
        positions: elements[0],
        normals: elements[1],
        indices: elements[2],
        edgeIndices: elements[3],
        matrices: elements[4],
        eachPrimitivePositionsAndNormalsPortion: elements[5],
        eachPrimitiveIndicesPortion: elements[6],
        eachPrimitiveEdgeIndicesPortion: elements[7],
        eachPrimitiveColor: elements[8],
        primitiveInstances: elements[9],
        eachEntityId: elements[10],
        eachEntityPrimitiveInstancesPortion: elements[11],
        eachEntityMatricesPortion: elements[12]
    };
}

function inflate$6(deflatedData) {
    return {
        positions: new Float32Array(pako$6.inflate(deflatedData.positions).buffer),
        normals: new Int8Array(pako$6.inflate(deflatedData.normals).buffer),
        indices: new Uint32Array(pako$6.inflate(deflatedData.indices).buffer),
        edgeIndices: new Uint32Array(pako$6.inflate(deflatedData.edgeIndices).buffer),
        matrices: new Float32Array(pako$6.inflate(deflatedData.matrices).buffer),
        eachPrimitivePositionsAndNormalsPortion: new Uint32Array(pako$6.inflate(deflatedData.eachPrimitivePositionsAndNormalsPortion).buffer),
        eachPrimitiveIndicesPortion: new Uint32Array(pako$6.inflate(deflatedData.eachPrimitiveIndicesPortion).buffer),
        eachPrimitiveEdgeIndicesPortion: new Uint32Array(pako$6.inflate(deflatedData.eachPrimitiveEdgeIndicesPortion).buffer),
        eachPrimitiveColor: new Uint8Array(pako$6.inflate(deflatedData.eachPrimitiveColor).buffer),
        primitiveInstances: new Uint32Array(pako$6.inflate(deflatedData.primitiveInstances).buffer),
        eachEntityId: pako$6.inflate(deflatedData.eachEntityId, {to: 'string'}),
        eachEntityPrimitiveInstancesPortion: new Uint32Array(pako$6.inflate(deflatedData.eachEntityPrimitiveInstancesPortion).buffer),
        eachEntityMatricesPortion: new Uint32Array(pako$6.inflate(deflatedData.eachEntityMatricesPortion).buffer)
    };
}

const decompressColor$7 = (function () {
    const color2 = new Float32Array(3);
    return function (color) {
        color2[0] = color[0] / 255.0;
        color2[1] = color[1] / 255.0;
        color2[2] = color[2] / 255.0;
        return color2;
    };
})();

function load$7(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx) {

    const modelPartId = manifestCtx.getNextId();

    sceneModel.positionsCompression = "disabled"; // Positions in XKT V4 are floats, which we never quantize, for precision with big models
    sceneModel.normalsCompression = "precompressed"; // Normals are oct-encoded though

    const positions = inflatedData.positions;
    const normals = inflatedData.normals;
    const indices = inflatedData.indices;
    const edgeIndices = inflatedData.edgeIndices;
    const matrices = inflatedData.matrices;

    const eachPrimitivePositionsAndNormalsPortion = inflatedData.eachPrimitivePositionsAndNormalsPortion;
    const eachPrimitiveIndicesPortion = inflatedData.eachPrimitiveIndicesPortion;
    const eachPrimitiveEdgeIndicesPortion = inflatedData.eachPrimitiveEdgeIndicesPortion;
    const eachPrimitiveColor = inflatedData.eachPrimitiveColor;

    const primitiveInstances = inflatedData.primitiveInstances;

    const eachEntityId = JSON.parse(inflatedData.eachEntityId);
    const eachEntityPrimitiveInstancesPortion = inflatedData.eachEntityPrimitiveInstancesPortion;
    const eachEntityMatricesPortion = inflatedData.eachEntityMatricesPortion;

    const numPrimitives = eachPrimitivePositionsAndNormalsPortion.length;
    const numPrimitiveInstances = primitiveInstances.length;
    const primitiveInstanceCounts = new Uint8Array(numPrimitives); // For each mesh, how many times it is instanced

    const numEntities = eachEntityId.length;

    // Count instances of each primitive

    for (let primitiveInstanceIndex = 0; primitiveInstanceIndex < numPrimitiveInstances; primitiveInstanceIndex++) {
        const primitiveIndex = primitiveInstances[primitiveInstanceIndex];
        primitiveInstanceCounts[primitiveIndex]++;
    }

    // Map batched primitives to the entities that will use them

    const batchedPrimitiveEntityIndexes = {};

    for (let entityIndex = 0; entityIndex < numEntities; entityIndex++) {

        const lastEntityIndex = (numEntities - 1);
        const atLastEntity = (entityIndex === lastEntityIndex);
        const firstEntityPrimitiveInstanceIndex = eachEntityPrimitiveInstancesPortion [entityIndex];
        const lastEntityPrimitiveInstanceIndex = atLastEntity ? eachEntityPrimitiveInstancesPortion[lastEntityIndex] : eachEntityPrimitiveInstancesPortion[entityIndex + 1];

        for (let primitiveInstancesIndex = firstEntityPrimitiveInstanceIndex; primitiveInstancesIndex < lastEntityPrimitiveInstanceIndex; primitiveInstancesIndex++) {

            const primitiveIndex = primitiveInstances[primitiveInstancesIndex];
            const primitiveInstanceCount = primitiveInstanceCounts[primitiveIndex];
            const isInstancedPrimitive = (primitiveInstanceCount > 1);

            if (!isInstancedPrimitive) {
                batchedPrimitiveEntityIndexes[primitiveIndex] = entityIndex;
            }
        }
    }

    // Create geometries for instanced primitives and meshes for batched primitives.

    for (let primitiveIndex = 0; primitiveIndex < numPrimitives; primitiveIndex++) {

        const atLastPrimitive = (primitiveIndex === (numPrimitives - 1));

        const primitiveInstanceCount = primitiveInstanceCounts[primitiveIndex];
        const isInstancedPrimitive = (primitiveInstanceCount > 1);

        const color = decompressColor$7(eachPrimitiveColor.subarray((primitiveIndex * 4), (primitiveIndex * 4) + 3));
        const opacity = eachPrimitiveColor[(primitiveIndex * 4) + 3] / 255.0;

        const primitivePositions = positions.subarray(eachPrimitivePositionsAndNormalsPortion [primitiveIndex], atLastPrimitive ? positions.length : eachPrimitivePositionsAndNormalsPortion [primitiveIndex + 1]);
        const primitiveNormals = normals.subarray(eachPrimitivePositionsAndNormalsPortion [primitiveIndex], atLastPrimitive ? normals.length : eachPrimitivePositionsAndNormalsPortion [primitiveIndex + 1]);
        const primitiveIndices = indices.subarray(eachPrimitiveIndicesPortion [primitiveIndex], atLastPrimitive ? indices.length : eachPrimitiveIndicesPortion [primitiveIndex + 1]);
        const primitiveEdgeIndices = edgeIndices.subarray(eachPrimitiveEdgeIndicesPortion [primitiveIndex], atLastPrimitive ? edgeIndices.length : eachPrimitiveEdgeIndicesPortion [primitiveIndex + 1]);

        if (isInstancedPrimitive) {

            // Primitive instanced by more than one entity, and has positions in Model-space

            const geometryId = `${modelPartId}-geometry.${primitiveIndex}`; // These IDs are local to the SceneModel

            sceneModel.createGeometry({
                id: geometryId,
                primitive: "triangles",
                positionsCompressed: primitivePositions,
                normalsCompressed: primitiveNormals,
                indices: primitiveIndices,
                edgeIndices: primitiveEdgeIndices
            });

        } else {

            // Primitive is used only by one entity, and has positions pre-transformed into World-space

            const meshId = primitiveIndex; // These IDs are local to the SceneModel

            const entityIndex = batchedPrimitiveEntityIndexes[primitiveIndex];
            eachEntityId[entityIndex];

            const meshDefaults = {}; // TODO: get from lookup from entity IDs

            sceneModel.createMesh(utils.apply(meshDefaults, {
                id: meshId,
                primitive: "triangles",
                positionsCompressed: primitivePositions,
                normalsCompressed: primitiveNormals,
                indices: primitiveIndices,
                edgeIndices: primitiveEdgeIndices,
                color: color,
                opacity: opacity
            }));
        }
    }

    let countInstances = 0;

    for (let entityIndex = 0; entityIndex < numEntities; entityIndex++) {

        const lastEntityIndex = (numEntities - 1);
        const atLastEntity = (entityIndex === lastEntityIndex);
        const entityId = eachEntityId[entityIndex];
        const firstEntityPrimitiveInstanceIndex = eachEntityPrimitiveInstancesPortion [entityIndex];
        const lastEntityPrimitiveInstanceIndex = atLastEntity ? eachEntityPrimitiveInstancesPortion[lastEntityIndex] : eachEntityPrimitiveInstancesPortion[entityIndex + 1];

        const meshIds = [];

        for (let primitiveInstancesIndex = firstEntityPrimitiveInstanceIndex; primitiveInstancesIndex < lastEntityPrimitiveInstanceIndex; primitiveInstancesIndex++) {

            const primitiveIndex = primitiveInstances[primitiveInstancesIndex];
            const primitiveInstanceCount = primitiveInstanceCounts[primitiveIndex];
            const isInstancedPrimitive = (primitiveInstanceCount > 1);

            if (isInstancedPrimitive) {

                const meshDefaults = {}; // TODO: get from lookup from entity IDs

                const meshId = "instance." + countInstances++;
                const geometryId = "geometry" + primitiveIndex;
                const matricesIndex = (eachEntityMatricesPortion [entityIndex]) * 16;
                const matrix = matrices.subarray(matricesIndex, matricesIndex + 16);

                sceneModel.createMesh(utils.apply(meshDefaults, {
                    id: meshId,
                    geometryId: geometryId,
                    matrix: matrix
                }));

                meshIds.push(meshId);

            } else {
                meshIds.push(primitiveIndex);
            }
        }

        if (meshIds.length > 0) {

            const entityDefaults = {}; // TODO: get from lookup from entity IDs

            sceneModel.createEntity(utils.apply(entityDefaults, {
                id: entityId,
                isObject: true, ///////////////// TODO: If metaobject exists
                meshIds: meshIds
            }));
        }
    }
}

/** @private */
const ParserV5 = {
    version: 5,
    parse: function (viewer, options, elements, sceneModel, metaModel, manifestCtx) {
        const deflatedData = extract$6(elements);
        const inflatedData = inflate$6(deflatedData);
        load$7(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx);
    }
};

/*

 Parser for .XKT Format V6

 */

let pako$5 = window.pako || p;
if (!pako$5.inflate) {  // See https://github.com/nodeca/pako/issues/97
    pako$5 = pako$5.default;
}

function extract$5(elements) {

    return {
        positions: elements[0],
        normals: elements[1],
        indices: elements[2],
        edgeIndices: elements[3],
        matrices: elements[4],
        reusedPrimitivesDecodeMatrix: elements[5],
        eachPrimitivePositionsAndNormalsPortion: elements[6],
        eachPrimitiveIndicesPortion: elements[7],
        eachPrimitiveEdgeIndicesPortion: elements[8],
        eachPrimitiveColorAndOpacity: elements[9],
        primitiveInstances: elements[10],
        eachEntityId: elements[11],
        eachEntityPrimitiveInstancesPortion: elements[12],
        eachEntityMatricesPortion: elements[13],
        eachTileAABB: elements[14],
        eachTileEntitiesPortion: elements[15]
    };
}

function inflate$5(deflatedData) {

    function inflate(array, options) {
        return (array.length === 0) ? [] : pako$5.inflate(array, options).buffer;
    }

    return {
        positions: new Uint16Array(inflate(deflatedData.positions)),
        normals: new Int8Array(inflate(deflatedData.normals)),
        indices: new Uint32Array(inflate(deflatedData.indices)),
        edgeIndices: new Uint32Array(inflate(deflatedData.edgeIndices)),
        matrices: new Float32Array(inflate(deflatedData.matrices)),
        reusedPrimitivesDecodeMatrix: new Float32Array(inflate(deflatedData.reusedPrimitivesDecodeMatrix)),
        eachPrimitivePositionsAndNormalsPortion: new Uint32Array(inflate(deflatedData.eachPrimitivePositionsAndNormalsPortion)),
        eachPrimitiveIndicesPortion: new Uint32Array(inflate(deflatedData.eachPrimitiveIndicesPortion)),
        eachPrimitiveEdgeIndicesPortion: new Uint32Array(inflate(deflatedData.eachPrimitiveEdgeIndicesPortion)),
        eachPrimitiveColorAndOpacity: new Uint8Array(inflate(deflatedData.eachPrimitiveColorAndOpacity)),
        primitiveInstances: new Uint32Array(inflate(deflatedData.primitiveInstances)),
        eachEntityId: pako$5.inflate(deflatedData.eachEntityId, {to: 'string'}),
        eachEntityPrimitiveInstancesPortion: new Uint32Array(inflate(deflatedData.eachEntityPrimitiveInstancesPortion)),
        eachEntityMatricesPortion: new Uint32Array(inflate(deflatedData.eachEntityMatricesPortion)),
        eachTileAABB: new Float64Array(inflate(deflatedData.eachTileAABB)),
        eachTileEntitiesPortion: new Uint32Array(inflate(deflatedData.eachTileEntitiesPortion))
    };
}

const decompressColor$6 = (function () {
    const floatColor = new Float32Array(3);
    return function (intColor) {
        floatColor[0] = intColor[0] / 255.0;
        floatColor[1] = intColor[1] / 255.0;
        floatColor[2] = intColor[2] / 255.0;
        return floatColor;
    };
})();

function load$6(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx) {

    const modelPartId = manifestCtx.getNextId();

    const positions = inflatedData.positions;
    const normals = inflatedData.normals;
    const indices = inflatedData.indices;
    const edgeIndices = inflatedData.edgeIndices;

    const matrices = inflatedData.matrices;

    const reusedPrimitivesDecodeMatrix = inflatedData.reusedPrimitivesDecodeMatrix;

    const eachPrimitivePositionsAndNormalsPortion = inflatedData.eachPrimitivePositionsAndNormalsPortion;
    const eachPrimitiveIndicesPortion = inflatedData.eachPrimitiveIndicesPortion;
    const eachPrimitiveEdgeIndicesPortion = inflatedData.eachPrimitiveEdgeIndicesPortion;
    const eachPrimitiveColorAndOpacity = inflatedData.eachPrimitiveColorAndOpacity;

    const primitiveInstances = inflatedData.primitiveInstances;

    const eachEntityId = JSON.parse(inflatedData.eachEntityId);
    const eachEntityPrimitiveInstancesPortion = inflatedData.eachEntityPrimitiveInstancesPortion;
    const eachEntityMatricesPortion = inflatedData.eachEntityMatricesPortion;

    const eachTileAABB = inflatedData.eachTileAABB;
    const eachTileEntitiesPortion = inflatedData.eachTileEntitiesPortion;

    const numPrimitives = eachPrimitivePositionsAndNormalsPortion.length;
    const numPrimitiveInstances = primitiveInstances.length;
    const numEntities = eachEntityId.length;
    const numTiles = eachTileEntitiesPortion.length;

    // Count instances of each primitive

    const primitiveReuseCounts = new Uint32Array(numPrimitives);

    for (let primitiveInstanceIndex = 0; primitiveInstanceIndex < numPrimitiveInstances; primitiveInstanceIndex++) {
        const primitiveIndex = primitiveInstances[primitiveInstanceIndex];
        if (primitiveReuseCounts[primitiveIndex] !== undefined) {
            primitiveReuseCounts[primitiveIndex]++;
        } else {
            primitiveReuseCounts[primitiveIndex] = 1;
        }
    }

    // Iterate over tiles

    const tileCenter = math.vec3();
    const rtcAABB = math.AABB3();

    for (let tileIndex = 0; tileIndex < numTiles; tileIndex++) {

        const lastTileIndex = (numTiles - 1);

        const atLastTile = (tileIndex === lastTileIndex);

        const firstTileEntityIndex = eachTileEntitiesPortion [tileIndex];
        const lastTileEntityIndex = atLastTile ? numEntities : eachTileEntitiesPortion[tileIndex + 1];

        const tileAABBIndex = tileIndex * 6;
        const tileAABB = eachTileAABB.subarray(tileAABBIndex, tileAABBIndex + 6);

        math.getAABB3Center(tileAABB, tileCenter);

        rtcAABB[0] = tileAABB[0] - tileCenter[0];
        rtcAABB[1] = tileAABB[1] - tileCenter[1];
        rtcAABB[2] = tileAABB[2] - tileCenter[2];
        rtcAABB[3] = tileAABB[3] - tileCenter[0];
        rtcAABB[4] = tileAABB[4] - tileCenter[1];
        rtcAABB[5] = tileAABB[5] - tileCenter[2];

        const tileDecodeMatrix = geometryCompressionUtils.createPositionsDecodeMatrix(rtcAABB);

        const geometryCreated = {};

        // Iterate over each tile's entities

        for (let tileEntityIndex = firstTileEntityIndex; tileEntityIndex < lastTileEntityIndex; tileEntityIndex++) {

            const xktEntityId = eachEntityId[tileEntityIndex];
            const entityId = options.globalizeObjectIds ? math.globalizeObjectId(sceneModel.id, xktEntityId) : xktEntityId;

            const entityMatrixIndex = eachEntityMatricesPortion[tileEntityIndex];
            const entityMatrix = matrices.slice(entityMatrixIndex, entityMatrixIndex + 16);

            const lastTileEntityIndex = (numEntities - 1);
            const atLastTileEntity = (tileEntityIndex === lastTileEntityIndex);
            const firstPrimitiveInstanceIndex = eachEntityPrimitiveInstancesPortion [tileEntityIndex];
            const lastPrimitiveInstanceIndex = atLastTileEntity ? primitiveInstances.length : eachEntityPrimitiveInstancesPortion[tileEntityIndex + 1];

            const meshIds = [];

            const metaObject = viewer.metaScene.metaObjects[entityId];
            const entityDefaults = {};
            const meshDefaults = {};

            if (metaObject) {

                // Mask loading of object types

                if (options.excludeTypesMap && metaObject.type && options.excludeTypesMap[metaObject.type]) {
                    continue;
                }

                if (options.includeTypesMap && metaObject.type && (!options.includeTypesMap[metaObject.type])) {
                    continue;
                }

                // Mask loading of object ids

                if (options.includeIdsMap && metaObject.id && (!options.includeIdsMap[metaObject.id])) {
                    continue;
                }

                // Get initial property values for object types

                const props = options.objectDefaults ? options.objectDefaults[metaObject.type] || options.objectDefaults["DEFAULT"] : null;

                if (props) {
                    if (props.visible === false) {
                        entityDefaults.visible = false;
                    }
                    if (props.pickable === false) {
                        entityDefaults.pickable = false;
                    }
                    if (props.colorize) {
                        meshDefaults.color = props.colorize;
                    }
                    if (props.opacity !== undefined && props.opacity !== null) {
                        meshDefaults.opacity = props.opacity;
                    }
                }

            } else {
                if (options.excludeUnclassifiedObjects) {
                    continue;
                }
            }

            // Iterate each entity's primitive instances

            for (let primitiveInstancesIndex = firstPrimitiveInstanceIndex; primitiveInstancesIndex < lastPrimitiveInstanceIndex; primitiveInstancesIndex++) {

                const primitiveIndex = primitiveInstances[primitiveInstancesIndex];
                const primitiveReuseCount = primitiveReuseCounts[primitiveIndex];
                const isReusedPrimitive = (primitiveReuseCount > 1);

                const atLastPrimitive = (primitiveIndex === (numPrimitives - 1));

                const primitivePositions = positions.subarray(eachPrimitivePositionsAndNormalsPortion [primitiveIndex], atLastPrimitive ? positions.length : eachPrimitivePositionsAndNormalsPortion [primitiveIndex + 1]);
                const primitiveNormals = normals.subarray(eachPrimitivePositionsAndNormalsPortion [primitiveIndex], atLastPrimitive ? normals.length : eachPrimitivePositionsAndNormalsPortion [primitiveIndex + 1]);
                const primitiveIndices = indices.subarray(eachPrimitiveIndicesPortion [primitiveIndex], atLastPrimitive ? indices.length : eachPrimitiveIndicesPortion [primitiveIndex + 1]);
                const primitiveEdgeIndices = edgeIndices.subarray(eachPrimitiveEdgeIndicesPortion [primitiveIndex], atLastPrimitive ? edgeIndices.length : eachPrimitiveEdgeIndicesPortion [primitiveIndex + 1]);

                const color = decompressColor$6(eachPrimitiveColorAndOpacity.subarray((primitiveIndex * 4), (primitiveIndex * 4) + 3));
                const opacity = eachPrimitiveColorAndOpacity[(primitiveIndex * 4) + 3] / 255.0;

                const meshId = manifestCtx.getNextId();

                if (isReusedPrimitive) {

                    // Create mesh for multi-use primitive - create (or reuse) geometry, create mesh using that geometry

                    const geometryId = `${modelPartId}-geometry.${tileIndex}.${primitiveIndex}`; // These IDs are local to the SceneModel

                    if (!geometryCreated[geometryId]) {

                        sceneModel.createGeometry({
                            id: geometryId,
                            primitive: "triangles",
                            positionsCompressed: primitivePositions,
                         //   normalsCompressed: primitiveNormals,
                            indices: primitiveIndices,
                            edgeIndices: primitiveEdgeIndices,
                            positionsDecodeMatrix: reusedPrimitivesDecodeMatrix
                        });

                        geometryCreated[geometryId] = true;
                    }

                    sceneModel.createMesh(utils.apply(meshDefaults, {
                        id: meshId,
                        geometryId: geometryId,
                        origin: tileCenter,
                        matrix: entityMatrix,
                        color: color,
                        opacity: opacity
                    }));

                    meshIds.push(meshId);

                } else {

                    sceneModel.createMesh(utils.apply(meshDefaults, {
                        id: meshId,
                        origin: tileCenter,
                        primitive: "triangles",
                        positionsCompressed: primitivePositions,
                        normalsCompressed: primitiveNormals,
                        indices: primitiveIndices,
                        edgeIndices: primitiveEdgeIndices,
                        positionsDecodeMatrix: tileDecodeMatrix,
                        color: color,
                        opacity: opacity
                    }));

                    meshIds.push(meshId);
                }
            }

            if (meshIds.length > 0) {

                sceneModel.createEntity(utils.apply(entityDefaults, {
                    id: entityId,
                    isObject: true,
                    meshIds: meshIds
                }));
            }
        }
    }
}

/** @private */
const ParserV6 = {
    version: 6,
    parse: function (viewer, options, elements, sceneModel, metaModel, manifestCtx) {
        const deflatedData = extract$5(elements);
        const inflatedData = inflate$5(deflatedData);
        load$6(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx);
    }
};

/*

 Parser for .XKT Format V7

 */

let pako$4 = window.pako || p;
if (!pako$4.inflate) {  // See https://github.com/nodeca/pako/issues/97
    pako$4 = pako$4.default;
}

function extract$4(elements) {

    return {

        // Vertex attributes

        positions: elements[0],
        normals: elements[1],
        colors: elements[2],

        // Indices

        indices: elements[3],
        edgeIndices: elements[4],

        // Transform matrices

        matrices: elements[5],

        reusedGeometriesDecodeMatrix: elements[6],

        // Geometries

        eachGeometryPrimitiveType: elements[7],
        eachGeometryPositionsPortion: elements[8],
        eachGeometryNormalsPortion: elements[9],
        eachGeometryColorsPortion: elements[10],
        eachGeometryIndicesPortion: elements[11],
        eachGeometryEdgeIndicesPortion: elements[12],

        // Meshes are grouped in runs that are shared by the same entities

        eachMeshGeometriesPortion: elements[13],
        eachMeshMatricesPortion: elements[14],
        eachMeshMaterial: elements[15],

        // Entity elements in the following arrays are grouped in runs that are shared by the same tiles

        eachEntityId: elements[16],
        eachEntityMeshesPortion: elements[17],

        eachTileAABB: elements[18],
        eachTileEntitiesPortion: elements[19]
    };
}

function inflate$4(deflatedData) {

    function inflate(array, options) {
        return (array.length === 0) ? [] : pako$4.inflate(array, options).buffer;
    }

    return {
        positions: new Uint16Array(inflate(deflatedData.positions)),
        normals: new Int8Array(inflate(deflatedData.normals)),
        colors: new Uint8Array(inflate(deflatedData.colors)),

        indices: new Uint32Array(inflate(deflatedData.indices)),
        edgeIndices: new Uint32Array(inflate(deflatedData.edgeIndices)),

        matrices: new Float32Array(inflate(deflatedData.matrices)),

        reusedGeometriesDecodeMatrix: new Float32Array(inflate(deflatedData.reusedGeometriesDecodeMatrix)),

        eachGeometryPrimitiveType: new Uint8Array(inflate(deflatedData.eachGeometryPrimitiveType)),
        eachGeometryPositionsPortion: new Uint32Array(inflate(deflatedData.eachGeometryPositionsPortion)),
        eachGeometryNormalsPortion: new Uint32Array(inflate(deflatedData.eachGeometryNormalsPortion)),
        eachGeometryColorsPortion: new Uint32Array(inflate(deflatedData.eachGeometryColorsPortion)),
        eachGeometryIndicesPortion: new Uint32Array(inflate(deflatedData.eachGeometryIndicesPortion)),
        eachGeometryEdgeIndicesPortion: new Uint32Array(inflate(deflatedData.eachGeometryEdgeIndicesPortion)),

        eachMeshGeometriesPortion: new Uint32Array(inflate(deflatedData.eachMeshGeometriesPortion)),
        eachMeshMatricesPortion: new Uint32Array(inflate(deflatedData.eachMeshMatricesPortion)),
        eachMeshMaterial: new Uint8Array(inflate(deflatedData.eachMeshMaterial)),

        eachEntityId: pako$4.inflate(deflatedData.eachEntityId, {to: 'string'}),
        eachEntityMeshesPortion: new Uint32Array(inflate(deflatedData.eachEntityMeshesPortion)),

        eachTileAABB: new Float64Array(inflate(deflatedData.eachTileAABB)),
        eachTileEntitiesPortion: new Uint32Array(inflate(deflatedData.eachTileEntitiesPortion)),
    };
}

const decompressColor$5 = (function () {
    const floatColor = new Float32Array(3);
    return function (intColor) {
        floatColor[0] = intColor[0] / 255.0;
        floatColor[1] = intColor[1] / 255.0;
        floatColor[2] = intColor[2] / 255.0;
        return floatColor;
    };
})();

function convertColorsRGBToRGBA$1(colorsRGB) {
    const colorsRGBA = [];
    for (let i = 0, len = colorsRGB.length; i < len; i+=3) {
        colorsRGBA.push(colorsRGB[i]);
        colorsRGBA.push(colorsRGB[i+1]);
        colorsRGBA.push(colorsRGB[i+2]);
        colorsRGBA.push(1.0);
    }
    return colorsRGBA;
}

function load$5(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx) {

    const modelPartId = manifestCtx.getNextId();

    const positions = inflatedData.positions;
    const normals = inflatedData.normals;
    const colors = inflatedData.colors;

    const indices = inflatedData.indices;
    const edgeIndices = inflatedData.edgeIndices;

    const matrices = inflatedData.matrices;

    const reusedGeometriesDecodeMatrix = inflatedData.reusedGeometriesDecodeMatrix;

    const eachGeometryPrimitiveType = inflatedData.eachGeometryPrimitiveType;
    const eachGeometryPositionsPortion = inflatedData.eachGeometryPositionsPortion;
    const eachGeometryNormalsPortion = inflatedData.eachGeometryNormalsPortion;
    const eachGeometryColorsPortion = inflatedData.eachGeometryColorsPortion;
    const eachGeometryIndicesPortion = inflatedData.eachGeometryIndicesPortion;
    const eachGeometryEdgeIndicesPortion = inflatedData.eachGeometryEdgeIndicesPortion;

    const eachMeshGeometriesPortion = inflatedData.eachMeshGeometriesPortion;
    const eachMeshMatricesPortion = inflatedData.eachMeshMatricesPortion;
    const eachMeshMaterial = inflatedData.eachMeshMaterial;

    const eachEntityId = JSON.parse(inflatedData.eachEntityId);
    const eachEntityMeshesPortion = inflatedData.eachEntityMeshesPortion;

    const eachTileAABB = inflatedData.eachTileAABB;
    const eachTileEntitiesPortion = inflatedData.eachTileEntitiesPortion;

    const numGeometries = eachGeometryPositionsPortion.length;
    const numMeshes = eachMeshGeometriesPortion.length;
    const numEntities = eachEntityId.length;
    const numTiles = eachTileEntitiesPortion.length;

    // Count instances of each geometry

    const geometryReuseCounts = new Uint32Array(numGeometries);

    for (let meshIndex = 0; meshIndex < numMeshes; meshIndex++) {
        const geometryIndex = eachMeshGeometriesPortion[meshIndex];
        if (geometryReuseCounts[geometryIndex] !== undefined) {
            geometryReuseCounts[geometryIndex]++;
        } else {
            geometryReuseCounts[geometryIndex] = 1;
        }
    }

    // Iterate over tiles

    const tileCenter = math.vec3();
    const rtcAABB = math.AABB3();

    for (let tileIndex = 0; tileIndex < numTiles; tileIndex++) {

        const lastTileIndex = (numTiles - 1);

        const atLastTile = (tileIndex === lastTileIndex);

        const firstTileEntityIndex = eachTileEntitiesPortion [tileIndex];
        const lastTileEntityIndex = atLastTile ? numEntities : eachTileEntitiesPortion[tileIndex + 1];

        const tileAABBIndex = tileIndex * 6;
        const tileAABB = eachTileAABB.subarray(tileAABBIndex, tileAABBIndex + 6);

        math.getAABB3Center(tileAABB, tileCenter);

        rtcAABB[0] = tileAABB[0] - tileCenter[0];
        rtcAABB[1] = tileAABB[1] - tileCenter[1];
        rtcAABB[2] = tileAABB[2] - tileCenter[2];
        rtcAABB[3] = tileAABB[3] - tileCenter[0];
        rtcAABB[4] = tileAABB[4] - tileCenter[1];
        rtcAABB[5] = tileAABB[5] - tileCenter[2];

        const tileDecodeMatrix = geometryCompressionUtils.createPositionsDecodeMatrix(rtcAABB);

        const geometryCreated = {};

        // Iterate over each tile's entities

        for (let tileEntityIndex = firstTileEntityIndex; tileEntityIndex < lastTileEntityIndex; tileEntityIndex++) {

            const xktEntityId = eachEntityId[tileEntityIndex];
            const entityId = options.globalizeObjectIds ? math.globalizeObjectId(sceneModel.id, xktEntityId) : xktEntityId;

            const lastTileEntityIndex = (numEntities - 1);
            const atLastTileEntity = (tileEntityIndex === lastTileEntityIndex);
            const firstMeshIndex = eachEntityMeshesPortion [tileEntityIndex];
            const lastMeshIndex = atLastTileEntity ? eachMeshGeometriesPortion.length : eachEntityMeshesPortion[tileEntityIndex + 1];

            const meshIds = [];

            const metaObject = viewer.metaScene.metaObjects[entityId];
            const entityDefaults = {};
            const meshDefaults = {};

            if (metaObject) {

                // Mask loading of object types

                if (options.excludeTypesMap && metaObject.type && options.excludeTypesMap[metaObject.type]) {
                    continue;
                }

                if (options.includeTypesMap && metaObject.type && (!options.includeTypesMap[metaObject.type])) {
                    continue;
                }

                // Mask loading of object ids

                if (options.includeIdsMap && metaObject.id && (!options.includeIdsMap[metaObject.id])) {
                    continue;
                }

                // Get initial property values for object types

                const props = options.objectDefaults ? options.objectDefaults[metaObject.type] || options.objectDefaults["DEFAULT"] : null;

                if (props) {
                    if (props.visible === false) {
                        entityDefaults.visible = false;
                    }
                    if (props.pickable === false) {
                        entityDefaults.pickable = false;
                    }
                    if (props.colorize) {
                        meshDefaults.color = props.colorize;
                    }
                    if (props.opacity !== undefined && props.opacity !== null) {
                        meshDefaults.opacity = props.opacity;
                    }
                    if (props.metallic !== undefined && props.metallic !== null) {
                        meshDefaults.metallic = props.metallic;
                    }
                    if (props.roughness !== undefined && props.roughness !== null) {
                        meshDefaults.roughness = props.roughness;
                    }
                }

            } else {
                if (options.excludeUnclassifiedObjects) {
                    continue;
                }
            }

            // Iterate each entity's meshes

            for (let meshIndex = firstMeshIndex; meshIndex < lastMeshIndex; meshIndex++) {

                const geometryIndex = eachMeshGeometriesPortion[meshIndex];
                const geometryReuseCount = geometryReuseCounts[geometryIndex];
                const isReusedGeometry = (geometryReuseCount > 1);

                const atLastGeometry = (geometryIndex === (numGeometries - 1));

                const meshColor = decompressColor$5(eachMeshMaterial.subarray((meshIndex * 6), (meshIndex * 6) + 3));
                const meshOpacity = eachMeshMaterial[(meshIndex * 6) + 3] / 255.0;
                const meshMetallic = eachMeshMaterial[(meshIndex * 6) + 4] / 255.0;
                const meshRoughness = eachMeshMaterial[(meshIndex * 6) + 5] / 255.0;

                const meshId = manifestCtx.getNextId();

                if (isReusedGeometry) {

                    // Create mesh for multi-use geometry - create (or reuse) geometry, create mesh using that geometry

                    const meshMatrixIndex = eachMeshMatricesPortion[meshIndex];
                    const meshMatrix = matrices.slice(meshMatrixIndex, meshMatrixIndex + 16);

                    const geometryId = `${modelPartId}-geometry.${tileIndex}.${geometryIndex}`; // These IDs are local to the SceneModel

                    if (!geometryCreated[geometryId]) {

                        const primitiveType = eachGeometryPrimitiveType[geometryIndex];

                        let primitiveName;
                        let geometryPositions;
                        let geometryNormals;
                        let geometryColors;
                        let geometryIndices;
                        let geometryEdgeIndices;

                        switch (primitiveType) {
                            case 0:
                                primitiveName = "solid";
                                geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                                geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                                geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                                break;
                            case 1:
                                primitiveName = "surface";
                                geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                                geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                                geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                                break;
                            case 2:
                                primitiveName = "points";
                                geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryColors = convertColorsRGBToRGBA$1(colors.subarray(eachGeometryColorsPortion [geometryIndex], atLastGeometry ? colors.length : eachGeometryColorsPortion [geometryIndex + 1]));
                                break;
                            case 3:
                                primitiveName = "lines";
                                geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                                break;
                            default:
                                continue;
                        }

                        sceneModel.createGeometry({
                            id: geometryId,
                            primitive: primitiveName,
                            positionsCompressed: geometryPositions,
                            normalsCompressed: geometryNormals,
                            colors: geometryColors,
                            indices: geometryIndices,
                            edgeIndices: geometryEdgeIndices,
                            positionsDecodeMatrix: reusedGeometriesDecodeMatrix
                        });

                        geometryCreated[geometryId] = true;
                    }

                    sceneModel.createMesh(utils.apply(meshDefaults, {
                        id: meshId,
                        geometryId: geometryId,
                        origin: tileCenter,
                        matrix: meshMatrix,
                        color: meshColor,
                        metallic: meshMetallic,
                        roughness: meshRoughness,
                        opacity: meshOpacity
                    }));

                    meshIds.push(meshId);

                } else {

                    const primitiveType = eachGeometryPrimitiveType[geometryIndex];

                    let primitiveName;
                    let geometryPositions;
                    let geometryNormals;
                    let geometryColors;
                    let geometryIndices;
                    let geometryEdgeIndices;

                    switch (primitiveType) {
                        case 0:
                            primitiveName = "solid";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                            geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                            geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                            break;
                        case 1:
                            primitiveName = "surface";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                            geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                            geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                            break;
                        case 2:
                            primitiveName = "points";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryColors = convertColorsRGBToRGBA$1(colors.subarray(eachGeometryColorsPortion [geometryIndex], atLastGeometry ? colors.length : eachGeometryColorsPortion [geometryIndex + 1]));
                            break;
                        case 3:
                            primitiveName = "lines";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                            break;
                        default:
                            continue;
                    }

                    sceneModel.createMesh(utils.apply(meshDefaults, {
                        id: meshId,
                        origin: tileCenter,
                        primitive: primitiveName,
                        positionsCompressed: geometryPositions,
                        normalsCompressed: geometryNormals,
                        colors: geometryColors,
                        indices: geometryIndices,
                        edgeIndices: geometryEdgeIndices,
                        positionsDecodeMatrix: tileDecodeMatrix,
                        color: meshColor,
                        metallic: meshMetallic,
                        roughness: meshRoughness,
                        opacity: meshOpacity
                    }));

                    meshIds.push(meshId);
                }
            }

            if (meshIds.length > 0) {

                sceneModel.createEntity(utils.apply(entityDefaults, {
                    id: entityId,
                    isObject: true,
                    meshIds: meshIds
                }));
            }
        }
    }
}

/** @private */
const ParserV7 = {
    version: 7,
    parse: function (viewer, options, elements, sceneModel, metaModel, manifestCtx) {
        const deflatedData = extract$4(elements);
        const inflatedData = inflate$4(deflatedData);
        load$5(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx);
    }
};

/*

 Parser for .XKT Format V8

 */

let pako$3 = window.pako || p;
if (!pako$3.inflate) {  // See https://github.com/nodeca/pako/issues/97
    pako$3 = pako$3.default;
}

const tempVec4a$4 = math.vec4();
const tempVec4b$4 = math.vec4();

function extract$3(elements) {

    return {

        // Vertex attributes

        types: elements[0],
        eachMetaObjectId: elements[1],
        eachMetaObjectType: elements[2],
        eachMetaObjectName: elements[3],
        eachMetaObjectParent: elements[4],

        positions: elements[5],
        normals: elements[6],
        colors: elements[7],
        indices: elements[8],
        edgeIndices: elements[9],

        // Transform matrices

        matrices: elements[10],
        reusedGeometriesDecodeMatrix: elements[11],

        // Geometries

        eachGeometryPrimitiveType: elements[12],
        eachGeometryPositionsPortion: elements[13],
        eachGeometryNormalsPortion: elements[14],
        eachGeometryColorsPortion: elements[15],
        eachGeometryIndicesPortion: elements[16],
        eachGeometryEdgeIndicesPortion: elements[17],

        // Meshes are grouped in runs that are shared by the same entities

        eachMeshGeometriesPortion: elements[18],
        eachMeshMatricesPortion: elements[19],
        eachMeshMaterial: elements[20],

        // Entity elements in the following arrays are grouped in runs that are shared by the same tiles

        eachEntityMetaObject: elements[21],
        eachEntityMeshesPortion: elements[22],

        eachTileAABB: elements[23],
        eachTileEntitiesPortion: elements[24]
    };
}

function inflate$3(deflatedData) {

    function inflate(array, options) {
        return (array.length === 0) ? [] : pako$3.inflate(array, options).buffer;
    }

    return {

        types: pako$3.inflate(deflatedData.types, {to: 'string'}),
        eachMetaObjectId: pako$3.inflate(deflatedData.eachMetaObjectId, {to: 'string'}),
        eachMetaObjectType: new Uint32Array(inflate(deflatedData.eachMetaObjectType)),
        eachMetaObjectName: pako$3.inflate(deflatedData.eachMetaObjectName, {to: 'string'}),
        eachMetaObjectParent: new Uint32Array(inflate(deflatedData.eachMetaObjectParent)),

        positions: new Uint16Array(inflate(deflatedData.positions)),
        normals: new Int8Array(inflate(deflatedData.normals)),
        colors: new Uint8Array(inflate(deflatedData.colors)),
        indices: new Uint32Array(inflate(deflatedData.indices)),
        edgeIndices: new Uint32Array(inflate(deflatedData.edgeIndices)),

        matrices: new Float32Array(inflate(deflatedData.matrices)),
        reusedGeometriesDecodeMatrix: new Float32Array(inflate(deflatedData.reusedGeometriesDecodeMatrix)),

        eachGeometryPrimitiveType: new Uint8Array(inflate(deflatedData.eachGeometryPrimitiveType)),
        eachGeometryPositionsPortion: new Uint32Array(inflate(deflatedData.eachGeometryPositionsPortion)),
        eachGeometryNormalsPortion: new Uint32Array(inflate(deflatedData.eachGeometryNormalsPortion)),
        eachGeometryColorsPortion: new Uint32Array(inflate(deflatedData.eachGeometryColorsPortion)),
        eachGeometryIndicesPortion: new Uint32Array(inflate(deflatedData.eachGeometryIndicesPortion)),
        eachGeometryEdgeIndicesPortion: new Uint32Array(inflate(deflatedData.eachGeometryEdgeIndicesPortion)),

        eachMeshGeometriesPortion: new Uint32Array(inflate(deflatedData.eachMeshGeometriesPortion)),
        eachMeshMatricesPortion: new Uint32Array(inflate(deflatedData.eachMeshMatricesPortion)),
        eachMeshMaterial: new Uint8Array(inflate(deflatedData.eachMeshMaterial)),

        eachEntityMetaObject: new Uint32Array(inflate(deflatedData.eachEntityMetaObject)),
        eachEntityMeshesPortion: new Uint32Array(inflate(deflatedData.eachEntityMeshesPortion)),

        eachTileAABB: new Float64Array(inflate(deflatedData.eachTileAABB)),
        eachTileEntitiesPortion: new Uint32Array(inflate(deflatedData.eachTileEntitiesPortion)),
    };
}

const decompressColor$4 = (function () {
    const floatColor = new Float32Array(3);
    return function (intColor) {
        floatColor[0] = intColor[0] / 255.0;
        floatColor[1] = intColor[1] / 255.0;
        floatColor[2] = intColor[2] / 255.0;
        return floatColor;
    };
})();

function convertColorsRGBToRGBA(colorsRGB) {
    const colorsRGBA = [];
    for (let i = 0, len = colorsRGB.length; i < len; i += 3) {
        colorsRGBA.push(colorsRGB[i]);
        colorsRGBA.push(colorsRGB[i + 1]);
        colorsRGBA.push(colorsRGB[i + 2]);
        colorsRGBA.push(1.0);
    }
    return colorsRGBA;
}

function load$4(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx) {

    const modelPartId = manifestCtx.getNextId();

    const types = JSON.parse(inflatedData.types);
    const eachMetaObjectId = JSON.parse(inflatedData.eachMetaObjectId);
    const eachMetaObjectType = inflatedData.eachMetaObjectType;
    const eachMetaObjectName = JSON.parse(inflatedData.eachMetaObjectName);
    const eachMetaObjectParent = inflatedData.eachMetaObjectParent;

    const positions = inflatedData.positions;
    const normals = inflatedData.normals;
    const colors = inflatedData.colors;
    const indices = inflatedData.indices;
    const edgeIndices = inflatedData.edgeIndices;

    const matrices = inflatedData.matrices;
    const reusedGeometriesDecodeMatrix = inflatedData.reusedGeometriesDecodeMatrix;

    const eachGeometryPrimitiveType = inflatedData.eachGeometryPrimitiveType;
    const eachGeometryPositionsPortion = inflatedData.eachGeometryPositionsPortion;
    const eachGeometryNormalsPortion = inflatedData.eachGeometryNormalsPortion;
    const eachGeometryColorsPortion = inflatedData.eachGeometryColorsPortion;
    const eachGeometryIndicesPortion = inflatedData.eachGeometryIndicesPortion;
    const eachGeometryEdgeIndicesPortion = inflatedData.eachGeometryEdgeIndicesPortion;

    const eachMeshGeometriesPortion = inflatedData.eachMeshGeometriesPortion;
    const eachMeshMatricesPortion = inflatedData.eachMeshMatricesPortion;
    const eachMeshMaterial = inflatedData.eachMeshMaterial;

    const eachEntityMetaObject = inflatedData.eachEntityMetaObject;
    const eachEntityMeshesPortion = inflatedData.eachEntityMeshesPortion;

    const eachTileAABB = inflatedData.eachTileAABB;
    const eachTileEntitiesPortion = inflatedData.eachTileEntitiesPortion;

    const numMetaObjects = eachMetaObjectId.length;
    const numGeometries = eachGeometryPositionsPortion.length;
    const numMeshes = eachMeshGeometriesPortion.length;
    const numEntities = eachEntityMetaObject.length;
    const numTiles = eachTileEntitiesPortion.length;

    if (metaModel) {
        const metaModelData = {
            metaObjects: []
        };
        for (let metaObjectIndex = 0; metaObjectIndex < numMetaObjects; metaObjectIndex++) {
            const metaObjectId = eachMetaObjectId[metaObjectIndex];
            const typeIndex = eachMetaObjectType[metaObjectIndex];
            const metaObjectType = types[typeIndex] || "default";
            const metaObjectName = eachMetaObjectName[metaObjectIndex];
            const metaObjectParentIndex = eachMetaObjectParent[metaObjectIndex];
            const metaObjectParentId = (metaObjectParentIndex !== metaObjectIndex) ? eachMetaObjectId[metaObjectParentIndex] : null;
            metaModelData.metaObjects.push({
                id: metaObjectId,
                type: metaObjectType,
                name: metaObjectName,
                parent: metaObjectParentId
            });
        }
        metaModel.loadData(metaModelData, {
            includeTypes: options.includeTypes,
            excludeTypes: options.excludeTypes,
            globalizeObjectIds: options.globalizeObjectIds
        });
    }

    // Count instances of each geometry

    const geometryReuseCounts = new Uint32Array(numGeometries);

    for (let meshIndex = 0; meshIndex < numMeshes; meshIndex++) {
        const geometryIndex = eachMeshGeometriesPortion[meshIndex];
        if (geometryReuseCounts[geometryIndex] !== undefined) {
            geometryReuseCounts[geometryIndex]++;
        } else {
            geometryReuseCounts[geometryIndex] = 1;
        }
    }

    // Iterate over tiles

    const tileCenter = math.vec3();
    const rtcAABB = math.AABB3();

    const geometryArraysCache = {};

    for (let tileIndex = 0; tileIndex < numTiles; tileIndex++) {

        const lastTileIndex = (numTiles - 1);

        const atLastTile = (tileIndex === lastTileIndex);

        const firstTileEntityIndex = eachTileEntitiesPortion [tileIndex];
        const lastTileEntityIndex = atLastTile ? numEntities : eachTileEntitiesPortion[tileIndex + 1];

        const tileAABBIndex = tileIndex * 6;
        const tileAABB = eachTileAABB.subarray(tileAABBIndex, tileAABBIndex + 6);

        math.getAABB3Center(tileAABB, tileCenter);

        rtcAABB[0] = tileAABB[0] - tileCenter[0];
        rtcAABB[1] = tileAABB[1] - tileCenter[1];
        rtcAABB[2] = tileAABB[2] - tileCenter[2];
        rtcAABB[3] = tileAABB[3] - tileCenter[0];
        rtcAABB[4] = tileAABB[4] - tileCenter[1];
        rtcAABB[5] = tileAABB[5] - tileCenter[2];

        const tileDecodeMatrix = geometryCompressionUtils.createPositionsDecodeMatrix(rtcAABB);

        const geometryCreatedInTile = {};

        // Iterate over each tile's entities

        for (let tileEntityIndex = firstTileEntityIndex; tileEntityIndex < lastTileEntityIndex; tileEntityIndex++) {

            const xktMetaObjectIndex = eachEntityMetaObject[tileEntityIndex];
            const xktMetaObjectId = eachMetaObjectId[xktMetaObjectIndex];
            const xktEntityId = xktMetaObjectId;

            const entityId = options.globalizeObjectIds ? math.globalizeObjectId(sceneModel.id, xktEntityId) : xktEntityId;

            const lastTileEntityIndex = (numEntities - 1);
            const atLastTileEntity = (tileEntityIndex === lastTileEntityIndex);
            const firstMeshIndex = eachEntityMeshesPortion [tileEntityIndex];
            const lastMeshIndex = atLastTileEntity ? eachMeshGeometriesPortion.length : eachEntityMeshesPortion[tileEntityIndex + 1];

            const meshIds = [];

            const metaObject = viewer.metaScene.metaObjects[entityId];
            const entityDefaults = {};
            const meshDefaults = {};

            if (metaObject) {

                // Mask loading of object types

                if (options.excludeTypesMap && metaObject.type && options.excludeTypesMap[metaObject.type]) {
                    continue;
                }

                if (options.includeTypesMap && metaObject.type && (!options.includeTypesMap[metaObject.type])) {
                    continue;
                }

                // Mask loading of object ids

                if (options.includeIdsMap && metaObject.id && (!options.includeIdsMap[metaObject.id])) {
                    continue;
                }

                // Get initial property values for object types

                const props = options.objectDefaults ? options.objectDefaults[metaObject.type] || options.objectDefaults["DEFAULT"] : null;

                if (props) {
                    if (props.visible === false) {
                        entityDefaults.visible = false;
                    }
                    if (props.pickable === false) {
                        entityDefaults.pickable = false;
                    }
                    if (props.colorize) {
                        meshDefaults.color = props.colorize;
                    }
                    if (props.opacity !== undefined && props.opacity !== null) {
                        meshDefaults.opacity = props.opacity;
                    }
                    if (props.metallic !== undefined && props.metallic !== null) {
                        meshDefaults.metallic = props.metallic;
                    }
                    if (props.roughness !== undefined && props.roughness !== null) {
                        meshDefaults.roughness = props.roughness;
                    }
                }

            } else {
                if (options.excludeUnclassifiedObjects) {
                    continue;
                }
            }

            // Iterate each entity's meshes

            for (let meshIndex = firstMeshIndex; meshIndex < lastMeshIndex; meshIndex++) {

                const geometryIndex = eachMeshGeometriesPortion[meshIndex];
                const geometryReuseCount = geometryReuseCounts[geometryIndex];
                const isReusedGeometry = (geometryReuseCount > 1);

                const atLastGeometry = (geometryIndex === (numGeometries - 1));

                const meshColor = decompressColor$4(eachMeshMaterial.subarray((meshIndex * 6), (meshIndex * 6) + 3));
                const meshOpacity = eachMeshMaterial[(meshIndex * 6) + 3] / 255.0;
                const meshMetallic = eachMeshMaterial[(meshIndex * 6) + 4] / 255.0;
                const meshRoughness = eachMeshMaterial[(meshIndex * 6) + 5] / 255.0;

                const meshId = manifestCtx.getNextId();

                if (isReusedGeometry) {

                    // Create mesh for multi-use geometry - create (or reuse) geometry, create mesh using that geometry

                    const meshMatrixIndex = eachMeshMatricesPortion[meshIndex];
                    const meshMatrix = matrices.slice(meshMatrixIndex, meshMatrixIndex + 16);

                     const geometryId = `${modelPartId}-geometry.${tileIndex}.${geometryIndex}`; // These IDs are local to the SceneModel

                    let geometryArrays = geometryArraysCache[geometryId];

                    if (!geometryArrays) {

                        geometryArrays = {
                            batchThisMesh: (!options.reuseGeometries)
                        };

                        const primitiveType = eachGeometryPrimitiveType[geometryIndex];

                        let geometryValid = false;

                        switch (primitiveType) {
                            case 0:
                                geometryArrays.primitiveName = "solid";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                                geometryArrays.geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                                geometryArrays.geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            case 1:
                                geometryArrays.primitiveName = "surface";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                                geometryArrays.geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                                geometryArrays.geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            case 2:
                                geometryArrays.primitiveName = "points";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryColors = convertColorsRGBToRGBA(colors.subarray(eachGeometryColorsPortion [geometryIndex], atLastGeometry ? colors.length : eachGeometryColorsPortion [geometryIndex + 1]));
                                geometryValid = (geometryArrays.geometryPositions.length > 0);
                                break;
                            case 3:
                                geometryArrays.primitiveName = "lines";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            default:
                                continue;
                        }

                        if (!geometryValid) {
                            geometryArrays = null;
                        }

                        if (geometryArrays) {
                            if (geometryArrays.geometryPositions.length > 1000) ;
                            if (geometryArrays.batchThisMesh) {
                                geometryArrays.decompressedPositions = new Float32Array(geometryArrays.geometryPositions.length);
                                const geometryPositions = geometryArrays.geometryPositions;
                                const decompressedPositions = geometryArrays.decompressedPositions;
                                for (let i = 0, len = geometryPositions.length; i < len; i += 3) {
                                    decompressedPositions[i + 0] = geometryPositions[i + 0] * reusedGeometriesDecodeMatrix[0] + reusedGeometriesDecodeMatrix[12];
                                    decompressedPositions[i + 1] = geometryPositions[i + 1] * reusedGeometriesDecodeMatrix[5] + reusedGeometriesDecodeMatrix[13];
                                    decompressedPositions[i + 2] = geometryPositions[i + 2] * reusedGeometriesDecodeMatrix[10] + reusedGeometriesDecodeMatrix[14];
                                }
                                geometryArrays.geometryPositions = null;
                                geometryArraysCache[geometryId] = geometryArrays;
                            }
                        }
                    }

                    if (geometryArrays) {

                        if (geometryArrays.batchThisMesh) {

                            const decompressedPositions = geometryArrays.decompressedPositions;
                            const positions = new Uint16Array(decompressedPositions.length);
                            for (let i = 0, len = decompressedPositions.length; i < len; i += 3) {
                                tempVec4a$4[0] = decompressedPositions[i + 0];
                                tempVec4a$4[1] = decompressedPositions[i + 1];
                                tempVec4a$4[2] = decompressedPositions[i + 2];
                                tempVec4a$4[3] = 1;
                                math.transformVec4(meshMatrix, tempVec4a$4, tempVec4b$4);
                                geometryCompressionUtils.compressPosition(tempVec4b$4, rtcAABB, tempVec4a$4);
                                positions[i + 0] = tempVec4a$4[0];
                                positions[i + 1] = tempVec4a$4[1];
                                positions[i + 2] = tempVec4a$4[2];
                            }

                            sceneModel.createMesh(utils.apply(meshDefaults, {
                                id: meshId,
                                origin: tileCenter,
                                primitive: geometryArrays.primitiveName,
                                positionsCompressed: positions,
                                normalsCompressed: geometryArrays.geometryNormals,
                                colorsCompressed: geometryArrays.geometryColors,
                                indices: geometryArrays.geometryIndices,
                                edgeIndices: geometryArrays.geometryEdgeIndices,
                                positionsDecodeMatrix: tileDecodeMatrix,
                                color: meshColor,
                                metallic: meshMetallic,
                                roughness: meshRoughness,
                                opacity: meshOpacity
                            }));

                            meshIds.push(meshId);

                        } else {

                            if (!geometryCreatedInTile[geometryId]) {

                                sceneModel.createGeometry({
                                    id: geometryId,
                                    primitive: geometryArrays.primitiveName,
                                    positionsCompressed: geometryArrays.geometryPositions,
                                    normalsCompressed: geometryArrays.geometryNormals,
                                    colorsCompressed: geometryArrays.geometryColors,
                                    indices: geometryArrays.geometryIndices,
                                    edgeIndices: geometryArrays.geometryEdgeIndices,
                                    positionsDecodeMatrix: reusedGeometriesDecodeMatrix
                                });

                                geometryCreatedInTile[geometryId] = true;
                            }

                            sceneModel.createMesh(utils.apply(meshDefaults, {
                                id: meshId,
                                geometryId: geometryId,
                                origin: tileCenter,
                                matrix: meshMatrix,
                                color: meshColor,
                                metallic: meshMetallic,
                                roughness: meshRoughness,
                                opacity: meshOpacity
                            }));

                            meshIds.push(meshId);
                        }
                    }

                } else {

                    const primitiveType = eachGeometryPrimitiveType[geometryIndex];

                    let primitiveName;
                    let geometryPositions;
                    let geometryNormals;
                    let geometryColors;
                    let geometryIndices;
                    let geometryEdgeIndices;
                    let geometryValid = false;

                    switch (primitiveType) {
                        case 0:
                            primitiveName = "solid";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                            geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                            geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                        case 1:
                            primitiveName = "surface";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                            geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                            geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                        case 2:
                            primitiveName = "points";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryColors = convertColorsRGBToRGBA(colors.subarray(eachGeometryColorsPortion [geometryIndex], atLastGeometry ? colors.length : eachGeometryColorsPortion [geometryIndex + 1]));
                            geometryValid = (geometryPositions.length > 0);
                            break;
                        case 3:
                            primitiveName = "lines";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                        default:
                            continue;
                    }

                    if (geometryValid) {

                        sceneModel.createMesh(utils.apply(meshDefaults, {
                            id: meshId,
                            origin: tileCenter,
                            primitive: primitiveName,
                            positionsCompressed: geometryPositions,
                            normalsCompressed: geometryNormals,
                            colorsCompressed: geometryColors,
                            indices: geometryIndices,
                            edgeIndices: geometryEdgeIndices,
                            positionsDecodeMatrix: tileDecodeMatrix,
                            color: meshColor,
                            metallic: meshMetallic,
                            roughness: meshRoughness,
                            opacity: meshOpacity
                        }));

                        meshIds.push(meshId);
                    }
                }
            }

            if (meshIds.length > 0) {

                sceneModel.createEntity(utils.apply(entityDefaults, {
                    id: entityId,
                    isObject: true,
                    meshIds: meshIds
                }));
            }
        }
    }
}

/** @private */
const ParserV8 = {
    version: 8,
    parse: function (viewer, options, elements, sceneModel, metaModel, manifestCtx) {
        const deflatedData = extract$3(elements);
        const inflatedData = inflate$3(deflatedData);
        load$4(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx);
    }
};

/*

 Parser for .XKT Format V9

 */

let pako$2 = window.pako || p;
if (!pako$2.inflate) {  // See https://github.com/nodeca/pako/issues/97
    pako$2 = pako$2.default;
}

const tempVec4a$3 = math.vec4();
const tempVec4b$3 = math.vec4();

function extract$2(elements) {

    return {

        // Metadata

        metadata: elements[0],

        positions: elements[1],
        normals: elements[2],
        colors: elements[3],
        indices: elements[4],
        edgeIndices: elements[5],

        // Transform matrices

        matrices: elements[6],
        reusedGeometriesDecodeMatrix: elements[7],

        // Geometries

        eachGeometryPrimitiveType: elements[8],
        eachGeometryPositionsPortion: elements[9],
        eachGeometryNormalsPortion: elements[10],
        eachGeometryColorsPortion: elements[11],
        eachGeometryIndicesPortion: elements[12],
        eachGeometryEdgeIndicesPortion: elements[13],

        // Meshes are grouped in runs that are shared by the same entities

        eachMeshGeometriesPortion: elements[14],
        eachMeshMatricesPortion: elements[15],
        eachMeshMaterial: elements[16],

        // Entity elements in the following arrays are grouped in runs that are shared by the same tiles

        eachEntityId: elements[17],
        eachEntityMeshesPortion: elements[18],

        eachTileAABB: elements[19],
        eachTileEntitiesPortion: elements[20]
    };
}

function inflate$2(deflatedData) {

    function inflate(array, options) {
        return (array.length === 0) ? [] : pako$2.inflate(array, options).buffer;
    }

    return {

        metadata: JSON.parse(pako$2.inflate(deflatedData.metadata, {to: 'string'})),

        positions: new Uint16Array(inflate(deflatedData.positions)),
        normals: new Int8Array(inflate(deflatedData.normals)),
        colors: new Uint8Array(inflate(deflatedData.colors)),
        indices: new Uint32Array(inflate(deflatedData.indices)),
        edgeIndices: new Uint32Array(inflate(deflatedData.edgeIndices)),

        matrices: new Float32Array(inflate(deflatedData.matrices)),
        reusedGeometriesDecodeMatrix: new Float32Array(inflate(deflatedData.reusedGeometriesDecodeMatrix)),

        eachGeometryPrimitiveType: new Uint8Array(inflate(deflatedData.eachGeometryPrimitiveType)),
        eachGeometryPositionsPortion: new Uint32Array(inflate(deflatedData.eachGeometryPositionsPortion)),
        eachGeometryNormalsPortion: new Uint32Array(inflate(deflatedData.eachGeometryNormalsPortion)),
        eachGeometryColorsPortion: new Uint32Array(inflate(deflatedData.eachGeometryColorsPortion)),
        eachGeometryIndicesPortion: new Uint32Array(inflate(deflatedData.eachGeometryIndicesPortion)),
        eachGeometryEdgeIndicesPortion: new Uint32Array(inflate(deflatedData.eachGeometryEdgeIndicesPortion)),

        eachMeshGeometriesPortion: new Uint32Array(inflate(deflatedData.eachMeshGeometriesPortion)),
        eachMeshMatricesPortion: new Uint32Array(inflate(deflatedData.eachMeshMatricesPortion)),
        eachMeshMaterial: new Uint8Array(inflate(deflatedData.eachMeshMaterial)),

        eachEntityId: JSON.parse(pako$2.inflate(deflatedData.eachEntityId, {to: 'string'})),
        eachEntityMeshesPortion: new Uint32Array(inflate(deflatedData.eachEntityMeshesPortion)),

        eachTileAABB: new Float64Array(inflate(deflatedData.eachTileAABB)),
        eachTileEntitiesPortion: new Uint32Array(inflate(deflatedData.eachTileEntitiesPortion)),
    };
}

const decompressColor$3 = (function () {
    const floatColor = new Float32Array(3);
    return function (intColor) {
        floatColor[0] = intColor[0] / 255.0;
        floatColor[1] = intColor[1] / 255.0;
        floatColor[2] = intColor[2] / 255.0;
        return floatColor;
    };
})();

function load$3(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx) {

    const modelPartId = manifestCtx.getNextId();

    const metadata = inflatedData.metadata;

    const positions = inflatedData.positions;
    const normals = inflatedData.normals;
    const colors = inflatedData.colors;
    const indices = inflatedData.indices;
    const edgeIndices = inflatedData.edgeIndices;

    const matrices = inflatedData.matrices;
    const reusedGeometriesDecodeMatrix = inflatedData.reusedGeometriesDecodeMatrix;

    const eachGeometryPrimitiveType = inflatedData.eachGeometryPrimitiveType;
    const eachGeometryPositionsPortion = inflatedData.eachGeometryPositionsPortion;
    const eachGeometryNormalsPortion = inflatedData.eachGeometryNormalsPortion;
    const eachGeometryColorsPortion = inflatedData.eachGeometryColorsPortion;
    const eachGeometryIndicesPortion = inflatedData.eachGeometryIndicesPortion;
    const eachGeometryEdgeIndicesPortion = inflatedData.eachGeometryEdgeIndicesPortion;

    const eachMeshGeometriesPortion = inflatedData.eachMeshGeometriesPortion;
    const eachMeshMatricesPortion = inflatedData.eachMeshMatricesPortion;
    const eachMeshMaterial = inflatedData.eachMeshMaterial;

    const eachEntityId = inflatedData.eachEntityId;
    const eachEntityMeshesPortion = inflatedData.eachEntityMeshesPortion;

    const eachTileAABB = inflatedData.eachTileAABB;
    const eachTileEntitiesPortion = inflatedData.eachTileEntitiesPortion;

    const numGeometries = eachGeometryPositionsPortion.length;
    const numMeshes = eachMeshGeometriesPortion.length;
    const numEntities = eachEntityMeshesPortion.length;
    const numTiles = eachTileEntitiesPortion.length;

    if (metaModel) {
        metaModel.loadData(metadata, {
            includeTypes: options.includeTypes,
            excludeTypes: options.excludeTypes,
            globalizeObjectIds: options.globalizeObjectIds
        }); // Can be empty
    }

    // Count instances of each geometry

    const geometryReuseCounts = new Uint32Array(numGeometries);

    for (let meshIndex = 0; meshIndex < numMeshes; meshIndex++) {
        const geometryIndex = eachMeshGeometriesPortion[meshIndex];
        if (geometryReuseCounts[geometryIndex] !== undefined) {
            geometryReuseCounts[geometryIndex]++;
        } else {
            geometryReuseCounts[geometryIndex] = 1;
        }
    }

    // Iterate over tiles

    const tileCenter = math.vec3();
    const rtcAABB = math.AABB3();

    const geometryArraysCache = {};

    for (let tileIndex = 0; tileIndex < numTiles; tileIndex++) {

        const lastTileIndex = (numTiles - 1);

        const atLastTile = (tileIndex === lastTileIndex);

        const firstTileEntityIndex = eachTileEntitiesPortion [tileIndex];
        const lastTileEntityIndex = atLastTile ? (numEntities - 1) : (eachTileEntitiesPortion[tileIndex + 1] - 1);

        const tileAABBIndex = tileIndex * 6;
        const tileAABB = eachTileAABB.subarray(tileAABBIndex, tileAABBIndex + 6);

        math.getAABB3Center(tileAABB, tileCenter);

        rtcAABB[0] = tileAABB[0] - tileCenter[0];
        rtcAABB[1] = tileAABB[1] - tileCenter[1];
        rtcAABB[2] = tileAABB[2] - tileCenter[2];
        rtcAABB[3] = tileAABB[3] - tileCenter[0];
        rtcAABB[4] = tileAABB[4] - tileCenter[1];
        rtcAABB[5] = tileAABB[5] - tileCenter[2];

        const tileDecodeMatrix = geometryCompressionUtils.createPositionsDecodeMatrix(rtcAABB);

        const geometryCreatedInTile = {};

        // Iterate over each tile's entities

        for (let tileEntityIndex = firstTileEntityIndex; tileEntityIndex <= lastTileEntityIndex; tileEntityIndex++) {

            const xktEntityId = eachEntityId[tileEntityIndex];

            const entityId = options.globalizeObjectIds ? math.globalizeObjectId(sceneModel.id, xktEntityId) : xktEntityId;

            const finalTileEntityIndex = (numEntities - 1);
            const atLastTileEntity = (tileEntityIndex === finalTileEntityIndex);
            const firstMeshIndex = eachEntityMeshesPortion [tileEntityIndex];
            const lastMeshIndex = atLastTileEntity ? (eachMeshGeometriesPortion.length - 1) : (eachEntityMeshesPortion[tileEntityIndex + 1] - 1);

            const meshIds = [];

            const metaObject = viewer.metaScene.metaObjects[entityId];
            const entityDefaults = {};
            const meshDefaults = {};

            if (metaObject) {

                // Mask loading of object types

                if (options.excludeTypesMap && metaObject.type && options.excludeTypesMap[metaObject.type]) {
                    continue;
                }

                if (options.includeTypesMap && metaObject.type && (!options.includeTypesMap[metaObject.type])) {
                    continue;
                }

                // Mask loading of object ids

                if (options.includeIdsMap && metaObject.id && (!options.includeIdsMap[metaObject.id])) {
                    continue;
                }

                // Get initial property values for object types

                const props = options.objectDefaults ? options.objectDefaults[metaObject.type] || options.objectDefaults["DEFAULT"] : null;

                if (props) {
                    if (props.visible === false) {
                        entityDefaults.visible = false;
                    }
                    if (props.pickable === false) {
                        entityDefaults.pickable = false;
                    }
                    if (props.colorize) {
                        meshDefaults.color = props.colorize;
                    }
                    if (props.opacity !== undefined && props.opacity !== null) {
                        meshDefaults.opacity = props.opacity;
                    }
                    if (props.metallic !== undefined && props.metallic !== null) {
                        meshDefaults.metallic = props.metallic;
                    }
                    if (props.roughness !== undefined && props.roughness !== null) {
                        meshDefaults.roughness = props.roughness;
                    }
                }

            } else {
                if (options.excludeUnclassifiedObjects) {
                    continue;
                }
            }

            // Iterate each entity's meshes

            for (let meshIndex = firstMeshIndex; meshIndex <= lastMeshIndex; meshIndex++) {

                const geometryIndex = eachMeshGeometriesPortion[meshIndex];
                const geometryReuseCount = geometryReuseCounts[geometryIndex];
                const isReusedGeometry = (geometryReuseCount > 1);

                const atLastGeometry = (geometryIndex === (numGeometries - 1));

                const meshColor = decompressColor$3(eachMeshMaterial.subarray((meshIndex * 6), (meshIndex * 6) + 3));
                const meshOpacity = eachMeshMaterial[(meshIndex * 6) + 3] / 255.0;
                const meshMetallic = eachMeshMaterial[(meshIndex * 6) + 4] / 255.0;
                const meshRoughness = eachMeshMaterial[(meshIndex * 6) + 5] / 255.0;

                const meshId = manifestCtx.getNextId();

                if (isReusedGeometry) {

                    // Create mesh for multi-use geometry - create (or reuse) geometry, create mesh using that geometry

                    const meshMatrixIndex = eachMeshMatricesPortion[meshIndex];
                    const meshMatrix = matrices.slice(meshMatrixIndex, meshMatrixIndex + 16);

                    const geometryId = `${modelPartId}-geometry.${tileIndex}.${geometryIndex}`; // These IDs are local to the SceneModel

                    let geometryArrays = geometryArraysCache[geometryId];

                    if (!geometryArrays) {
                        geometryArrays = {
                            batchThisMesh: (!options.reuseGeometries)
                        };
                        const primitiveType = eachGeometryPrimitiveType[geometryIndex];
                        let geometryValid = false;
                        switch (primitiveType) {
                            case 0:
                                geometryArrays.primitiveName = "solid";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                                geometryArrays.geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                                geometryArrays.geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            case 1:
                                geometryArrays.primitiveName = "surface";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                                geometryArrays.geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                                geometryArrays.geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            case 2:
                                geometryArrays.primitiveName = "points";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryColors = colors.subarray(eachGeometryColorsPortion [geometryIndex], atLastGeometry ? colors.length : eachGeometryColorsPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0);
                                break;
                            case 3:
                                geometryArrays.primitiveName = "lines";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            default:
                                continue;
                        }

                        if (!geometryValid) {
                            geometryArrays = null;
                        }

                        if (geometryArrays) {
                            if (geometryArrays.geometryPositions.length > 1000) ;
                            if (geometryArrays.batchThisMesh) {
                                geometryArrays.decompressedPositions = new Float32Array(geometryArrays.geometryPositions.length);
                                geometryArrays.transformedAndRecompressedPositions = new Uint16Array(geometryArrays.geometryPositions.length);
                                const geometryPositions = geometryArrays.geometryPositions;
                                const decompressedPositions = geometryArrays.decompressedPositions;
                                for (let i = 0, len = geometryPositions.length; i < len; i += 3) {
                                    decompressedPositions[i + 0] = geometryPositions[i + 0] * reusedGeometriesDecodeMatrix[0] + reusedGeometriesDecodeMatrix[12];
                                    decompressedPositions[i + 1] = geometryPositions[i + 1] * reusedGeometriesDecodeMatrix[5] + reusedGeometriesDecodeMatrix[13];
                                    decompressedPositions[i + 2] = geometryPositions[i + 2] * reusedGeometriesDecodeMatrix[10] + reusedGeometriesDecodeMatrix[14];
                                }
                                geometryArrays.geometryPositions = null;
                                geometryArraysCache[geometryId] = geometryArrays;
                            }
                        }
                    }

                    if (geometryArrays) {

                        if (geometryArrays.batchThisMesh) {

                            const decompressedPositions = geometryArrays.decompressedPositions;
                            const transformedAndRecompressedPositions = geometryArrays.transformedAndRecompressedPositions;

                            for (let i = 0, len = decompressedPositions.length; i < len; i += 3) {
                                tempVec4a$3[0] = decompressedPositions[i + 0];
                                tempVec4a$3[1] = decompressedPositions[i + 1];
                                tempVec4a$3[2] = decompressedPositions[i + 2];
                                tempVec4a$3[3] = 1;
                                math.transformVec4(meshMatrix, tempVec4a$3, tempVec4b$3);
                                geometryCompressionUtils.compressPosition(tempVec4b$3, rtcAABB, tempVec4a$3);
                                transformedAndRecompressedPositions[i + 0] = tempVec4a$3[0];
                                transformedAndRecompressedPositions[i + 1] = tempVec4a$3[1];
                                transformedAndRecompressedPositions[i + 2] = tempVec4a$3[2];
                            }

                            sceneModel.createMesh(utils.apply(meshDefaults, {
                                id: meshId,
                                origin: tileCenter,
                                primitive: geometryArrays.primitiveName,
                                positionsCompressed: transformedAndRecompressedPositions,
                                normalsCompressed: geometryArrays.geometryNormals,
                                colorsCompressed: geometryArrays.geometryColors,
                                indices: geometryArrays.geometryIndices,
                                edgeIndices: geometryArrays.geometryEdgeIndices,
                                positionsDecodeMatrix: tileDecodeMatrix,
                                color: meshColor,
                                metallic: meshMetallic,
                                roughness: meshRoughness,
                                opacity: meshOpacity
                            }));

                            meshIds.push(meshId);

                        } else {

                            if (!geometryCreatedInTile[geometryId]) {

                                sceneModel.createGeometry({
                                    id: geometryId,
                                    primitive: geometryArrays.primitiveName,
                                    positionsCompressed: geometryArrays.geometryPositions,
                                    normalsCompressed: geometryArrays.geometryNormals,
                                    colorsCompressed: geometryArrays.geometryColors,
                                    indices: geometryArrays.geometryIndices,
                                    edgeIndices: geometryArrays.geometryEdgeIndices,
                                    positionsDecodeMatrix: reusedGeometriesDecodeMatrix
                                });

                                geometryCreatedInTile[geometryId] = true;
                            }

                            sceneModel.createMesh(utils.apply(meshDefaults, {
                                id: meshId,
                                geometryId: geometryId,
                                origin: tileCenter,
                                matrix: meshMatrix,
                                color: meshColor,
                                metallic: meshMetallic,
                                roughness: meshRoughness,
                                opacity: meshOpacity
                            }));

                            meshIds.push(meshId);
                        }
                    }

                } else {

                    const primitiveType = eachGeometryPrimitiveType[geometryIndex];

                    let primitiveName;
                    let geometryPositions;
                    let geometryNormals;
                    let geometryColors;
                    let geometryIndices;
                    let geometryEdgeIndices;
                    let geometryValid = false;

                    switch (primitiveType) {
                        case 0:
                            primitiveName = "solid";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                            geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                            geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                        case 1:
                            primitiveName = "surface";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                            geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                            geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                        case 2:
                            primitiveName = "points";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryColors = colors.subarray(eachGeometryColorsPortion [geometryIndex], atLastGeometry ? colors.length : eachGeometryColorsPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0);
                            break;
                        case 3:
                            primitiveName = "lines";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                        default:
                            continue;
                    }

                    if (geometryValid) {

                        sceneModel.createMesh(utils.apply(meshDefaults, {
                            id: meshId,
                            origin: tileCenter,
                            primitive: primitiveName,
                            positionsCompressed: geometryPositions,
                            normalsCompressed: geometryNormals,
                            colorsCompressed: geometryColors,
                            indices: geometryIndices,
                            edgeIndices: geometryEdgeIndices,
                            positionsDecodeMatrix: tileDecodeMatrix,
                            color: meshColor,
                            metallic: meshMetallic,
                            roughness: meshRoughness,
                            opacity: meshOpacity
                        }));

                        meshIds.push(meshId);
                    }
                }
            }

            if (meshIds.length > 0) {

                sceneModel.createEntity(utils.apply(entityDefaults, {
                    id: entityId,
                    isObject: true,
                    meshIds: meshIds
                }));
            }
        }
    }
}

/** @private */
const ParserV9 = {
    version: 9,
    parse: function (viewer, options, elements, sceneModel, metaModel, manifestCtx) {
        const deflatedData = extract$2(elements);
        const inflatedData = inflate$2(deflatedData);
        load$3(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx);
    }
};

/*
 Parser for .XKT Format V10
*/

let pako$1 = window.pako || p;
if (!pako$1.inflate) {  // See https://github.com/nodeca/pako/issues/97
    pako$1 = pako$1.default;
}

const tempVec4a$2 = math.vec4();
const tempVec4b$2 = math.vec4();

const NUM_TEXTURE_ATTRIBUTES$2 = 9;

function extract$1(elements) {

    let i = 0;

    return {
        metadata: elements[i++],
        textureData: elements[i++],
        eachTextureDataPortion: elements[i++],
        eachTextureAttributes: elements[i++],
        positions: elements[i++],
        normals: elements[i++],
        colors: elements[i++],
        uvs: elements[i++],
        indices: elements[i++],
        edgeIndices: elements[i++],
        eachTextureSetTextures: elements[i++],
        matrices: elements[i++],
        reusedGeometriesDecodeMatrix: elements[i++],
        eachGeometryPrimitiveType: elements[i++],
        eachGeometryPositionsPortion: elements[i++],
        eachGeometryNormalsPortion: elements[i++],
        eachGeometryColorsPortion: elements[i++],
        eachGeometryUVsPortion: elements[i++],
        eachGeometryIndicesPortion: elements[i++],
        eachGeometryEdgeIndicesPortion: elements[i++],
        eachMeshGeometriesPortion: elements[i++],
        eachMeshMatricesPortion: elements[i++],
        eachMeshTextureSet: elements[i++],
        eachMeshMaterialAttributes: elements[i++],
        eachEntityId: elements[i++],
        eachEntityMeshesPortion: elements[i++],
        eachTileAABB: elements[i++],
        eachTileEntitiesPortion: elements[i++]
    };
}

function inflate$1(deflatedData) {

    function inflate(array, options) {
        return (array.length === 0) ? [] : pako$1.inflate(array, options).buffer;
    }

    return {
        metadata: JSON.parse(pako$1.inflate(deflatedData.metadata, {to: 'string'})),
        textureData: new Uint8Array(inflate(deflatedData.textureData)),  // <<----------------------------- ??? ZIPPing to blame?
        eachTextureDataPortion: new Uint32Array(inflate(deflatedData.eachTextureDataPortion)),
        eachTextureAttributes: new Uint16Array(inflate(deflatedData.eachTextureAttributes)),
        positions: new Uint16Array(inflate(deflatedData.positions)),
        normals: new Int8Array(inflate(deflatedData.normals)),
        colors: new Uint8Array(inflate(deflatedData.colors)),
        uvs: new Float32Array(inflate(deflatedData.uvs)),
        indices: new Uint32Array(inflate(deflatedData.indices)),
        edgeIndices: new Uint32Array(inflate(deflatedData.edgeIndices)),
        eachTextureSetTextures: new Int32Array(inflate(deflatedData.eachTextureSetTextures)),
        matrices: new Float32Array(inflate(deflatedData.matrices)),
        reusedGeometriesDecodeMatrix: new Float32Array(inflate(deflatedData.reusedGeometriesDecodeMatrix)),
        eachGeometryPrimitiveType: new Uint8Array(inflate(deflatedData.eachGeometryPrimitiveType)),
        eachGeometryPositionsPortion: new Uint32Array(inflate(deflatedData.eachGeometryPositionsPortion)),
        eachGeometryNormalsPortion: new Uint32Array(inflate(deflatedData.eachGeometryNormalsPortion)),
        eachGeometryColorsPortion: new Uint32Array(inflate(deflatedData.eachGeometryColorsPortion)),
        eachGeometryUVsPortion: new Uint32Array(inflate(deflatedData.eachGeometryUVsPortion)),
        eachGeometryIndicesPortion: new Uint32Array(inflate(deflatedData.eachGeometryIndicesPortion)),
        eachGeometryEdgeIndicesPortion: new Uint32Array(inflate(deflatedData.eachGeometryEdgeIndicesPortion)),
        eachMeshGeometriesPortion: new Uint32Array(inflate(deflatedData.eachMeshGeometriesPortion)),
        eachMeshMatricesPortion: new Uint32Array(inflate(deflatedData.eachMeshMatricesPortion)),
        eachMeshTextureSet: new Int32Array(inflate(deflatedData.eachMeshTextureSet)), // Can be -1
        eachMeshMaterialAttributes: new Uint8Array(inflate(deflatedData.eachMeshMaterialAttributes)),
        eachEntityId: JSON.parse(pako$1.inflate(deflatedData.eachEntityId, {to: 'string'})),
        eachEntityMeshesPortion: new Uint32Array(inflate(deflatedData.eachEntityMeshesPortion)),
        eachTileAABB: new Float64Array(inflate(deflatedData.eachTileAABB)),
        eachTileEntitiesPortion: new Uint32Array(inflate(deflatedData.eachTileEntitiesPortion)),
    };
}

const decompressColor$2 = (function () {
    const floatColor = new Float32Array(3);
    return function (intColor) {
        floatColor[0] = intColor[0] / 255.0;
        floatColor[1] = intColor[1] / 255.0;
        floatColor[2] = intColor[2] / 255.0;
        return floatColor;
    };
})();

((function () {
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    return function (imagedata) {
        canvas.width = imagedata.width;
        canvas.height = imagedata.height;
        context.putImageData(imagedata, 0, 0);
        return canvas.toDataURL();
    };
}))();

function load$2(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx) {

    const modelPartId = manifestCtx.getNextId();

    const metadata = inflatedData.metadata;
    const textureData = inflatedData.textureData;
    const eachTextureDataPortion = inflatedData.eachTextureDataPortion;
    const eachTextureAttributes = inflatedData.eachTextureAttributes;
    const positions = inflatedData.positions;
    const normals = inflatedData.normals;
    const colors = inflatedData.colors;
    const uvs = inflatedData.uvs;
    const indices = inflatedData.indices;
    const edgeIndices = inflatedData.edgeIndices;
    const eachTextureSetTextures = inflatedData.eachTextureSetTextures;
    const matrices = inflatedData.matrices;
    const reusedGeometriesDecodeMatrix = inflatedData.reusedGeometriesDecodeMatrix;
    const eachGeometryPrimitiveType = inflatedData.eachGeometryPrimitiveType;
    const eachGeometryPositionsPortion = inflatedData.eachGeometryPositionsPortion;
    const eachGeometryNormalsPortion = inflatedData.eachGeometryNormalsPortion;
    const eachGeometryColorsPortion = inflatedData.eachGeometryColorsPortion;
    const eachGeometryUVsPortion = inflatedData.eachGeometryUVsPortion;
    const eachGeometryIndicesPortion = inflatedData.eachGeometryIndicesPortion;
    const eachGeometryEdgeIndicesPortion = inflatedData.eachGeometryEdgeIndicesPortion;
    const eachMeshGeometriesPortion = inflatedData.eachMeshGeometriesPortion;
    const eachMeshMatricesPortion = inflatedData.eachMeshMatricesPortion;
    const eachMeshTextureSet = inflatedData.eachMeshTextureSet;
    const eachMeshMaterialAttributes = inflatedData.eachMeshMaterialAttributes;
    const eachEntityId = inflatedData.eachEntityId;
    const eachEntityMeshesPortion = inflatedData.eachEntityMeshesPortion;
    const eachTileAABB = inflatedData.eachTileAABB;
    const eachTileEntitiesPortion = inflatedData.eachTileEntitiesPortion;

    const numTextures = eachTextureDataPortion.length;
    const numTextureSets = eachTextureSetTextures.length / 5;
    const numGeometries = eachGeometryPositionsPortion.length;
    const numMeshes = eachMeshGeometriesPortion.length;
    const numEntities = eachEntityMeshesPortion.length;
    const numTiles = eachTileEntitiesPortion.length;

    if (metaModel) {
        metaModel.loadData(metadata, {
            includeTypes: options.includeTypes,
            excludeTypes: options.excludeTypes,
            globalizeObjectIds: options.globalizeObjectIds
        }); // Can be empty
    }

    // Create textures

    for (let textureIndex = 0; textureIndex < numTextures; textureIndex++) {
        const atLastTexture = (textureIndex === (numTextures - 1));
        const textureDataPortionStart = eachTextureDataPortion[textureIndex];
        const textureDataPortionEnd = atLastTexture ? textureData.length : (eachTextureDataPortion[textureIndex + 1]);

        const textureDataPortionSize = textureDataPortionEnd - textureDataPortionStart;
        const textureDataPortionExists = (textureDataPortionSize > 0);

        const textureAttrBaseIdx = (textureIndex * NUM_TEXTURE_ATTRIBUTES$2);

        const compressed = (eachTextureAttributes[textureAttrBaseIdx + 0] === 1);
        const mediaType = eachTextureAttributes[textureAttrBaseIdx + 1];
        eachTextureAttributes[textureAttrBaseIdx + 2];
        eachTextureAttributes[textureAttrBaseIdx + 3];
        const minFilter = eachTextureAttributes[textureAttrBaseIdx + 4];
        const magFilter = eachTextureAttributes[textureAttrBaseIdx + 5]; // LinearFilter | NearestFilter
        const wrapS = eachTextureAttributes[textureAttrBaseIdx + 6]; // ClampToEdgeWrapping | MirroredRepeatWrapping | RepeatWrapping
        const wrapT = eachTextureAttributes[textureAttrBaseIdx + 7]; // ClampToEdgeWrapping | MirroredRepeatWrapping | RepeatWrapping
        const wrapR = eachTextureAttributes[textureAttrBaseIdx + 8]; // ClampToEdgeWrapping | MirroredRepeatWrapping | RepeatWrapping

        if (textureDataPortionExists) {

            const imageDataSubarray = new Uint8Array(textureData.subarray(textureDataPortionStart, textureDataPortionEnd));
            const arrayBuffer = imageDataSubarray.buffer;
            const textureId = `${modelPartId}-texture-${textureIndex}`;

            if (compressed) {

                sceneModel.createTexture({
                    id: textureId,
                    buffers: [arrayBuffer],
                    minFilter,
                    magFilter,
                    wrapS,
                    wrapT,
                    wrapR
                });

            } else {

                const mimeType = mediaType === JPEGMediaType ? "image/jpeg" : (mediaType === PNGMediaType ? "image/png" : "image/gif");
                const blob = new Blob([arrayBuffer], {type: mimeType});
                const urlCreator = window.URL || window.webkitURL;
                const imageUrl = urlCreator.createObjectURL(blob);
                const img = document.createElement('img');
                img.src = imageUrl;

                sceneModel.createTexture({
                    id: textureId,
                    image: img,
                    //mediaType,
                    minFilter,
                    magFilter,
                    wrapS,
                    wrapT,
                    wrapR
                });
            }
        }
    }

    // Create texture sets

    for (let textureSetIndex = 0; textureSetIndex < numTextureSets; textureSetIndex++) {
        const eachTextureSetTexturesIndex = textureSetIndex * 5;
        const textureSetId = `${modelPartId}-textureSet-${textureSetIndex}`;
        const colorTextureIndex = eachTextureSetTextures[eachTextureSetTexturesIndex + 0];
        const metallicRoughnessTextureIndex = eachTextureSetTextures[eachTextureSetTexturesIndex + 1];
        const normalsTextureIndex = eachTextureSetTextures[eachTextureSetTexturesIndex + 2];
        const emissiveTextureIndex = eachTextureSetTextures[eachTextureSetTexturesIndex + 3];
        const occlusionTextureIndex = eachTextureSetTextures[eachTextureSetTexturesIndex + 4];
        sceneModel.createTextureSet({
            id: textureSetId,
            colorTextureId: colorTextureIndex >= 0 ? `${modelPartId}-texture-${colorTextureIndex}` : null,
            normalsTextureId: normalsTextureIndex >= 0 ? `${modelPartId}-texture-${normalsTextureIndex}` : null,
            metallicRoughnessTextureId: metallicRoughnessTextureIndex >= 0 ? `${modelPartId}-texture-${metallicRoughnessTextureIndex}` : null,
            emissiveTextureId: emissiveTextureIndex >= 0 ? `${modelPartId}-texture-${emissiveTextureIndex}` : null,
            occlusionTextureId: occlusionTextureIndex >= 0 ? `${modelPartId}-texture-${occlusionTextureIndex}` : null
        });
    }

    // Count instances of each geometry

    const geometryReuseCounts = new Uint32Array(numGeometries);

    for (let meshIndex = 0; meshIndex < numMeshes; meshIndex++) {
        const geometryIndex = eachMeshGeometriesPortion[meshIndex];
        if (geometryReuseCounts[geometryIndex] !== undefined) {
            geometryReuseCounts[geometryIndex]++;
        } else {
            geometryReuseCounts[geometryIndex] = 1;
        }
    }

    // Iterate over tiles

    const tileCenter = math.vec3();
    const rtcAABB = math.AABB3();

    const geometryArraysCache = {};

    for (let tileIndex = 0; tileIndex < numTiles; tileIndex++) {

        const lastTileIndex = (numTiles - 1);

        const atLastTile = (tileIndex === lastTileIndex);

        const firstTileEntityIndex = eachTileEntitiesPortion [tileIndex];
        const lastTileEntityIndex = atLastTile ? (numEntities - 1) : (eachTileEntitiesPortion[tileIndex + 1] - 1);

        const tileAABBIndex = tileIndex * 6;
        const tileAABB = eachTileAABB.subarray(tileAABBIndex, tileAABBIndex + 6);

        math.getAABB3Center(tileAABB, tileCenter);

        rtcAABB[0] = tileAABB[0] - tileCenter[0];
        rtcAABB[1] = tileAABB[1] - tileCenter[1];
        rtcAABB[2] = tileAABB[2] - tileCenter[2];
        rtcAABB[3] = tileAABB[3] - tileCenter[0];
        rtcAABB[4] = tileAABB[4] - tileCenter[1];
        rtcAABB[5] = tileAABB[5] - tileCenter[2];

        const tileDecodeMatrix = geometryCompressionUtils.createPositionsDecodeMatrix(rtcAABB);

        const geometryCreatedInTile = {};

        // Iterate over each tile's entities

        for (let tileEntityIndex = firstTileEntityIndex; tileEntityIndex <= lastTileEntityIndex; tileEntityIndex++) {

            const xktEntityId = eachEntityId[tileEntityIndex];

            const entityId = options.globalizeObjectIds ? math.globalizeObjectId(sceneModel.id, xktEntityId) : xktEntityId;

            const finalTileEntityIndex = (numEntities - 1);
            const atLastTileEntity = (tileEntityIndex === finalTileEntityIndex);
            const firstMeshIndex = eachEntityMeshesPortion [tileEntityIndex];
            const lastMeshIndex = atLastTileEntity ? (eachMeshGeometriesPortion.length - 1) : (eachEntityMeshesPortion[tileEntityIndex + 1] - 1);

            const meshIds = [];

            const metaObject = viewer.metaScene.metaObjects[entityId];
            const entityDefaults = {};
            const meshDefaults = {};

            if (metaObject) {

                // Mask loading of object types

                if (options.excludeTypesMap && metaObject.type && options.excludeTypesMap[metaObject.type]) {
                    continue;
                }

                if (options.includeTypesMap && metaObject.type && (!options.includeTypesMap[metaObject.type])) {
                    continue;
                }

                // Mask loading of object ids

                if (options.includeIdsMap && metaObject.id && (!options.includeIdsMap[metaObject.id])) {
                    continue;
                }

                // Get initial property values for object types

                const props = options.objectDefaults ? options.objectDefaults[metaObject.type] || options.objectDefaults["DEFAULT"] : null;

                if (props) {
                    if (props.visible === false) {
                        entityDefaults.visible = false;
                    }
                    if (props.pickable === false) {
                        entityDefaults.pickable = false;
                    }
                    if (props.colorize) {
                        meshDefaults.color = props.colorize;
                    }
                    if (props.opacity !== undefined && props.opacity !== null) {
                        meshDefaults.opacity = props.opacity;
                    }
                    if (props.metallic !== undefined && props.metallic !== null) {
                        meshDefaults.metallic = props.metallic;
                    }
                    if (props.roughness !== undefined && props.roughness !== null) {
                        meshDefaults.roughness = props.roughness;
                    }
                }

            } else {
                if (options.excludeUnclassifiedObjects) {
                    continue;
                }
            }

            // Iterate each entity's meshes

            for (let meshIndex = firstMeshIndex; meshIndex <= lastMeshIndex; meshIndex++) {

                const geometryIndex = eachMeshGeometriesPortion[meshIndex];
                const geometryReuseCount = geometryReuseCounts[geometryIndex];
                const isReusedGeometry = (geometryReuseCount > 1);

                const atLastGeometry = (geometryIndex === (numGeometries - 1));

                const textureSetIndex = eachMeshTextureSet[meshIndex];

                const textureSetId = (textureSetIndex >= 0) ? `${modelPartId}-textureSet-${textureSetIndex}` : null;

                const meshColor = decompressColor$2(eachMeshMaterialAttributes.subarray((meshIndex * 6), (meshIndex * 6) + 3));
                const meshOpacity = eachMeshMaterialAttributes[(meshIndex * 6) + 3] / 255.0;
                const meshMetallic = eachMeshMaterialAttributes[(meshIndex * 6) + 4] / 255.0;
                const meshRoughness = eachMeshMaterialAttributes[(meshIndex * 6) + 5] / 255.0;

                const meshId = manifestCtx.getNextId();

                if (isReusedGeometry) {

                    // Create mesh for multi-use geometry - create (or reuse) geometry, create mesh using that geometry

                    const meshMatrixIndex = eachMeshMatricesPortion[meshIndex];
                    const meshMatrix = matrices.slice(meshMatrixIndex, meshMatrixIndex + 16);

                    const geometryId = `${modelPartId}-geometry.${tileIndex}.${geometryIndex}`; // These IDs are local to the SceneModel

                    let geometryArrays = geometryArraysCache[geometryId];

                    if (!geometryArrays) {
                        geometryArrays = {
                            batchThisMesh: (!options.reuseGeometries)
                        };
                        const primitiveType = eachGeometryPrimitiveType[geometryIndex];
                        let geometryValid = false;
                        switch (primitiveType) {
                            case 0:
                                geometryArrays.primitiveName = "solid";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                                geometryArrays.geometryUVs = uvs.subarray(eachGeometryUVsPortion [geometryIndex], atLastGeometry ? uvs.length : eachGeometryUVsPortion [geometryIndex + 1]);
                                geometryArrays.geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                                geometryArrays.geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            case 1:
                                geometryArrays.primitiveName = "surface";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                                geometryArrays.geometryUVs = uvs.subarray(eachGeometryUVsPortion [geometryIndex], atLastGeometry ? uvs.length : eachGeometryUVsPortion [geometryIndex + 1]);
                                geometryArrays.geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                                geometryArrays.geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            case 2:
                                geometryArrays.primitiveName = "points";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryColors = colors.subarray(eachGeometryColorsPortion [geometryIndex], atLastGeometry ? colors.length : eachGeometryColorsPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0);
                                break;
                            case 3:
                                geometryArrays.primitiveName = "lines";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            case 4:
                                geometryArrays.primitiveName = "lines";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryIndices = lineStripToLines$2(
                                    geometryArrays.geometryPositions,
                                    indices.subarray(eachGeometryIndicesPortion [geometryIndex],
                                        atLastGeometry
                                            ? indices.length
                                            : eachGeometryIndicesPortion [geometryIndex + 1]));
                                geometryValid = (geometryArrays.geometryPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            default:
                                continue;
                        }

                        if (!geometryValid) {
                            geometryArrays = null;
                        }

                        if (geometryArrays) {
                            if (geometryArrays.geometryPositions.length > 1000) ;
                            if (geometryArrays.batchThisMesh) {
                                geometryArrays.decompressedPositions = new Float32Array(geometryArrays.geometryPositions.length);
                                geometryArrays.transformedAndRecompressedPositions = new Uint16Array(geometryArrays.geometryPositions.length);
                                const geometryPositions = geometryArrays.geometryPositions;
                                const decompressedPositions = geometryArrays.decompressedPositions;
                                for (let i = 0, len = geometryPositions.length; i < len; i += 3) {
                                    decompressedPositions[i + 0] = geometryPositions[i + 0] * reusedGeometriesDecodeMatrix[0] + reusedGeometriesDecodeMatrix[12];
                                    decompressedPositions[i + 1] = geometryPositions[i + 1] * reusedGeometriesDecodeMatrix[5] + reusedGeometriesDecodeMatrix[13];
                                    decompressedPositions[i + 2] = geometryPositions[i + 2] * reusedGeometriesDecodeMatrix[10] + reusedGeometriesDecodeMatrix[14];
                                }
                                geometryArrays.geometryPositions = null;
                                geometryArraysCache[geometryId] = geometryArrays;
                            }
                        }
                    }

                    if (geometryArrays) {

                        if (geometryArrays.batchThisMesh) {

                            const decompressedPositions = geometryArrays.decompressedPositions;
                            const transformedAndRecompressedPositions = geometryArrays.transformedAndRecompressedPositions;

                            for (let i = 0, len = decompressedPositions.length; i < len; i += 3) {
                                tempVec4a$2[0] = decompressedPositions[i + 0];
                                tempVec4a$2[1] = decompressedPositions[i + 1];
                                tempVec4a$2[2] = decompressedPositions[i + 2];
                                tempVec4a$2[3] = 1;
                                math.transformVec4(meshMatrix, tempVec4a$2, tempVec4b$2);
                                geometryCompressionUtils.compressPosition(tempVec4b$2, rtcAABB, tempVec4a$2);
                                transformedAndRecompressedPositions[i + 0] = tempVec4a$2[0];
                                transformedAndRecompressedPositions[i + 1] = tempVec4a$2[1];
                                transformedAndRecompressedPositions[i + 2] = tempVec4a$2[2];
                            }

                            sceneModel.createMesh(utils.apply(meshDefaults, {
                                id: meshId,
                                textureSetId,
                                origin: tileCenter,
                                primitive: geometryArrays.primitiveName,
                                positionsCompressed: transformedAndRecompressedPositions,
                                normalsCompressed: geometryArrays.geometryNormals,
                                uv: geometryArrays.geometryUVs,
                                colorsCompressed: geometryArrays.geometryColors,
                                indices: geometryArrays.geometryIndices,
                                edgeIndices: geometryArrays.geometryEdgeIndices,
                                positionsDecodeMatrix: tileDecodeMatrix,
                                color: meshColor,
                                metallic: meshMetallic,
                                roughness: meshRoughness,
                                opacity: meshOpacity
                            }));

                            meshIds.push(meshId);

                        } else {

                            if (!geometryCreatedInTile[geometryId]) {

                                sceneModel.createGeometry({
                                    id: geometryId,
                                    primitive: geometryArrays.primitiveName,
                                    positionsCompressed: geometryArrays.geometryPositions,
                                    normalsCompressed: geometryArrays.geometryNormals,
                                    uv: geometryArrays.geometryUVs,
                                    colorsCompressed: geometryArrays.geometryColors,
                                    indices: geometryArrays.geometryIndices,
                                    edgeIndices: geometryArrays.geometryEdgeIndices,
                                    positionsDecodeMatrix: reusedGeometriesDecodeMatrix
                                });

                                geometryCreatedInTile[geometryId] = true;
                            }

                            sceneModel.createMesh(utils.apply(meshDefaults, {
                                id: meshId,
                                geometryId,
                                textureSetId,
                                matrix: meshMatrix,
                                color: meshColor,
                                metallic: meshMetallic,
                                roughness: meshRoughness,
                                opacity: meshOpacity,
                                origin: tileCenter
                            }));

                            meshIds.push(meshId);
                        }
                    }

                } else { // Do not reuse geometry

                    const primitiveType = eachGeometryPrimitiveType[geometryIndex];

                    let primitiveName;
                    let geometryPositions;
                    let geometryNormals;
                    let geometryUVs;
                    let geometryColors;
                    let geometryIndices;
                    let geometryEdgeIndices;
                    let geometryValid = false;

                    switch (primitiveType) {
                        case 0:
                            primitiveName = "solid";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                            geometryUVs = uvs.subarray(eachGeometryUVsPortion [geometryIndex], atLastGeometry ? uvs.length : eachGeometryUVsPortion [geometryIndex + 1]);
                            geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                            geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                        case 1:
                            primitiveName = "surface";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                            geometryUVs = uvs.subarray(eachGeometryUVsPortion [geometryIndex], atLastGeometry ? uvs.length : eachGeometryUVsPortion [geometryIndex + 1]);
                            geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                            geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                        case 2:
                            primitiveName = "points";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryColors = colors.subarray(eachGeometryColorsPortion [geometryIndex], atLastGeometry ? colors.length : eachGeometryColorsPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0);
                            break;
                        case 3:
                            primitiveName = "lines";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                        case 4:
                            primitiveName = "lines";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryIndices = lineStripToLines$2(
                                geometryPositions,
                                indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry
                                    ? indices.length
                                    : eachGeometryIndicesPortion [geometryIndex + 1]));
                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                        default:
                            continue;
                    }

                    if (geometryValid) {

                        sceneModel.createMesh(utils.apply(meshDefaults, {
                            id: meshId,
                            textureSetId,
                            origin: tileCenter,
                            primitive: primitiveName,
                            positionsCompressed: geometryPositions,
                            normalsCompressed: geometryNormals,
                            uv: geometryUVs && geometryUVs.length > 0 ? geometryUVs : null,
                            colorsCompressed: geometryColors,
                            indices: geometryIndices,
                            edgeIndices: geometryEdgeIndices,
                            positionsDecodeMatrix: tileDecodeMatrix,
                            color: meshColor,
                            metallic: meshMetallic,
                            roughness: meshRoughness,
                            opacity: meshOpacity
                        }));

                        meshIds.push(meshId);
                    }
                }
            }

            if (meshIds.length > 0) {

                sceneModel.createEntity(utils.apply(entityDefaults, {
                    id: entityId,
                    isObject: true,
                    meshIds: meshIds
                }));
            }
        }
    }
}

function lineStripToLines$2(positions, indices) {
    const linesIndices = [];
    if (indices.length > 1) {
        for (let i = 0, len = indices.length - 1; i < len; i++) {
            linesIndices.push(indices[i]);
            linesIndices.push(indices[i + 1]);
        }
    } else if (positions.length > 1) {
        for (let i = 0, len = (positions.length / 3) - 1; i < len; i++) {
            linesIndices.push(i);
            linesIndices.push(i + 1);
        }
    }
    return linesIndices;
}

/** @private */
const ParserV10 = {
    version: 10,
    parse: function (viewer, options, elements, sceneModel, metaModel, manifestCtx) {
        const deflatedData = extract$1(elements);
        const inflatedData = inflate$1(deflatedData);
        load$2(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx);
    }
};

/*
 Parser for .XKT Format V11
*/

const tempVec4a$1 = math.vec4();
const tempVec4b$1 = math.vec4();

const NUM_TEXTURE_ATTRIBUTES$1 = 9;

function decodeData$1(arrayBuffer) {
    const requiresSwapFromLittleEndian = (function() {
        const buffer = new ArrayBuffer(2);
        new Uint16Array(buffer)[0] = 1;
        return new Uint8Array(buffer)[0] !== 1;
    })();

    const nextArray = (function() {
        let i = 0;
        const dataView = new DataView(arrayBuffer);
        return function(type) {
            const idx = 1 + 2 * i++; // `1' for the version nr
            const byteOffset = dataView.getUint32(idx       * 4, true);
            const byteLength = dataView.getUint32((idx + 1) * 4, true);

            const BPE = type.BYTES_PER_ELEMENT;
            if (requiresSwapFromLittleEndian && (BPE > 1)) {
                const subarray = new Uint8Array(arrayBuffer, byteOffset, byteLength);
                const swaps = BPE / 2;
                const cnt = subarray.length / BPE;
                for (let b = 0; b < cnt; b++) {
                    const offset = b * BPE;
                    for (let j = 0; j < swaps; j++) {
                        const i1 = offset + j;
                        const i2 = offset - j + BPE - 1;
                        const tmp = subarray[i1];
                        subarray[i1] = subarray[i2];
                        subarray[i2] = tmp;
                    }
                }
            }

            return new type(arrayBuffer, byteOffset, byteLength / BPE);
        };
    })();

    const nextObject = (function() {
        const decoder = new TextDecoder();
        return () => JSON.parse(decoder.decode(nextArray(Uint8Array)));
    })();

    return {
        metadata:                       nextObject(),
        textureData:                    nextArray(Uint8Array),  // <<----------------------------- ??? ZIPPing to blame?
        eachTextureDataPortion:         nextArray(Uint32Array),
        eachTextureAttributes:          nextArray(Uint16Array),
        positions:                      nextArray(Uint16Array),
        normals:                        nextArray(Int8Array),
        colors:                         nextArray(Uint8Array),
        uvs:                            nextArray(Float32Array),
        indices:                        nextArray(Uint32Array),
        edgeIndices:                    nextArray(Uint32Array),
        eachTextureSetTextures:         nextArray(Int32Array),
        matrices:                       nextArray(Float32Array),
        reusedGeometriesDecodeMatrix:   nextArray(Float32Array),
        eachGeometryPrimitiveType:      nextArray(Uint8Array),
        eachGeometryPositionsPortion:   nextArray(Uint32Array),
        eachGeometryNormalsPortion:     nextArray(Uint32Array),
        eachGeometryColorsPortion:      nextArray(Uint32Array),
        eachGeometryUVsPortion:         nextArray(Uint32Array),
        eachGeometryIndicesPortion:     nextArray(Uint32Array),
        eachGeometryEdgeIndicesPortion: nextArray(Uint32Array),
        eachMeshGeometriesPortion:      nextArray(Uint32Array),
        eachMeshMatricesPortion:        nextArray(Uint32Array),
        eachMeshTextureSet:             nextArray(Int32Array), // Can be -1
        eachMeshMaterialAttributes:     nextArray(Uint8Array),
        eachEntityId:                   nextObject(),
        eachEntityMeshesPortion:        nextArray(Uint32Array),
        eachTileAABB:                   nextArray(Float64Array),
        eachTileEntitiesPortion:        nextArray(Uint32Array),
    };
}

const decompressColor$1 = (function () {
    const floatColor = new Float32Array(3);
    return function (intColor) {
        floatColor[0] = intColor[0] / 255.0;
        floatColor[1] = intColor[1] / 255.0;
        floatColor[2] = intColor[2] / 255.0;
        return floatColor;
    };
})();

((function () {
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    return function (imagedata) {
        canvas.width = imagedata.width;
        canvas.height = imagedata.height;
        context.putImageData(imagedata, 0, 0);
        return canvas.toDataURL();
    };
}))();

function load$1(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx) {

    const modelPartId = manifestCtx.getNextId();

    const metadata = inflatedData.metadata;
    const textureData = inflatedData.textureData;
    const eachTextureDataPortion = inflatedData.eachTextureDataPortion;
    const eachTextureAttributes = inflatedData.eachTextureAttributes;
    const positions = inflatedData.positions;
    const normals = inflatedData.normals;
    const colors = inflatedData.colors;
    const uvs = inflatedData.uvs;
    const indices = inflatedData.indices;
    const edgeIndices = inflatedData.edgeIndices;
    const eachTextureSetTextures = inflatedData.eachTextureSetTextures;
    const matrices = inflatedData.matrices;
    const reusedGeometriesDecodeMatrix = inflatedData.reusedGeometriesDecodeMatrix;
    const eachGeometryPrimitiveType = inflatedData.eachGeometryPrimitiveType;
    const eachGeometryPositionsPortion = inflatedData.eachGeometryPositionsPortion;
    const eachGeometryNormalsPortion = inflatedData.eachGeometryNormalsPortion;
    const eachGeometryColorsPortion = inflatedData.eachGeometryColorsPortion;
    const eachGeometryUVsPortion = inflatedData.eachGeometryUVsPortion;
    const eachGeometryIndicesPortion = inflatedData.eachGeometryIndicesPortion;
    const eachGeometryEdgeIndicesPortion = inflatedData.eachGeometryEdgeIndicesPortion;
    const eachMeshGeometriesPortion = inflatedData.eachMeshGeometriesPortion;
    const eachMeshMatricesPortion = inflatedData.eachMeshMatricesPortion;
    const eachMeshTextureSet = inflatedData.eachMeshTextureSet;
    const eachMeshMaterialAttributes = inflatedData.eachMeshMaterialAttributes;
    const eachEntityId = inflatedData.eachEntityId;
    const eachEntityMeshesPortion = inflatedData.eachEntityMeshesPortion;
    const eachTileAABB = inflatedData.eachTileAABB;
    const eachTileEntitiesPortion = inflatedData.eachTileEntitiesPortion;

    const numTextures = eachTextureDataPortion.length;
    const numTextureSets = eachTextureSetTextures.length / 5;
    const numGeometries = eachGeometryPositionsPortion.length;
    const numMeshes = eachMeshGeometriesPortion.length;
    const numEntities = eachEntityMeshesPortion.length;
    const numTiles = eachTileEntitiesPortion.length;

    if (metaModel) {
        metaModel.loadData(metadata, {
            includeTypes: options.includeTypes,
            excludeTypes: options.excludeTypes,
            globalizeObjectIds: options.globalizeObjectIds
        }); // Can be empty
    }

    // Create textures

    for (let textureIndex = 0; textureIndex < numTextures; textureIndex++) {
        const atLastTexture = (textureIndex === (numTextures - 1));
        const textureDataPortionStart = eachTextureDataPortion[textureIndex];
        const textureDataPortionEnd = atLastTexture ? textureData.length : (eachTextureDataPortion[textureIndex + 1]);

        const textureDataPortionSize = textureDataPortionEnd - textureDataPortionStart;
        const textureDataPortionExists = (textureDataPortionSize > 0);

        const textureAttrBaseIdx = (textureIndex * NUM_TEXTURE_ATTRIBUTES$1);

        const compressed = (eachTextureAttributes[textureAttrBaseIdx + 0] === 1);
        const mediaType = eachTextureAttributes[textureAttrBaseIdx + 1];
        eachTextureAttributes[textureAttrBaseIdx + 2];
        eachTextureAttributes[textureAttrBaseIdx + 3];
        const minFilter = eachTextureAttributes[textureAttrBaseIdx + 4];
        const magFilter = eachTextureAttributes[textureAttrBaseIdx + 5]; // LinearFilter | NearestFilter
        const wrapS = eachTextureAttributes[textureAttrBaseIdx + 6]; // ClampToEdgeWrapping | MirroredRepeatWrapping | RepeatWrapping
        const wrapT = eachTextureAttributes[textureAttrBaseIdx + 7]; // ClampToEdgeWrapping | MirroredRepeatWrapping | RepeatWrapping
        const wrapR = eachTextureAttributes[textureAttrBaseIdx + 8]; // ClampToEdgeWrapping | MirroredRepeatWrapping | RepeatWrapping

        if (textureDataPortionExists) {

            const imageDataSubarray = new Uint8Array(textureData.subarray(textureDataPortionStart, textureDataPortionEnd));
            const arrayBuffer = imageDataSubarray.buffer;
            const textureId = `${modelPartId}-texture-${textureIndex}`;

            if (compressed) {

                sceneModel.createTexture({
                    id: textureId,
                    buffers: [arrayBuffer],
                    minFilter,
                    magFilter,
                    wrapS,
                    wrapT,
                    wrapR
                });

            } else {

                const mimeType = mediaType === JPEGMediaType ? "image/jpeg" : (mediaType === PNGMediaType ? "image/png" : "image/gif");
                const blob = new Blob([arrayBuffer], {type: mimeType});
                const urlCreator = window.URL || window.webkitURL;
                const imageUrl = urlCreator.createObjectURL(blob);
                const img = document.createElement('img');
                img.src = imageUrl;

                sceneModel.createTexture({
                    id: textureId,
                    image: img,
                    //mediaType,
                    minFilter,
                    magFilter,
                    wrapS,
                    wrapT,
                    wrapR
                });
            }
        }
    }

    // Create texture sets

    for (let textureSetIndex = 0; textureSetIndex < numTextureSets; textureSetIndex++) {
        const eachTextureSetTexturesIndex = textureSetIndex * 5;
        const textureSetId = `${modelPartId}-textureSet-${textureSetIndex}`;
        const colorTextureIndex = eachTextureSetTextures[eachTextureSetTexturesIndex + 0];
        const metallicRoughnessTextureIndex = eachTextureSetTextures[eachTextureSetTexturesIndex + 1];
        const normalsTextureIndex = eachTextureSetTextures[eachTextureSetTexturesIndex + 2];
        const emissiveTextureIndex = eachTextureSetTextures[eachTextureSetTexturesIndex + 3];
        const occlusionTextureIndex = eachTextureSetTextures[eachTextureSetTexturesIndex + 4];
        sceneModel.createTextureSet({
            id: textureSetId,
            colorTextureId: colorTextureIndex >= 0 ? `${modelPartId}-texture-${colorTextureIndex}` : null,
            normalsTextureId: normalsTextureIndex >= 0 ? `${modelPartId}-texture-${normalsTextureIndex}` : null,
            metallicRoughnessTextureId: metallicRoughnessTextureIndex >= 0 ? `${modelPartId}-texture-${metallicRoughnessTextureIndex}` : null,
            emissiveTextureId: emissiveTextureIndex >= 0 ? `${modelPartId}-texture-${emissiveTextureIndex}` : null,
            occlusionTextureId: occlusionTextureIndex >= 0 ? `${modelPartId}-texture-${occlusionTextureIndex}` : null
        });
    }

    // Count instances of each geometry

    const geometryReuseCounts = new Uint32Array(numGeometries);

    for (let meshIndex = 0; meshIndex < numMeshes; meshIndex++) {
        const geometryIndex = eachMeshGeometriesPortion[meshIndex];
        if (geometryReuseCounts[geometryIndex] !== undefined) {
            geometryReuseCounts[geometryIndex]++;
        } else {
            geometryReuseCounts[geometryIndex] = 1;
        }
    }

    // Iterate over tiles

    const tileCenter = math.vec3();
    const rtcAABB = math.AABB3();

    const geometryArraysCache = {};

    for (let tileIndex = 0; tileIndex < numTiles; tileIndex++) {

        const lastTileIndex = (numTiles - 1);

        const atLastTile = (tileIndex === lastTileIndex);

        const firstTileEntityIndex = eachTileEntitiesPortion [tileIndex];
        const lastTileEntityIndex = atLastTile ? (numEntities - 1) : (eachTileEntitiesPortion[tileIndex + 1] - 1);

        const tileAABBIndex = tileIndex * 6;
        const tileAABB = eachTileAABB.subarray(tileAABBIndex, tileAABBIndex + 6);

        math.getAABB3Center(tileAABB, tileCenter);

        rtcAABB[0] = tileAABB[0] - tileCenter[0];
        rtcAABB[1] = tileAABB[1] - tileCenter[1];
        rtcAABB[2] = tileAABB[2] - tileCenter[2];
        rtcAABB[3] = tileAABB[3] - tileCenter[0];
        rtcAABB[4] = tileAABB[4] - tileCenter[1];
        rtcAABB[5] = tileAABB[5] - tileCenter[2];

        const tileDecodeMatrix = geometryCompressionUtils.createPositionsDecodeMatrix(rtcAABB);

        const geometryCreatedInTile = {};

        // Iterate over each tile's entities

        for (let tileEntityIndex = firstTileEntityIndex; tileEntityIndex <= lastTileEntityIndex; tileEntityIndex++) {

            const xktEntityId = eachEntityId[tileEntityIndex];

            const entityId = options.globalizeObjectIds ? math.globalizeObjectId(sceneModel.id, xktEntityId) : xktEntityId;

            const finalTileEntityIndex = (numEntities - 1);
            const atLastTileEntity = (tileEntityIndex === finalTileEntityIndex);
            const firstMeshIndex = eachEntityMeshesPortion [tileEntityIndex];
            const lastMeshIndex = atLastTileEntity ? (eachMeshGeometriesPortion.length - 1) : (eachEntityMeshesPortion[tileEntityIndex + 1] - 1);

            const meshIds = [];

            const metaObject = viewer.metaScene.metaObjects[entityId];
            const entityDefaults = {};
            const meshDefaults = {};

            if (metaObject) {

                // Mask loading of object types

                if (options.excludeTypesMap && metaObject.type && options.excludeTypesMap[metaObject.type]) {
                    continue;
                }

                if (options.includeTypesMap && metaObject.type && (!options.includeTypesMap[metaObject.type])) {
                    continue;
                }

                // Mask loading of object ids

                if (options.includeIdsMap && metaObject.id && (!options.includeIdsMap[metaObject.id])) {
                    continue;
                }

                // Get initial property values for object types

                const props = options.objectDefaults ? options.objectDefaults[metaObject.type] || options.objectDefaults["DEFAULT"] : null;

                if (props) {
                    if (props.visible === false) {
                        entityDefaults.visible = false;
                    }
                    if (props.pickable === false) {
                        entityDefaults.pickable = false;
                    }
                    if (props.colorize) {
                        meshDefaults.color = props.colorize;
                    }
                    if (props.opacity !== undefined && props.opacity !== null) {
                        meshDefaults.opacity = props.opacity;
                    }
                    if (props.metallic !== undefined && props.metallic !== null) {
                        meshDefaults.metallic = props.metallic;
                    }
                    if (props.roughness !== undefined && props.roughness !== null) {
                        meshDefaults.roughness = props.roughness;
                    }
                }

            } else {
                if (options.excludeUnclassifiedObjects) {
                    continue;
                }
            }

            // Iterate each entity's meshes

            for (let meshIndex = firstMeshIndex; meshIndex <= lastMeshIndex; meshIndex++) {

                const geometryIndex = eachMeshGeometriesPortion[meshIndex];
                const geometryReuseCount = geometryReuseCounts[geometryIndex];
                const isReusedGeometry = (geometryReuseCount > 1);

                const atLastGeometry = (geometryIndex === (numGeometries - 1));

                const textureSetIndex = eachMeshTextureSet[meshIndex];

                const textureSetId = (textureSetIndex >= 0) ? `${modelPartId}-textureSet-${textureSetIndex}` : null;

                const meshColor = decompressColor$1(eachMeshMaterialAttributes.subarray((meshIndex * 6), (meshIndex * 6) + 3));
                const meshOpacity = eachMeshMaterialAttributes[(meshIndex * 6) + 3] / 255.0;
                const meshMetallic = eachMeshMaterialAttributes[(meshIndex * 6) + 4] / 255.0;
                const meshRoughness = eachMeshMaterialAttributes[(meshIndex * 6) + 5] / 255.0;

                const meshId = manifestCtx.getNextId();

                if (isReusedGeometry) {

                    // Create mesh for multi-use geometry - create (or reuse) geometry, create mesh using that geometry

                    const meshMatrixIndex = eachMeshMatricesPortion[meshIndex];
                    const meshMatrix = matrices.slice(meshMatrixIndex, meshMatrixIndex + 16);

                    const geometryId = `${modelPartId}-geometry.${tileIndex}.${geometryIndex}`; // These IDs are local to the SceneModel

                    let geometryArrays = geometryArraysCache[geometryId];

                    if (!geometryArrays) {
                        geometryArrays = {
                            batchThisMesh: (!options.reuseGeometries)
                        };
                        const primitiveType = eachGeometryPrimitiveType[geometryIndex];
                        let geometryValid = false;
                        switch (primitiveType) {
                            case 0:
                                geometryArrays.primitiveName = "solid";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                                geometryArrays.geometryUVs = uvs.subarray(eachGeometryUVsPortion [geometryIndex], atLastGeometry ? uvs.length : eachGeometryUVsPortion [geometryIndex + 1]);
                                geometryArrays.geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                                geometryArrays.geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            case 1:
                                geometryArrays.primitiveName = "surface";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                                geometryArrays.geometryUVs = uvs.subarray(eachGeometryUVsPortion [geometryIndex], atLastGeometry ? uvs.length : eachGeometryUVsPortion [geometryIndex + 1]);
                                geometryArrays.geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                                geometryArrays.geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            case 2:
                                geometryArrays.primitiveName = "points";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryColors = colors.subarray(eachGeometryColorsPortion [geometryIndex], atLastGeometry ? colors.length : eachGeometryColorsPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0);
                                break;
                            case 3:
                                geometryArrays.primitiveName = "lines";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            case 4:
                                geometryArrays.primitiveName = "lines";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryIndices = lineStripToLines$1(
                                    geometryArrays.geometryPositions,
                                    indices.subarray(eachGeometryIndicesPortion [geometryIndex],
                                        atLastGeometry
                                            ? indices.length
                                            : eachGeometryIndicesPortion [geometryIndex + 1]));
                                geometryValid = (geometryArrays.geometryPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            default:
                                continue;
                        }

                        if (!geometryValid) {
                            geometryArrays = null;
                        }

                        if (geometryArrays) {
                            if (geometryArrays.geometryPositions.length > 1000) ;
                            if (geometryArrays.batchThisMesh) {
                                geometryArrays.decompressedPositions = new Float32Array(geometryArrays.geometryPositions.length);
                                geometryArrays.transformedAndRecompressedPositions = new Uint16Array(geometryArrays.geometryPositions.length);
                                const geometryPositions = geometryArrays.geometryPositions;
                                const decompressedPositions = geometryArrays.decompressedPositions;
                                for (let i = 0, len = geometryPositions.length; i < len; i += 3) {
                                    decompressedPositions[i + 0] = geometryPositions[i + 0] * reusedGeometriesDecodeMatrix[0] + reusedGeometriesDecodeMatrix[12];
                                    decompressedPositions[i + 1] = geometryPositions[i + 1] * reusedGeometriesDecodeMatrix[5] + reusedGeometriesDecodeMatrix[13];
                                    decompressedPositions[i + 2] = geometryPositions[i + 2] * reusedGeometriesDecodeMatrix[10] + reusedGeometriesDecodeMatrix[14];
                                }
                                geometryArrays.geometryPositions = null;
                                geometryArraysCache[geometryId] = geometryArrays;
                            }
                        }
                    }

                    if (geometryArrays) {

                        if (geometryArrays.batchThisMesh) {

                            const decompressedPositions = geometryArrays.decompressedPositions;
                            const transformedAndRecompressedPositions = geometryArrays.transformedAndRecompressedPositions;

                            for (let i = 0, len = decompressedPositions.length; i < len; i += 3) {
                                tempVec4a$1[0] = decompressedPositions[i + 0];
                                tempVec4a$1[1] = decompressedPositions[i + 1];
                                tempVec4a$1[2] = decompressedPositions[i + 2];
                                tempVec4a$1[3] = 1;
                                math.transformVec4(meshMatrix, tempVec4a$1, tempVec4b$1);
                                geometryCompressionUtils.compressPosition(tempVec4b$1, rtcAABB, tempVec4a$1);
                                transformedAndRecompressedPositions[i + 0] = tempVec4a$1[0];
                                transformedAndRecompressedPositions[i + 1] = tempVec4a$1[1];
                                transformedAndRecompressedPositions[i + 2] = tempVec4a$1[2];
                            }

                            sceneModel.createMesh(utils.apply(meshDefaults, {
                                id: meshId,
                                textureSetId,
                                origin: tileCenter,
                                primitive: geometryArrays.primitiveName,
                                positionsCompressed: transformedAndRecompressedPositions,
                                normalsCompressed: geometryArrays.geometryNormals,
                                uv: geometryArrays.geometryUVs,
                                colorsCompressed: geometryArrays.geometryColors,
                                indices: geometryArrays.geometryIndices,
                                edgeIndices: geometryArrays.geometryEdgeIndices,
                                positionsDecodeMatrix: tileDecodeMatrix,
                                color: meshColor,
                                metallic: meshMetallic,
                                roughness: meshRoughness,
                                opacity: meshOpacity
                            }));

                            meshIds.push(meshId);

                        } else {

                            if (!geometryCreatedInTile[geometryId]) {

                                sceneModel.createGeometry({
                                    id: geometryId,
                                    primitive: geometryArrays.primitiveName,
                                    positionsCompressed: geometryArrays.geometryPositions,
                                    normalsCompressed: geometryArrays.geometryNormals,
                                    uv: geometryArrays.geometryUVs,
                                    colorsCompressed: geometryArrays.geometryColors,
                                    indices: geometryArrays.geometryIndices,
                                    edgeIndices: geometryArrays.geometryEdgeIndices,
                                    positionsDecodeMatrix: reusedGeometriesDecodeMatrix
                                });

                                geometryCreatedInTile[geometryId] = true;
                            }

                            sceneModel.createMesh(utils.apply(meshDefaults, {
                                id: meshId,
                                geometryId,
                                textureSetId,
                                matrix: meshMatrix,
                                color: meshColor,
                                metallic: meshMetallic,
                                roughness: meshRoughness,
                                opacity: meshOpacity,
                                origin: tileCenter
                            }));

                            meshIds.push(meshId);
                        }
                    }

                } else { // Do not reuse geometry

                    const primitiveType = eachGeometryPrimitiveType[geometryIndex];

                    let primitiveName;
                    let geometryPositions;
                    let geometryNormals;
                    let geometryUVs;
                    let geometryColors;
                    let geometryIndices;
                    let geometryEdgeIndices;
                    let geometryValid = false;

                    switch (primitiveType) {
                        case 0:
                            primitiveName = "solid";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                            geometryUVs = uvs.subarray(eachGeometryUVsPortion [geometryIndex], atLastGeometry ? uvs.length : eachGeometryUVsPortion [geometryIndex + 1]);
                            geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                            geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                        case 1:
                            primitiveName = "surface";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                            geometryUVs = uvs.subarray(eachGeometryUVsPortion [geometryIndex], atLastGeometry ? uvs.length : eachGeometryUVsPortion [geometryIndex + 1]);
                            geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                            geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                        case 2:
                            primitiveName = "points";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryColors = colors.subarray(eachGeometryColorsPortion [geometryIndex], atLastGeometry ? colors.length : eachGeometryColorsPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0);
                            break;
                        case 3:
                            primitiveName = "lines";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                        case 4:
                            primitiveName = "lines";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryIndices = lineStripToLines$1(
                                geometryPositions,
                                indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry
                                    ? indices.length
                                    : eachGeometryIndicesPortion [geometryIndex + 1]));
                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                        default:
                            continue;
                    }

                    if (geometryValid) {

                        sceneModel.createMesh(utils.apply(meshDefaults, {
                            id: meshId,
                            textureSetId,
                            origin: tileCenter,
                            primitive: primitiveName,
                            positionsCompressed: geometryPositions,
                            normalsCompressed: geometryNormals,
                            uv: geometryUVs && geometryUVs.length > 0 ? geometryUVs : null,
                            colorsCompressed: geometryColors,
                            indices: geometryIndices,
                            edgeIndices: geometryEdgeIndices,
                            positionsDecodeMatrix: tileDecodeMatrix,
                            color: meshColor,
                            metallic: meshMetallic,
                            roughness: meshRoughness,
                            opacity: meshOpacity
                        }));

                        meshIds.push(meshId);
                    }
                }
            }

            if (meshIds.length > 0) {

                sceneModel.createEntity(utils.apply(entityDefaults, {
                    id: entityId,
                    isObject: true,
                    meshIds: meshIds
                }));
            }
        }
    }
}

function lineStripToLines$1(positions, indices) {
    const linesIndices = [];
    if (indices.length > 1) {
        for (let i = 0, len = indices.length - 1; i < len; i++) {
            linesIndices.push(indices[i]);
            linesIndices.push(indices[i + 1]);
        }
    } else if (positions.length > 1) {
        for (let i = 0, len = (positions.length / 3) - 1; i < len; i++) {
            linesIndices.push(i);
            linesIndices.push(i + 1);
        }
    }
    return linesIndices;
}

/** @private */
// V11 uses a single uncompressed Uint8Array buffer to store arrays of different types.
// To efficiently create typed arrays from this buffer,
// each typed array's source data needs to be aligned with its element byte size.
// This sometimes requires padding subarrays inside the single Uint8Array, so the byteOffset needs to be stored alongside element count.
// It is a different encoding than used in earlier versions, and requires different approach to parsing elements.
const ParserV11 = {
    version: 11,
    parseArrayBuffer: function (viewer, options, arrayBuffer, sceneModel, metaModel, manifestCtx) {
        const inflatedData = decodeData$1(arrayBuffer);
        load$1(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx);
    }
};

/*
 Parser for .XKT Format V11
*/

let pako = window.pako || p;
if (!pako.inflate) {  // See https://github.com/nodeca/pako/issues/97
    pako = pako.default;
}

const tempVec4a = math.vec4();
const tempVec4b = math.vec4();

const NUM_TEXTURE_ATTRIBUTES = 9;

function decodeData(arrayBuffer) {
    const requiresSwapFromLittleEndian = (function() {
        const buffer = new ArrayBuffer(2);
        new Uint16Array(buffer)[0] = 1;
        return new Uint8Array(buffer)[0] !== 1;
    })();

    const nextArray = (function() {
        let i = 0;
        const dataView = new DataView(arrayBuffer);
        return function(type) {
            const idx = 1 + 2 * i++; // `1' for the version nr
            const byteOffset = dataView.getUint32(idx       * 4, true);
            const byteLength = dataView.getUint32((idx + 1) * 4, true);

            const BPE = type.BYTES_PER_ELEMENT;
            if (requiresSwapFromLittleEndian && (BPE > 1)) {
                const subarray = new Uint8Array(arrayBuffer, byteOffset, byteLength);
                const swaps = BPE / 2;
                const cnt = subarray.length / BPE;
                for (let b = 0; b < cnt; b++) {
                    const offset = b * BPE;
                    for (let j = 0; j < swaps; j++) {
                        const i1 = offset + j;
                        const i2 = offset - j + BPE - 1;
                        const tmp = subarray[i1];
                        subarray[i1] = subarray[i2];
                        subarray[i2] = tmp;
                    }
                }
            }

            return new type(arrayBuffer, byteOffset, byteLength / BPE);
        };
    })();

    const nextObject = (function() {
        const decoder = new TextDecoder();
        return () => JSON.parse(decoder.decode(nextArray(Uint8Array)));
    })();

    return {
        metadata:                       nextObject(),
        textureData:                    nextArray(Uint8Array),  // <<----------------------------- ??? ZIPPing to blame?
        eachTextureDataPortion:         nextArray(Uint32Array),
        eachTextureAttributes:          nextArray(Uint16Array),
        positions:                      nextArray(Uint16Array),
        normals:                        nextArray(Int8Array),
        colors:                         nextArray(Uint8Array),
        uvs:                            nextArray(Float32Array),
        indices:                        nextArray(Uint32Array),
        edgeIndices:                    nextArray(Uint32Array),
        eachTextureSetTextures:         nextArray(Int32Array),
        matrices:                       nextArray(Float32Array),
        reusedGeometriesDecodeMatrix:   nextArray(Float32Array),
        eachGeometryPrimitiveType:      nextArray(Uint8Array),
        eachGeometryAxisLabel:          nextObject(),
        eachGeometryPositionsPortion:   nextArray(Uint32Array),
        eachGeometryNormalsPortion:     nextArray(Uint32Array),
        eachGeometryColorsPortion:      nextArray(Uint32Array),
        eachGeometryUVsPortion:         nextArray(Uint32Array),
        eachGeometryIndicesPortion:     nextArray(Uint32Array),
        eachGeometryEdgeIndicesPortion: nextArray(Uint32Array),
        eachMeshGeometriesPortion:      nextArray(Uint32Array),
        eachMeshMatricesPortion:        nextArray(Uint32Array),
        eachMeshTextureSet:             nextArray(Int32Array), // Can be -1
        eachMeshMaterialAttributes:     nextArray(Uint8Array),
        eachEntityId:                   nextObject(),
        eachEntityMeshesPortion:        nextArray(Uint32Array),
        eachTileAABB:                   nextArray(Float64Array),
        eachTileEntitiesPortion:        nextArray(Uint32Array),
    };
}

function extract(elements) {

    let i = 0;

    return {
        metadata: elements[i++],
        textureData: elements[i++],
        eachTextureDataPortion: elements[i++],
        eachTextureAttributes: elements[i++],
        positions: elements[i++],
        normals: elements[i++],
        colors: elements[i++],
        uvs: elements[i++],
        indices: elements[i++],
        edgeIndices: elements[i++],
        eachTextureSetTextures: elements[i++],
        matrices: elements[i++],
        reusedGeometriesDecodeMatrix: elements[i++],
        eachGeometryPrimitiveType: elements[i++],
        eachGeometryAxisLabel: elements[i++],
        eachGeometryPositionsPortion: elements[i++],
        eachGeometryNormalsPortion: elements[i++],
        eachGeometryColorsPortion: elements[i++],
        eachGeometryUVsPortion: elements[i++],
        eachGeometryIndicesPortion: elements[i++],
        eachGeometryEdgeIndicesPortion: elements[i++],
        eachMeshGeometriesPortion: elements[i++],
        eachMeshMatricesPortion: elements[i++],
        eachMeshTextureSet: elements[i++],
        eachMeshMaterialAttributes: elements[i++],
        eachEntityId: elements[i++],
        eachEntityMeshesPortion: elements[i++],
        eachTileAABB: elements[i++],
        eachTileEntitiesPortion: elements[i++],
    };
}

function inflate(deflatedData) {

    function inflate(array, options) {
        return (array.length === 0) ? [] : pako.inflate(array, options).buffer;
    }

    return {
        metadata: JSON.parse(pako.inflate(deflatedData.metadata, {to: 'string'})),
        textureData: new Uint8Array(inflate(deflatedData.textureData)),  // <<----------------------------- ??? ZIPPing to blame?
        eachTextureDataPortion: new Uint32Array(inflate(deflatedData.eachTextureDataPortion)),
        eachTextureAttributes: new Uint16Array(inflate(deflatedData.eachTextureAttributes)),
        positions: new Uint16Array(inflate(deflatedData.positions)),
        normals: new Int8Array(inflate(deflatedData.normals)),
        colors: new Uint8Array(inflate(deflatedData.colors)),
        uvs: new Float32Array(inflate(deflatedData.uvs)),
        indices: new Uint32Array(inflate(deflatedData.indices)),
        edgeIndices: new Uint32Array(inflate(deflatedData.edgeIndices)),
        eachTextureSetTextures: new Int32Array(inflate(deflatedData.eachTextureSetTextures)),
        matrices: new Float32Array(inflate(deflatedData.matrices)),
        reusedGeometriesDecodeMatrix: new Float32Array(inflate(deflatedData.reusedGeometriesDecodeMatrix)),
        eachGeometryPrimitiveType: new Uint8Array(inflate(deflatedData.eachGeometryPrimitiveType)),
        eachGeometryAxisLabel: JSON.parse(pako.inflate(deflatedData.eachGeometryAxisLabel, {to: 'string'})),
        eachGeometryPositionsPortion: new Uint32Array(inflate(deflatedData.eachGeometryPositionsPortion)),
        eachGeometryNormalsPortion: new Uint32Array(inflate(deflatedData.eachGeometryNormalsPortion)),
        eachGeometryColorsPortion: new Uint32Array(inflate(deflatedData.eachGeometryColorsPortion)),
        eachGeometryUVsPortion: new Uint32Array(inflate(deflatedData.eachGeometryUVsPortion)),
        eachGeometryIndicesPortion: new Uint32Array(inflate(deflatedData.eachGeometryIndicesPortion)),
        eachGeometryEdgeIndicesPortion: new Uint32Array(inflate(deflatedData.eachGeometryEdgeIndicesPortion)),
        eachMeshGeometriesPortion: new Uint32Array(inflate(deflatedData.eachMeshGeometriesPortion)),
        eachMeshMatricesPortion: new Uint32Array(inflate(deflatedData.eachMeshMatricesPortion)),
        eachMeshTextureSet: new Int32Array(inflate(deflatedData.eachMeshTextureSet)), // Can be -1
        eachMeshMaterialAttributes: new Uint8Array(inflate(deflatedData.eachMeshMaterialAttributes)),
        eachEntityId: JSON.parse(pako.inflate(deflatedData.eachEntityId, {to: 'string'})),
        eachEntityMeshesPortion: new Uint32Array(inflate(deflatedData.eachEntityMeshesPortion)),
        eachTileAABB: new Float64Array(inflate(deflatedData.eachTileAABB)),
        eachTileEntitiesPortion: new Uint32Array(inflate(deflatedData.eachTileEntitiesPortion)),
    };
}

const decompressColor = (function () {
    const floatColor = new Float32Array(3);
    return function (intColor) {
        floatColor[0] = intColor[0] / 255.0;
        floatColor[1] = intColor[1] / 255.0;
        floatColor[2] = intColor[2] / 255.0;
        return floatColor;
    };
})();

((function () {
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    return function (imagedata) {
        canvas.width = imagedata.width;
        canvas.height = imagedata.height;
        context.putImageData(imagedata, 0, 0);
        return canvas.toDataURL();
    };
}))();

function load(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx) {

    const modelPartId = manifestCtx.getNextId();

    const metadata = inflatedData.metadata;
    const textureData = inflatedData.textureData;
    const eachTextureDataPortion = inflatedData.eachTextureDataPortion;
    const eachTextureAttributes = inflatedData.eachTextureAttributes;
    const positions = inflatedData.positions;
    const normals = inflatedData.normals;
    const colors = inflatedData.colors;
    const uvs = inflatedData.uvs;
    const indices = inflatedData.indices;
    const edgeIndices = inflatedData.edgeIndices;
    const eachTextureSetTextures = inflatedData.eachTextureSetTextures;
    const matrices = inflatedData.matrices;
    const reusedGeometriesDecodeMatrix = inflatedData.reusedGeometriesDecodeMatrix;
    const eachGeometryPrimitiveType = inflatedData.eachGeometryPrimitiveType;
    const eachGeometryAxisLabel = inflatedData.eachGeometryAxisLabel;
    const eachGeometryPositionsPortion = inflatedData.eachGeometryPositionsPortion;
    const eachGeometryNormalsPortion = inflatedData.eachGeometryNormalsPortion;
    const eachGeometryColorsPortion = inflatedData.eachGeometryColorsPortion;
    const eachGeometryUVsPortion = inflatedData.eachGeometryUVsPortion;
    const eachGeometryIndicesPortion = inflatedData.eachGeometryIndicesPortion;
    const eachGeometryEdgeIndicesPortion = inflatedData.eachGeometryEdgeIndicesPortion;
    const eachMeshGeometriesPortion = inflatedData.eachMeshGeometriesPortion;
    const eachMeshMatricesPortion = inflatedData.eachMeshMatricesPortion;
    const eachMeshTextureSet = inflatedData.eachMeshTextureSet;
    const eachMeshMaterialAttributes = inflatedData.eachMeshMaterialAttributes;
    const eachEntityId = inflatedData.eachEntityId;
    const eachEntityMeshesPortion = inflatedData.eachEntityMeshesPortion;
    const eachTileAABB = inflatedData.eachTileAABB;
    const eachTileEntitiesPortion = inflatedData.eachTileEntitiesPortion;

    const numTextures = eachTextureDataPortion.length;
    const numTextureSets = eachTextureSetTextures.length / 5;
    const numGeometries = eachGeometryPositionsPortion.length;
    const numMeshes = eachMeshGeometriesPortion.length;
    const numEntities = eachEntityMeshesPortion.length;
    const numTiles = eachTileEntitiesPortion.length;

    if (metaModel) {
        metaModel.loadData(metadata, {
            includeTypes: options.includeTypes,
            excludeTypes: options.excludeTypes,
            globalizeObjectIds: options.globalizeObjectIds
        }); // Can be empty
    }

    // Create textures

    for (let textureIndex = 0; textureIndex < numTextures; textureIndex++) {
        const atLastTexture = (textureIndex === (numTextures - 1));
        const textureDataPortionStart = eachTextureDataPortion[textureIndex];
        const textureDataPortionEnd = atLastTexture ? textureData.length : (eachTextureDataPortion[textureIndex + 1]);

        const textureDataPortionSize = textureDataPortionEnd - textureDataPortionStart;
        const textureDataPortionExists = (textureDataPortionSize > 0);

        const textureAttrBaseIdx = (textureIndex * NUM_TEXTURE_ATTRIBUTES);

        const compressed = (eachTextureAttributes[textureAttrBaseIdx + 0] === 1);
        const mediaType = eachTextureAttributes[textureAttrBaseIdx + 1];
        eachTextureAttributes[textureAttrBaseIdx + 2];
        eachTextureAttributes[textureAttrBaseIdx + 3];
        const minFilter = eachTextureAttributes[textureAttrBaseIdx + 4];
        const magFilter = eachTextureAttributes[textureAttrBaseIdx + 5]; // LinearFilter | NearestFilter
        const wrapS = eachTextureAttributes[textureAttrBaseIdx + 6]; // ClampToEdgeWrapping | MirroredRepeatWrapping | RepeatWrapping
        const wrapT = eachTextureAttributes[textureAttrBaseIdx + 7]; // ClampToEdgeWrapping | MirroredRepeatWrapping | RepeatWrapping
        const wrapR = eachTextureAttributes[textureAttrBaseIdx + 8]; // ClampToEdgeWrapping | MirroredRepeatWrapping | RepeatWrapping

        if (textureDataPortionExists) {

            const imageDataSubarray = new Uint8Array(textureData.subarray(textureDataPortionStart, textureDataPortionEnd));
            const arrayBuffer = imageDataSubarray.buffer;
            const textureId = `${modelPartId}-texture-${textureIndex}`;

            if (compressed) {

                sceneModel.createTexture({
                    id: textureId,
                    buffers: [arrayBuffer],
                    minFilter,
                    magFilter,
                    wrapS,
                    wrapT,
                    wrapR
                });

            } else {

                const mimeType = mediaType === JPEGMediaType ? "image/jpeg" : (mediaType === PNGMediaType ? "image/png" : "image/gif");
                const blob = new Blob([arrayBuffer], {type: mimeType});
                const urlCreator = window.URL || window.webkitURL;
                const imageUrl = urlCreator.createObjectURL(blob);
                const img = document.createElement('img');
                img.src = imageUrl;

                sceneModel.createTexture({
                    id: textureId,
                    image: img,
                    //mediaType,
                    minFilter,
                    magFilter,
                    wrapS,
                    wrapT,
                    wrapR
                });
            }
        }
    }

    // Create texture sets

    for (let textureSetIndex = 0; textureSetIndex < numTextureSets; textureSetIndex++) {
        const eachTextureSetTexturesIndex = textureSetIndex * 5;
        const textureSetId = `${modelPartId}-textureSet-${textureSetIndex}`;
        const colorTextureIndex = eachTextureSetTextures[eachTextureSetTexturesIndex + 0];
        const metallicRoughnessTextureIndex = eachTextureSetTextures[eachTextureSetTexturesIndex + 1];
        const normalsTextureIndex = eachTextureSetTextures[eachTextureSetTexturesIndex + 2];
        const emissiveTextureIndex = eachTextureSetTextures[eachTextureSetTexturesIndex + 3];
        const occlusionTextureIndex = eachTextureSetTextures[eachTextureSetTexturesIndex + 4];
        sceneModel.createTextureSet({
            id: textureSetId,
            colorTextureId: colorTextureIndex >= 0 ? `${modelPartId}-texture-${colorTextureIndex}` : null,
            normalsTextureId: normalsTextureIndex >= 0 ? `${modelPartId}-texture-${normalsTextureIndex}` : null,
            metallicRoughnessTextureId: metallicRoughnessTextureIndex >= 0 ? `${modelPartId}-texture-${metallicRoughnessTextureIndex}` : null,
            emissiveTextureId: emissiveTextureIndex >= 0 ? `${modelPartId}-texture-${emissiveTextureIndex}` : null,
            occlusionTextureId: occlusionTextureIndex >= 0 ? `${modelPartId}-texture-${occlusionTextureIndex}` : null
        });
    }

    // Count instances of each geometry

    const geometryReuseCounts = new Uint32Array(numGeometries);

    for (let meshIndex = 0; meshIndex < numMeshes; meshIndex++) {
        const geometryIndex = eachMeshGeometriesPortion[meshIndex];
        if (geometryReuseCounts[geometryIndex] !== undefined) {
            geometryReuseCounts[geometryIndex]++;
        } else {
            geometryReuseCounts[geometryIndex] = 1;
        }
    }

    // Iterate over tiles

    const tileCenter = math.vec3();
    const rtcAABB = math.AABB3();

    const geometryArraysCache = {};

    for (let tileIndex = 0; tileIndex < numTiles; tileIndex++) {

        const lastTileIndex = (numTiles - 1);

        const atLastTile = (tileIndex === lastTileIndex);

        const firstTileEntityIndex = eachTileEntitiesPortion [tileIndex];
        const lastTileEntityIndex = atLastTile ? (numEntities - 1) : (eachTileEntitiesPortion[tileIndex + 1] - 1);

        const tileAABBIndex = tileIndex * 6;
        const tileAABB = eachTileAABB.subarray(tileAABBIndex, tileAABBIndex + 6);

        math.getAABB3Center(tileAABB, tileCenter);

        rtcAABB[0] = tileAABB[0] - tileCenter[0];
        rtcAABB[1] = tileAABB[1] - tileCenter[1];
        rtcAABB[2] = tileAABB[2] - tileCenter[2];
        rtcAABB[3] = tileAABB[3] - tileCenter[0];
        rtcAABB[4] = tileAABB[4] - tileCenter[1];
        rtcAABB[5] = tileAABB[5] - tileCenter[2];

        const tileDecodeMatrix = geometryCompressionUtils.createPositionsDecodeMatrix(rtcAABB);

        const geometryCreatedInTile = {};

        // Iterate over each tile's entities

        for (let tileEntityIndex = firstTileEntityIndex; tileEntityIndex <= lastTileEntityIndex; tileEntityIndex++) {

            const xktEntityId = eachEntityId[tileEntityIndex];

            const entityId = options.globalizeObjectIds ? math.globalizeObjectId(sceneModel.id, xktEntityId) : xktEntityId;

            const finalTileEntityIndex = (numEntities - 1);
            const atLastTileEntity = (tileEntityIndex === finalTileEntityIndex);
            const firstMeshIndex = eachEntityMeshesPortion [tileEntityIndex];
            const lastMeshIndex = atLastTileEntity ? (eachMeshGeometriesPortion.length - 1) : (eachEntityMeshesPortion[tileEntityIndex + 1] - 1);

            const meshIds = [];

            const metaObject = viewer.metaScene.metaObjects[entityId];
            const entityDefaults = {};
            const meshDefaults = {};

            if (metaObject) {

                // Mask loading of object types

                if (options.excludeTypesMap && metaObject.type && options.excludeTypesMap[metaObject.type]) {
                    continue;
                }

                if (options.includeTypesMap && metaObject.type && (!options.includeTypesMap[metaObject.type])) {
                    continue;
                }

                // Mask loading of object ids

                if (options.includeIdsMap && metaObject.id && (!options.includeIdsMap[metaObject.id])) {
                    continue;
                }

                // Get initial property values for object types

                const props = options.objectDefaults ? options.objectDefaults[metaObject.type] || options.objectDefaults["DEFAULT"] : null;

                if (props) {
                    if (props.visible === false) {
                        entityDefaults.visible = false;
                    }
                    if (props.pickable === false) {
                        entityDefaults.pickable = false;
                    }
                    if (props.colorize) {
                        meshDefaults.color = props.colorize;
                    }
                    if (props.opacity !== undefined && props.opacity !== null) {
                        meshDefaults.opacity = props.opacity;
                    }
                    if (props.metallic !== undefined && props.metallic !== null) {
                        meshDefaults.metallic = props.metallic;
                    }
                    if (props.roughness !== undefined && props.roughness !== null) {
                        meshDefaults.roughness = props.roughness;
                    }
                }

            } else {
                if (options.excludeUnclassifiedObjects) {
                    continue;
                }
            }

            // Iterate each entity's meshes

            for (let meshIndex = firstMeshIndex; meshIndex <= lastMeshIndex; meshIndex++) {

                const geometryIndex = eachMeshGeometriesPortion[meshIndex];
                const geometryReuseCount = geometryReuseCounts[geometryIndex];
                const isReusedGeometry = (geometryReuseCount > 1);

                const atLastGeometry = (geometryIndex === (numGeometries - 1));

                const textureSetIndex = eachMeshTextureSet[meshIndex];

                const textureSetId = (textureSetIndex >= 0) ? `${modelPartId}-textureSet-${textureSetIndex}` : null;

                const meshColor = decompressColor(eachMeshMaterialAttributes.subarray((meshIndex * 6), (meshIndex * 6) + 3));
                const meshOpacity = eachMeshMaterialAttributes[(meshIndex * 6) + 3] / 255.0;
                const meshMetallic = eachMeshMaterialAttributes[(meshIndex * 6) + 4] / 255.0;
                const meshRoughness = eachMeshMaterialAttributes[(meshIndex * 6) + 5] / 255.0;

                const meshId = manifestCtx.getNextId();

                if (isReusedGeometry) {

                    // Create mesh for multi-use geometry - create (or reuse) geometry, create mesh using that geometry

                    const meshMatrixIndex = eachMeshMatricesPortion[meshIndex];
                    const meshMatrix = matrices.slice(meshMatrixIndex, meshMatrixIndex + 16);

                    const geometryId = `${modelPartId}-geometry.${tileIndex}.${geometryIndex}`; // These IDs are local to the SceneModel

                    let geometryArrays = geometryArraysCache[geometryId];

                    if (!geometryArrays) {
                        geometryArrays = {
                            batchThisMesh: (!options.reuseGeometries)
                        };
                        const primitiveType = eachGeometryPrimitiveType[geometryIndex];
                        const axisLabel = eachGeometryAxisLabel[geometryIndex];
                        let geometryValid = false;
                        let ifcLabel;
                        switch (primitiveType) {
                            case 0:
                                geometryArrays.primitiveName = "solid";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                                geometryArrays.geometryUVs = uvs.subarray(eachGeometryUVsPortion [geometryIndex], atLastGeometry ? uvs.length : eachGeometryUVsPortion [geometryIndex + 1]);
                                geometryArrays.geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                                geometryArrays.geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            case 1:
                                geometryArrays.primitiveName = "surface";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                                geometryArrays.geometryUVs = uvs.subarray(eachGeometryUVsPortion [geometryIndex], atLastGeometry ? uvs.length : eachGeometryUVsPortion [geometryIndex + 1]);
                                geometryArrays.geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                                geometryArrays.geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            case 2:
                                geometryArrays.primitiveName = "points";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryColors = colors.subarray(eachGeometryColorsPortion [geometryIndex], atLastGeometry ? colors.length : eachGeometryColorsPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0);
                                break;
                            case 3:
                                geometryArrays.primitiveName = "lines";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                                geometryValid = (geometryArrays.geometryPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            case 4:
                                geometryArrays.primitiveName = "lines";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.geometryIndices = lineStripToLines(
                                    geometryArrays.geometryPositions,
                                    indices.subarray(eachGeometryIndicesPortion [geometryIndex],
                                        atLastGeometry
                                            ? indices.length
                                            : eachGeometryIndicesPortion [geometryIndex + 1]));
                                geometryValid = (geometryArrays.geometryPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            case 7:
                                geometryArrays.primitiveName = "lines";
                                geometryArrays.geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                                geometryArrays.decompressedPositions = geometryCompressionUtils.decompressPositions(geometryArrays.geometryPositions, reusedGeometriesDecodeMatrix);
                                ifcLabel = buildVectorTextGeometry({
                                    origin: geometryArrays.decompressedPositions,
                                    text: axisLabel,
                                    size: 2
                                });
                                
                                geometryArrays.decompressedPositions = ifcLabel.positions;
                                geometryArrays.geometryIndices = ifcLabel.indices;
                                geometryArrays.geometryPositions = null;
    
                                geometryValid = (geometryArrays.decompressedPositions.length > 0 && geometryArrays.geometryIndices.length > 0);
                                break;
                            default:
                                continue;
                        }

                        if (!geometryValid) {
                            geometryArrays = null;
                        }

                        if (geometryArrays) {
                            if (geometryArrays.geometryPositions && geometryArrays.geometryPositions.length > 1000) ;
                            if (geometryArrays.batchThisMesh) {
                                if(primitiveType === 7) {
                                    geometryArrays.transformedAndRecompressedPositions = new Uint16Array(geometryArrays.decompressedPositions.length);
                                }
                                else {
                                    geometryArrays.decompressedPositions = new Float32Array(geometryArrays.geometryPositions.length);
                                    geometryArrays.transformedAndRecompressedPositions = new Uint16Array(geometryArrays.geometryPositions.length);
                                    const geometryPositions = geometryArrays.geometryPositions;
                                    const decompressedPositions = geometryArrays.decompressedPositions;
                                    for (let i = 0, len = geometryPositions.length; i < len; i += 3) {
                                        decompressedPositions[i + 0] = geometryPositions[i + 0] * reusedGeometriesDecodeMatrix[0] + reusedGeometriesDecodeMatrix[12];
                                        decompressedPositions[i + 1] = geometryPositions[i + 1] * reusedGeometriesDecodeMatrix[5] + reusedGeometriesDecodeMatrix[13];
                                        decompressedPositions[i + 2] = geometryPositions[i + 2] * reusedGeometriesDecodeMatrix[10] + reusedGeometriesDecodeMatrix[14];
                                    }
                                    geometryArrays.geometryPositions = null;
                                }
                                
                                geometryArraysCache[geometryId] = geometryArrays;
                            }
                        }
                    }

                    if (geometryArrays) {

                        if (geometryArrays.batchThisMesh) {

                            const decompressedPositions = geometryArrays.decompressedPositions;
                            const transformedAndRecompressedPositions = geometryArrays.transformedAndRecompressedPositions;

                            for (let i = 0, len = decompressedPositions.length; i < len; i += 3) {
                                tempVec4a[0] = decompressedPositions[i + 0];
                                tempVec4a[1] = decompressedPositions[i + 1];
                                tempVec4a[2] = decompressedPositions[i + 2];
                                tempVec4a[3] = 1;
                                math.transformVec4(meshMatrix, tempVec4a, tempVec4b);
                                geometryCompressionUtils.compressPosition(tempVec4b, rtcAABB, tempVec4a);
                                transformedAndRecompressedPositions[i + 0] = tempVec4a[0];
                                transformedAndRecompressedPositions[i + 1] = tempVec4a[1];
                                transformedAndRecompressedPositions[i + 2] = tempVec4a[2];
                            }

                            sceneModel.createMesh(utils.apply(meshDefaults, {
                                id: meshId,
                                textureSetId,
                                origin: tileCenter,
                                primitive: geometryArrays.primitiveName,
                                positionsCompressed: transformedAndRecompressedPositions,
                                normalsCompressed: geometryArrays.geometryNormals,
                                uv: geometryArrays.geometryUVs,
                                colorsCompressed: geometryArrays.geometryColors,
                                indices: geometryArrays.geometryIndices,
                                edgeIndices: geometryArrays.geometryEdgeIndices,
                                positionsDecodeMatrix: tileDecodeMatrix,
                                color: meshColor,
                                metallic: meshMetallic,
                                roughness: meshRoughness,
                                opacity: meshOpacity
                            }));

                            meshIds.push(meshId);

                        } else {

                            if (!geometryCreatedInTile[geometryId]) {
                                let geometryCfg = {
                                    id: geometryId,
                                    primitive: geometryArrays.primitiveName,
                                    normalsCompressed: geometryArrays.geometryNormals,
                                    uv: geometryArrays.geometryUVs,
                                    colorsCompressed: geometryArrays.geometryColors,
                                    indices: geometryArrays.geometryIndices,
                                    edgeIndices: geometryArrays.geometryEdgeIndices
                                };

                                if(geometryArrays.geometryPositions){
                                    geometryCfg = {
                                        ...geometryCfg,
                                        positionsCompressed: geometryArrays.geometryPositions,
                                        positionsDecodeMatrix: reusedGeometriesDecodeMatrix
                                    };
                                }
                                else {
                                    geometryCfg = {
                                        ...geometryCfg,
                                        positions: geometryArrays.decompressedPositions,
                                    };
                                }

                                sceneModel.createGeometry(geometryCfg);

                                geometryCreatedInTile[geometryId] = true;
                            }

                            sceneModel.createMesh(utils.apply(meshDefaults, {
                                id: meshId,
                                geometryId,
                                textureSetId,
                                matrix: meshMatrix,
                                color: meshColor,
                                metallic: meshMetallic,
                                roughness: meshRoughness,
                                opacity: meshOpacity,
                                origin: tileCenter
                            }));

                            meshIds.push(meshId);
                        }
                    }

                } else { // Do not reuse geometry

                    const primitiveType = eachGeometryPrimitiveType[geometryIndex];
                    const axisLabel = eachGeometryAxisLabel[geometryIndex];

                    let primitiveName;
                    let geometryPositions;
                    let geometryNormals;
                    let geometryUVs;
                    let geometryColors;
                    let geometryIndices;
                    let geometryEdgeIndices;
                    let geometryValid = false;
                    let ifcLabel;

                    switch (primitiveType) {
                        case 0:
                            primitiveName = "solid";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                            geometryUVs = uvs.subarray(eachGeometryUVsPortion [geometryIndex], atLastGeometry ? uvs.length : eachGeometryUVsPortion [geometryIndex + 1]);
                            geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                            geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                        case 1:
                            primitiveName = "surface";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryNormals = normals.subarray(eachGeometryNormalsPortion [geometryIndex], atLastGeometry ? normals.length : eachGeometryNormalsPortion [geometryIndex + 1]);
                            geometryUVs = uvs.subarray(eachGeometryUVsPortion [geometryIndex], atLastGeometry ? uvs.length : eachGeometryUVsPortion [geometryIndex + 1]);
                            geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                            geometryEdgeIndices = edgeIndices.subarray(eachGeometryEdgeIndicesPortion [geometryIndex], atLastGeometry ? edgeIndices.length : eachGeometryEdgeIndicesPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                        case 2:
                            primitiveName = "points";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryColors = colors.subarray(eachGeometryColorsPortion [geometryIndex], atLastGeometry ? colors.length : eachGeometryColorsPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0);
                            break;
                        case 3:
                            primitiveName = "lines";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryIndices = indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry ? indices.length : eachGeometryIndicesPortion [geometryIndex + 1]);
                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                        case 4:
                            primitiveName = "lines";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryIndices = lineStripToLines(
                                geometryPositions,
                                indices.subarray(eachGeometryIndicesPortion [geometryIndex], atLastGeometry
                                    ? indices.length
                                    : eachGeometryIndicesPortion [geometryIndex + 1]));
                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                            //Ifc Labels
                        case 7:
                            primitiveName = "lines";
                            geometryPositions = positions.subarray(eachGeometryPositionsPortion [geometryIndex], atLastGeometry ? positions.length : eachGeometryPositionsPortion [geometryIndex + 1]);
                            geometryPositions = geometryCompressionUtils.decompressPositions(geometryPositions, tileDecodeMatrix);
                            ifcLabel = buildVectorTextGeometry({
                                origin: geometryPositions,
                                text: axisLabel,
                                size: 2
                            });
                            
                            geometryPositions = ifcLabel.positions;
                            geometryIndices = ifcLabel.indices;

                            geometryValid = (geometryPositions.length > 0 && geometryIndices.length > 0);
                            break;
                        default:
                            continue;
                    }

                    if (geometryValid) {

                        let meshCfg = utils.apply(meshDefaults, {
                            id: meshId,
                            textureSetId,
                            origin: tileCenter,
                            primitive: primitiveName,
                            normalsCompressed: geometryNormals,
                            uv: geometryUVs && geometryUVs.length > 0 ? geometryUVs : null,
                            colorsCompressed: geometryColors,
                            indices: geometryIndices,
                            edgeIndices: geometryEdgeIndices,
                            color: meshColor,
                            metallic: meshMetallic,
                            roughness: meshRoughness,
                            opacity: meshOpacity
                        });

                        if(primitiveType !== 7) {
                            meshCfg = {
                                ...meshCfg,
                                positionsCompressed: geometryPositions,
                                positionsDecodeMatrix: tileDecodeMatrix
                            };
                        }
                        else {
                            meshCfg = {
                                ...meshCfg,
                                positions: geometryPositions
                            };
                        }

                        sceneModel.createMesh(meshCfg);

                        meshIds.push(meshId);
                    }
                }
            }

            if (meshIds.length > 0) {

                sceneModel.createEntity(utils.apply(entityDefaults, {
                    id: entityId,
                    isObject: true,
                    meshIds: meshIds
                }));
            }
        }
    }
}

function lineStripToLines(positions, indices) {
    const linesIndices = [];
    if (indices.length > 1) {
        for (let i = 0, len = indices.length - 1; i < len; i++) {
            linesIndices.push(indices[i]);
            linesIndices.push(indices[i + 1]);
        }
    } else if (positions.length > 1) {
        for (let i = 0, len = (positions.length / 3) - 1; i < len; i++) {
            linesIndices.push(i);
            linesIndices.push(i + 1);
        }
    }
    return linesIndices;
}

/** @private */
// V11 uses a single uncompressed Uint8Array buffer to store arrays of different types.
// To efficiently create typed arrays from this buffer,
// each typed array's source data needs to be aligned with its element byte size.
// This sometimes requires padding subarrays inside the single Uint8Array, so the byteOffset needs to be stored alongside element count.
// It is a different encoding than used in earlier versions, and requires different approach to parsing elements.
const ParserV12 = {
    version: 12,
    parseArrayBuffer: function (viewer, options, arrayBuffer, sceneModel, metaModel, manifestCtx) {
        const inflatedData = decodeData(arrayBuffer);
        load(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx);
    },
    parse: function (viewer, options, elements, sceneModel, metaModel, manifestCtx) {
        const deflatedData = extract(elements);
        const inflatedData = inflate(deflatedData);
        load(viewer, options, inflatedData, sceneModel, metaModel, manifestCtx);
    }
};

const parsers = {};

parsers[ParserV1.version] = ParserV1;
parsers[ParserV2.version] = ParserV2;
parsers[ParserV3.version] = ParserV3;
parsers[ParserV4.version] = ParserV4;
parsers[ParserV5.version] = ParserV5;
parsers[ParserV6.version] = ParserV6;
parsers[ParserV7.version] = ParserV7;
parsers[ParserV8.version] = ParserV8;
parsers[ParserV9.version] = ParserV9;
parsers[ParserV10.version] = ParserV10;
parsers[ParserV11.version] = ParserV11;
parsers[ParserV12.version] = ParserV12;

/**
 * {@link Viewer} plugin that loads models from xeokit's optimized *````.XKT````* format.
 *
 * <a href="https://xeokit.github.io/xeokit-sdk/examples/index.html#loading_XKT_OTCConferenceCenter"><img src="http://xeokit.io/img/docs/XKTLoaderPlugin/XKTLoaderPlugin.png"></a>
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#loading_XKT_OTCConferenceCenter)]
 *
 * # Overview
 *
 * * XKTLoaderPlugin is the most efficient way to load high-detail models into xeokit.
 * * An *````.XKT````* file is a single BLOB containing a model, compressed using geometry quantization
 * and [pako](https://nodeca.github.io/pako/).
 * * Supports double-precision coordinates.
 * * Supports compressed textures.
 * * Set the position, scale and rotation of each model as you load it.
 * * Filter which IFC types get loaded.
 * * Configure initial default appearances for IFC types.
 * * Set a custom data source for *````.XKT````* and IFC metadata files.
 * * Option to load multiple copies of the same model, without object ID clashes.
 *
 * # Creating *````.XKT````* Files and Metadata
 *
 * We have several sways to convert your files into XKT. See these tutorials for more info:
 *
 * * [Converting Models to XKT with convert2xkt](https://www.notion.so/xeokit/Converting-Models-to-XKT-with-convert2xkt-fa567843313f4db8a7d6535e76da9380) - how to convert various file formats (glTF, IFC, CityJSON, LAS/LAZ...) to XKT using our nodejs-based converter.
 * * [Converting IFC Models to XKT using 3rd-Party Open Source Tools](https://www.notion.so/xeokit/Converting-IFC-Models-to-XKT-using-3rd-Party-Open-Source-Tools-c373e48bc4094ff5b6e5c5700ff580ee) - how to convert IFC files to XKT using 3rd-party open source CLI tools.
 *
 * # Scene representation
 *
 * When loading a model, XKTLoaderPlugin creates an {@link Entity} that represents the model, which
 * will have {@link Entity#isModel} set ````true```` and will be registered by {@link Entity#id}
 * in {@link Scene#models}. The XKTLoaderPlugin also creates an {@link Entity} for each object within the
 * model. Those Entities will have {@link Entity#isObject} set ````true```` and will be registered
 * by {@link Entity#id} in {@link Scene#objects}.
 *
 * # Metadata
 *
 * Since XKT V8, model metadata is included in the XKT file. If the XKT file has metadata, then loading it creates
 * model metadata components within the Viewer, namely a {@link MetaModel} corresponding to the model {@link Entity},
 * and a {@link MetaObject} for each object {@link Entity}.
 *
 * Each {@link MetaObject} has a {@link MetaObject#type}, which indicates the classification of its corresponding
 * {@link Entity}. When loading metadata, we can also configure XKTLoaderPlugin with a custom lookup table of initial
 * values to set on the properties of each type of {@link Entity}. By default, XKTLoaderPlugin uses its own map of
 * default colors and visibilities for IFC element types.
 *
 * For XKT versions prior to V8, we provided the metadata to XKTLoaderPlugin as an accompanying JSON file to load. We can
 * still do that for all XKT versions, and for XKT V8+ it will override any metadata provided within the XKT file.
 *
 * # Usage
 *
 * In the example below we'll load the Schependomlaan model from a [.XKT file](https://github.com/xeokit/xeokit-sdk/tree/master/examples/models/xkt/schependomlaan).
 *
 * This will create a bunch of {@link Entity}s that represents the model and its objects, along with a {@link MetaModel} and {@link MetaObject}s
 * that hold their metadata.
 *
 * Since this model contains IFC types, the XKTLoaderPlugin will set the initial appearance of each object
 * {@link Entity} according to its IFC type in {@link XKTLoaderPlugin#objectDefaults}.
 *
 * Read more about this example in the user guide on [Viewing BIM Models Offline](https://www.notion.so/xeokit/Viewing-an-IFC-Model-with-xeokit-c373e48bc4094ff5b6e5c5700ff580ee).
 *
 * * [[Run example](https://xeokit.github.io/xeokit-sdk/examples/index.html#BIMOffline_XKT_metadata_Schependomlaan)]
 *
 * ````javascript
 * import {Viewer, XKTLoaderPlugin} from "xeokit-sdk.es.js";
 *
 * //------------------------------------------------------------------------------------------------------------------
 * // 1. Create a Viewer,
 * // 2. Arrange the camera
 * //------------------------------------------------------------------------------------------------------------------
 *
 * // 1
 * const viewer = new Viewer({
 *      canvasId: "myCanvas",
 *      transparent: true
 * });
 *
 * // 2
 * viewer.camera.eye = [-2.56, 8.38, 8.27];
 * viewer.camera.look = [13.44, 3.31, -14.83];
 * viewer.camera.up = [0.10, 0.98, -0.14];
 *
 * //------------------------------------------------------------------------------------------------------------------
 * // 1. Create a XKTLoaderPlugin,
 * // 2. Load a building model and JSON IFC metadata
 * //------------------------------------------------------------------------------------------------------------------
 *
 * // 1
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * // 2
 * const model = xktLoader.load({          // Returns an Entity that represents the model
 *     id: "myModel",
 *     src: "./models/xkt/Schependomlaan.xkt",
 *     edges: true
 * });
 *
 * model.on("loaded", () => {
 *
 *     //--------------------------------------------------------------------------------------------------------------
 *     // 1. Find metadata on the third storey
 *     // 2. Select all the objects in the building's third storey
 *     // 3. Fit the camera to all the objects on the third storey
 *     //--------------------------------------------------------------------------------------------------------------
 *
 *     // 1
 *     const metaModel = viewer.metaScene.metaModels["myModel"];       // MetaModel with ID "myModel"
 *     const metaObject
 *          = viewer.metaScene.metaObjects["0u4wgLe6n0ABVaiXyikbkA"];  // MetaObject with ID "0u4wgLe6n0ABVaiXyikbkA"
 *
 *     const name = metaObject.name;                                   // "01 eerste verdieping"
 *     const type = metaObject.type;                                   // "IfcBuildingStorey"
 *     const parent = metaObject.parent;                               // MetaObject with type "IfcBuilding"
 *     const children = metaObject.children;                           // Array of child MetaObjects
 *     const objectId = metaObject.id;                                 // "0u4wgLe6n0ABVaiXyikbkA"
 *     const objectIds = viewer.metaScene.getObjectIDsInSubtree(objectId);   // IDs of leaf sub-objects
 *     const aabb = viewer.scene.getAABB(objectIds);                   // Axis-aligned boundary of the leaf sub-objects
 *
 *     // 2
 *     viewer.scene.setObjectsSelected(objectIds, true);
 *
 *     // 3
 *     viewer.cameraFlight.flyTo(aabb);
 * });
 *
 * // Find the model Entity by ID
 * model = viewer.scene.models["myModel"];
 *
 * // Destroy the model
 * model.destroy();
 * ````
 *
 * # Loading XKT files containing textures
 *
 * XKTLoaderPlugin uses a {@link KTX2TextureTranscoder} to load textures in XKT files (XKT v10+). An XKTLoaderPlugin has its own
 * default KTX2TextureTranscoder, configured to load the Basis Codec from the CDN. If we wish, we can override that with our own
 * KTX2TextureTranscoder instance that's configured to load the Codec locally.
 *
 * In the example below, we'll create a {@link Viewer} and add an XKTLoaderPlugin
 * configured with a KTX2TextureTranscoder that finds the Codec in our local file system. Then we'll use the
 * XKTLoaderPlugin to load an XKT file that contains KTX2 textures, which the plugin will transcode using
 * its KTX2TextureTranscoder.
 *
 * We'll configure our KTX2TextureTranscoder to load the Basis Codec from a local directory. If we were happy with loading the
 * Codec from our CDN (ie. our app will always have an Internet connection) then we could just leave out the
 * KTX2TextureTranscoder altogether, and let the XKTLoaderPlugin use its internal default KTX2TextureTranscoder, which is configured to
 * load the Codec from the CDN. We'll stick with loading our own Codec, in case we want to run our app without an Internet connection.
 *
 * <a href="https://xeokit.github.io/xeokit-sdk/examples/buildings/#xkt_vbo_textures_HousePlan"><img src="https://xeokit.github.io/xeokit-sdk/assets/images/xktWithTextures.png"></a>
 *
 * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/buildings/#xkt_vbo_textures_HousePlan)]
 *
 * ````javascript
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.camera.eye = [-2.56, 8.38, 8.27];
 * viewer.camera.look = [13.44, 3.31, -14.83];
 * viewer.camera.up = [0.10, 0.98, -0.14];
 *
 * const textureTranscoder = new KTX2TextureTranscoder({
 *     viewer,
 *     transcoderPath: "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/" // <------ Path to Basis Universal transcoder
 * });
 *
 * const xktLoader = new XKTLoaderPlugin(viewer, {
 *     textureTranscoder // <<------------- Transcodes KTX2 textures in XKT files
 * });
 *
 * const sceneModel = xktLoader.load({
 *     id: "myModel",
 *     src: "./HousePlan.xkt" // <<------ XKT file with KTX2 textures
 * });
 * ````
 *
 * # Transforming
 *
 * We have the option to rotate, scale and translate each  *````.XKT````* model as we load it.
 *
 * This lets us load multiple models, or even multiple copies of the same model, and position them apart from each other.
 *
 * In the example below, we'll scale our model to half its size, rotate it 90 degrees about its local X-axis, then
 * translate it 100 units along its X axis.
 *
 * ````javascript
 * xktLoader.load({
 *      src: "./models/xkt/Duplex.ifc.xkt",
 *      rotation: [90,0,0],
 *      scale: [0.5, 0.5, 0.5],
 *      position: [100, 0, 0]
 * });
 * ````
 *
 * # Including and excluding IFC types
 *
 * We can also load only those objects that have the specified IFC types.
 *
 * In the example below, we'll load only the objects that represent walls.
 *
 * ````javascript
 * const model2 = xktLoader.load({
 *     id: "myModel2",
 *     src: "./models/xkt/OTCConferenceCenter.xkt",
 *     includeTypes: ["IfcWallStandardCase"]
 * });
 * ````
 *
 * We can also load only those objects that **don't** have the specified IFC types.
 *
 * In the example below, we'll load only the objects that do not represent empty space.
 *
 * ````javascript
 * const model3 = xktLoader.load({
 *     id: "myModel3",
 *     src: "./models/xkt/OTCConferenceCenter.xkt",
 *     excludeTypes: ["IfcSpace"]
 * });
 * ````
 *
 * # Including specific elements by id
 *
 * We can also load only those objects that have the specified ids.
 *
 * In the example below, we'll load only the objects that represent a specified wall with 4 windows.
 *
 * ````javascript
 * const model4 = xktLoader.load({
 *      id: "myModel4",
 *      src: "../../assets/models/xkt/v10/glTF-Embedded/Duplex_A_20110505.glTFEmbedded.xkt",
 *      includeIds: ["2O2Fr$t4X7Zf8NOew3FLOH", "1hOSvn6df7F8_7GcBWlS_W", "1hOSvn6df7F8_7GcBWlS4Q", "1hOSvn6df7F8_7GcBWlS1M", "1hOSvn6df7F8_7GcBWlS2V"],
 * });
 * ````
 *
 * # Configuring initial IFC object appearances
 *
 * We can specify the custom initial appearance of loaded objects according to their IFC types.
 *
 * This is useful for things like:
 *
 * * setting the colors to our objects according to their IFC types,
 * * automatically hiding ````IfcSpace```` objects, and
 * * ensuring that ````IfcWindow```` objects are always transparent.
 * <br>
 * In the example below, we'll load a model, while configuring ````IfcSpace```` elements to be always initially invisible,
 * and ````IfcWindow```` types to be always translucent blue.
 *
 * ````javascript
 * const myObjectDefaults = {
 *
 *      IfcSpace: {
 *          visible: false
 *      },
 *      IfcWindow: {
 *          colorize: [0.337255, 0.303922, 0.870588], // Blue
 *          opacity: 0.3
 *      },
 *
 *      //...
 *
 *      DEFAULT: {
 *          colorize: [0.5, 0.5, 0.5]
 *      }
 * };
 *
 * const model4 = xktLoader.load({
 *      id: "myModel4",
 *      src: "./models/xkt/Duplex.ifc.xkt",
 *      objectDefaults: myObjectDefaults // Use our custom initial default states for object Entities
 * });
 * ````
 *
 * When we don't customize the appearance of IFC types, as just above, then IfcSpace elements tend to obscure other
 * elements, which can be confusing.
 *
 * It's often helpful to make IfcSpaces transparent and unpickable, like this:
 *
 * ````javascript
 * const xktLoader = new XKTLoaderPlugin(viewer, {
 *    objectDefaults: {
 *        IfcSpace: {
 *            pickable: false,
 *            opacity: 0.2
 *        }
 *    }
 * });
 * ````
 *
 * Alternatively, we could just make IfcSpaces invisible, which also makes them unpickable:
 *
 * ````javascript
 * const xktLoader = new XKTLoaderPlugin(viewer, {
 *    objectDefaults: {
 *        IfcSpace: {
 *            visible: false
 *        }
 *    }
 * });
 * ````
 *
 * # Configuring a custom data source
 *
 * By default, XKTLoaderPlugin will load *````.XKT````* files and metadata JSON over HTTP.
 *
 * In the example below, we'll customize the way XKTLoaderPlugin loads the files by configuring it with our own data source
 * object. For simplicity, our custom data source example also uses HTTP, using a couple of xeokit utility functions.
 *
 * ````javascript
 * import {utils} from "xeokit-sdk.es.js";
 *
 * class MyDataSource {
 *
 *      constructor() {
 *      }
 *
 *      // Gets metamodel JSON
 *      getMetaModel(metaModelSrc, ok, error) {
 *          console.log("MyDataSource#getMetaModel(" + metaModelSrc + ", ... )");
 *          utils.loadJSON(metaModelSrc,
 *              (json) => {
 *                  ok(json);
 *              },
 *              function (errMsg) {
 *                  error(errMsg);
 *              });
 *      }
 *
 *      // Gets the contents of the given .XKT file in an arraybuffer
 *      getXKT(src, ok, error) {
 *          console.log("MyDataSource#getXKT(" + xKTSrc + ", ... )");
 *          utils.loadArraybuffer(src,
 *              (arraybuffer) => {
 *                  ok(arraybuffer);
 *              },
 *              function (errMsg) {
 *                  error(errMsg);
 *              });
 *      }
 * }
 *
 * const xktLoader2 = new XKTLoaderPlugin(viewer, {
 *       dataSource: new MyDataSource()
 * });
 *
 * const model5 = xktLoader2.load({
 *      id: "myModel5",
 *      src: "./models/xkt/Duplex.ifc.xkt"
 * });
 * ````
 *
 * # Loading multiple copies of a model, without object ID clashes
 *
 * Sometimes we need to load two or more instances of the same model, without having clashes
 * between the IDs of the equivalent objects in the model instances.
 *
 * As shown in the example below, we do this by setting {@link XKTLoaderPlugin#globalizeObjectIds} ````true```` before we load our models.
 *
 * * [[Run example](https://xeokit.github.io/xeokit-sdk/examples/index.html#TreeViewPlugin_Containment_MultipleModels)]
 *
 * ````javascript
 * xktLoader.globalizeObjectIds = true;
 *
 * const model = xktLoader.load({
 *      id: "model1",
 *      src: "./models/xkt/Schependomlaan.xkt"
 * });
 *
 * const model2 = xktLoader.load({
 *    id: "model2",
 *    src: "./models/xkt/Schependomlaan.xkt"
 * });
 * ````
 *
 * For each {@link Entity} loaded by these two calls, {@link Entity#id} and {@link MetaObject#id} will get prefixed by
 * the ID of their model, in order to avoid ID clashes between the two models.
 *
 * An Entity belonging to the first model will get an ID like this:
 *
 * ````
 * myModel1#0BTBFw6f90Nfh9rP1dlXrb
 * ````
 *
 * The equivalent Entity in the second model will get an ID like this:
 *
 * ````
 * myModel2#0BTBFw6f90Nfh9rP1dlXrb
 * ````
 *
 * Now, to update the visibility of both of those Entities collectively, using {@link Scene#setObjectsVisible}, we can
 * supply just the IFC product ID part to that method:
 *
 * ````javascript
 * myViewer.scene.setObjectVisibilities("0BTBFw6f90Nfh9rP1dlXrb", true);
 * ````
 *
 * The method, along with {@link Scene#setObjectsXRayed}, {@link Scene#setObjectsHighlighted} etc, will internally expand
 * the given ID to refer to the instances of that Entity in both models.
 *
 * We can also, of course, reference each Entity directly, using its globalized ID:
 *
 * ````javascript
 * myViewer.scene.setObjectVisibilities("myModel1#0BTBFw6f90Nfh9rP1dlXrb", true);
 *````
 *
 * We can also provide an HTTP URL to the XKT file:
 *
 * ````javascript
 * const sceneModel = xktLoader.load({
 *   src: "https://xeokit.github.io/xeokit-sdk/assets/models/models/xkt/Schependomlaan.xkt",
 *   id: "myModel",
 * });
 * ````
 *
 * # Loading a model from a manifest of XKT files
 *
 * The `ifc2gltf` tool from Creoox, which converts IFC files into glTF geometry and JSON metadata files, has the option to
 * split its output into multiple pairs of glTF and JSON files, accompanied by a JSON manifest that lists the files.
 *
 * To integrate with that option, the `convert2xkt` tool, which converts glTF geometry and JSON metadata files into XKT files,
 * also has the option to batch-convert the glTF+JSON files in the manifest, in one invocation.
 *
 * When we use this option, convert2xkt will output a bunch of XKT files, along with a JSON manifest file that lists those XKT files.
 *
 * Working down the pipeline, the XKTLoaderPlugin has the option batch-load all XKT files listed in that manifest
 * into a xeokit Viewer in one load operation, combining the XKT files into a single SceneModel and MetaModel.
 *
 * You can learn more about this conversion and loading process, with splitting, batch converting and batch loading,
 * in [this tutorial](https://www.notion.so/xeokit/Importing-Huge-IFC-Models-as-Multiple-XKT-Files-165fc022e94742cf966ee50003572259).
 *
 * To show how to use XKTLoaderPlugin to load a manifest of XKT files, let's imagine that we have a set of such XKT files. As
 * described in the tutorial, they were converted by `ifc2gltf` from an IFC file into a set of glTF+JSON files, that were
 * then converted by convert2xkt into this set of XKT files and a manifest, as shown below.
 *
 * ````bash
 * ./
 * ├── model_1.xkt
 * ├── model_2.xkt
 * ├── model_3.xkt
 * ├── model_4..xkt
 * └── model.xkt.manifest.json
 * ````
 *
 * The `model.xkt.manifest.json` XKT manifest would look something like this:
 *
 * ````json
 * {
 *   "inputFile": null,
 *   "converterApplication": "convert2xkt",
 *   "converterApplicationVersion": "v1.1.9",
 *   "conversionDate": "10-08-2023- 02-05-01",
 *   "outputDir": null,
 *   "xktFiles": [
 *     "model_1.xkt",
 *     "model_2.xkt",
 *     "model_3.xkt",
 *     "model_4.xkt"
 *   ]
 * }
 * ````
 *
 * Now, to load all those XKT files into a single SceneModel and MetaModel in one operation, we pass a path to the XKT
 * manifest to `XKTLoaderPlugin.load`, as shown in the example below:
 *
 * * [[Run example](https://xeokit.github.io/xeokit-sdk/examples/buildings/#xkt_manifest_KarhumakiBridge)]
 *
 * ````javascript
 * import {
 *   Viewer,
 *   XKTLoaderPlugin,
 *   TreeViewPlugin,
 * } from "xeokit-sdk.es.js";
 *
 * constviewer = new Viewer({
 *   canvasId: "myCanvas"
 * });
 *
 * viewer.scene.camera.eye = [26.54, 29.29, 36.20,];
 * viewer.scene.camera.look = [-23.51, -8.26, -21.65,];
 * viewer.scene.camera.up = [-0.2, 0.89, -0.33,];
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const sceneModel = xktLoader.load({
 *   manifestSrc: "model.xkt.manifest.json",
 *   id: "myModel",
 * });
 *
 * const metaModel = viewer.metaScene.metaModels[sceneModel.id];
 *
 * // Then when we need to, we can destroy the SceneModel
 * // and MetaModel in one shot, like so:
 *
 * sceneModel.destroy();
 * metaModel.destroy();
 * ````
 *
 * The main advantage here, of splitting IFC files like this within the conversion and import pipeline,
 * is to reduce the memory pressure on each of the `ifc2gltf`, `convert2xkt` and XKTLoaderPlugin components.
 * They work much reliably (and faster) when processing smaller files (eg. 20MB) than when processing large files (eg. 500MB), where
 * they have less trouble allocating the system memory they need for conversion and parsing.
 *
 * We can also provide an HTTP URL to the manifest:
 *
 * ````javascript
 * const sceneModel = xktLoader.load({
 *   manifestSrc: "https://xeokit.github.io/xeokit-sdk/assets/models/xkt/v10/split/Karhumaki-Bridge/model.xkt.manifest.json",
 *   id: "myModel",
 * });
 * ````
 *
 * We can also provide the manifest as parameter object:
 *
 * ````javascript
 * const sceneModel = xktLoader.load({
 *   id: "myModel",
 *   manifest: {
 *   inputFile: "assets/models/gltf/Karhumaki/model.glb.manifest.json",
 *   converterApplication: "convert2xkt",
 *   converterApplicationVersion: "v1.1.10",
 *   conversionDate": "09-11-2023- 18-29-01",
 *     xktFiles: [
 *       "../../assets/models/xkt/v10/split/Karhumaki-Bridge/model.xkt",
 *       "../../assets/models/xkt/v10/split/Karhumaki-Bridge/model_1.xkt",
 *       "../../assets/models/xkt/v10/split/Karhumaki-Bridge/model_2.xkt",
 *       "../../assets/models/xkt/v10/split/Karhumaki-Bridge/model_3.xkt",
 *       "../../assets/models/xkt/v10/split/Karhumaki-Bridge/model_4.xkt",
 *       "../../assets/models/xkt/v10/split/Karhumaki-Bridge/model_5.xkt",
 *       "../../assets/models/xkt/v10/split/Karhumaki-Bridge/model_6.xkt",
 *       "../../assets/models/xkt/v10/split/Karhumaki-Bridge/model_7.xkt",
 *       "../../assets/models/xkt/v10/split/Karhumaki-Bridge/model_8.xkt"
 *     ]
 *   }
 * });
 * ````
 *
 * We can also provide the paths to the XKT files as HTTP URLs:
 *
 * ````javascript
 * const sceneModel = xktLoader.load({
 *   id: "myModel",
 *   manifest: {
 *   inputFile: "assets/models/gltf/Karhumaki/model.glb.manifest.json",
 *   converterApplication: "convert2xkt",
 *   converterApplicationVersion: "v1.1.10",
 *   conversionDate": "09-11-2023- 18-29-01",
 *     xktFiles: [
 *       "https://xeokit.github.io/xeokit-sdk/assets/models/xkt/v10/split/Karhumaki-Bridge/model.xkt",
 *       "https://xeokit.github.io/xeokit-sdk/assets/models/xkt/v10/split/Karhumaki-Bridge/model_1.xkt",
 *       "https://xeokit.github.io/xeokit-sdk/assets/models/xkt/v10/split/Karhumaki-Bridge/model_2.xkt",
 *       "https://xeokit.github.io/xeokit-sdk/assets/models/xkt/v10/split/Karhumaki-Bridge/model_3.xkt",
 *       "https://xeokit.github.io/xeokit-sdk/assets/models/xkt/v10/split/Karhumaki-Bridge/model_4.xkt",
 *       "https://xeokit.github.io/xeokit-sdk/assets/models/xkt/v10/split/Karhumaki-Bridge/model_5.xkt",
 *       "https://xeokit.github.io/xeokit-sdk/assets/models/xkt/v10/split/Karhumaki-Bridge/model_6.xkt",
 *       "https://xeokit.github.io/xeokit-sdk/assets/models/xkt/v10/split/Karhumaki-Bridge/model_7.xkt",
 *       "https://xeokit.github.io/xeokit-sdk/assets/models/xkt/v10/split/Karhumaki-Bridge/model_8.xkt"
 *     ]
 *   }
 * });
 * ````
 *
 * @class XKTLoaderPlugin
 */
class XKTLoaderPlugin extends Plugin {

    /**
     * @constructor
     *
     * @param {Viewer} viewer The Viewer.
     * @param {Object} cfg  Plugin configuration.
     * @param {String} [cfg.id="XKTLoader"] Optional ID for this plugin, so that we can find it within {@link Viewer#plugins}.
     * @param {Object} [cfg.objectDefaults] Map of initial default states for each loaded {@link Entity} that represents an object.  Default value is {@link IFCObjectDefaults}.
     * @param {Object} [cfg.dataSource] A custom data source through which the XKTLoaderPlugin can load model and metadata files. Defaults to an instance of {@link XKTDefaultDataSource}, which loads uover HTTP.
     * @param {String[]} [cfg.includeTypes] When loading metadata, only loads objects that have {@link MetaObject}s with {@link MetaObject#type} values in this list.
     * @param {String[]} [cfg.excludeTypes] When loading metadata, never loads objects that have {@link MetaObject}s with {@link MetaObject#type} values in this list.
     * @param {Boolean} [cfg.excludeUnclassifiedObjects=false] When loading metadata and this is ````true````, will only load {@link Entity}s that have {@link MetaObject}s (that are not excluded). This is useful when we don't want Entitys in the Scene that are not represented within IFC navigation components, such as {@link TreeViewPlugin}.
     * @param {Boolean} [cfg.reuseGeometries=true] Indicates whether to enable geometry reuse (````true```` by default) or whether to internally expand
     * all geometry instances into batches (````false````), and not use instancing to render them. Setting this ````false```` can significantly
     * improve Viewer performance for models that have a lot of geometry reuse, but may also increase the amount of
     * browser and GPU memory they require. See [#769](https://github.com/xeokit/xeokit-sdk/issues/769) for more info.
     * @param {Number} [cfg.maxGeometryBatchSize=50000000] Maximum geometry batch size, as number of vertices. This is optionally supplied
     * to limit the size of the batched geometry arrays that {@link SceneModel} internally creates for batched geometries.
     * A low value means less heap allocation/de-allocation while loading batched geometries, but more draw calls and
     * slower rendering speed. A high value means larger heap allocation/de-allocation while loading, but less draw calls
     * and faster rendering speed. It's recommended to keep this somewhere roughly between ````50000```` and ````50000000```.
     * @param {KTX2TextureTranscoder} [cfg.textureTranscoder] Transcoder used internally to transcode KTX2
     * textures within the XKT. Only required when the XKT is version 10 or later, and contains KTX2 textures.
     */
    constructor(viewer, cfg = {}) {

        super("XKTLoader", viewer, cfg);

        this._maxGeometryBatchSize = cfg.maxGeometryBatchSize;

        this.textureTranscoder = cfg.textureTranscoder;
        this.dataSource = cfg.dataSource;
        this.objectDefaults = cfg.objectDefaults;
        this.includeTypes = cfg.includeTypes;
        this.excludeTypes = cfg.excludeTypes;
        this.excludeUnclassifiedObjects = cfg.excludeUnclassifiedObjects;
        this.reuseGeometries = cfg.reuseGeometries;
    }

    /**
     * Gets the ````.xkt```` format versions supported by this XKTLoaderPlugin/
     * @returns {string[]}
     */
    get supportedVersions() {
        return Object.keys(parsers);
    }

    /**
     * Gets the texture transcoder.
     *
     * @type {TextureTranscoder}
     */
    get textureTranscoder() {
        return this._textureTranscoder;
    }

    /**
     * Sets the texture transcoder.
     *
     * @type {TextureTranscoder}
     */
    set textureTranscoder(textureTranscoder) {
        this._textureTranscoder = textureTranscoder;
    }

    /**
     * Gets the custom data source through which the XKTLoaderPlugin can load models and metadata.
     *
     * Default value is {@link XKTDefaultDataSource}, which loads via HTTP.
     *
     * @type {Object}
     */
    get dataSource() {
        return this._dataSource;
    }

    /**
     * Sets a custom data source through which the XKTLoaderPlugin can load models and metadata.
     *
     * Default value is {@link XKTDefaultDataSource}, which loads via HTTP.
     *
     * @type {Object}
     */
    set dataSource(value) {
        this._dataSource = value || new XKTDefaultDataSource();
    }

    /**
     * Gets map of initial default states for each loaded {@link Entity} that represents an object.
     *
     * Default value is {@link IFCObjectDefaults}.
     *
     * @type {{String: Object}}
     */
    get objectDefaults() {
        return this._objectDefaults;
    }

    /**
     * Sets map of initial default states for each loaded {@link Entity} that represents an object.
     *
     * Default value is {@link IFCObjectDefaults}.
     *
     * @type {{String: Object}}
     */
    set objectDefaults(value) {
        this._objectDefaults = value || IFCObjectDefaults;
    }

    /**
     * Gets the whitelist of the IFC types loaded by this XKTLoaderPlugin.
     *
     * When loading models with metadata, causes this XKTLoaderPlugin to only load objects whose types are in this
     * list. An object's type is indicated by its {@link MetaObject}'s {@link MetaObject#type}.
     *
     * Default value is ````undefined````.
     *
     * @type {String[]}
     */
    get includeTypes() {
        return this._includeTypes;
    }

    /**
     * Sets the whitelist of the IFC types loaded by this XKTLoaderPlugin.
     *
     * When loading models with metadata, causes this XKTLoaderPlugin to only load objects whose types are in this
     * list. An object's type is indicated by its {@link MetaObject}'s {@link MetaObject#type}.
     *
     * Default value is ````undefined````.
     *
     * @type {String[]}
     */
    set includeTypes(value) {
        this._includeTypes = value;
    }

    /**
     * Gets the blacklist of IFC types that are never loaded by this XKTLoaderPlugin.
     *
     * When loading models with metadata, causes this XKTLoaderPlugin to **not** load objects whose types are in this
     * list. An object's type is indicated by its {@link MetaObject}'s {@link MetaObject#type}.
     *
     * Default value is ````undefined````.
     *
     * @type {String[]}
     */
    get excludeTypes() {
        return this._excludeTypes;
    }

    /**
     * Sets the blacklist of IFC types that are never loaded by this XKTLoaderPlugin.
     *
     * When loading models with metadata, causes this XKTLoaderPlugin to **not** load objects whose types are in this
     * list. An object's type is indicated by its {@link MetaObject}'s {@link MetaObject#type}.
     *
     * Default value is ````undefined````.
     *
     * @type {String[]}
     */
    set excludeTypes(value) {
        this._excludeTypes = value;
    }

    /**
     * Gets the whitelist of the specified elements loaded by this XKTLoaderPlugin.
     *
     * When loading models with metadata, causes this XKTLoaderPlugin to only load objects whose ids are in this
     * list. An object's id is indicated by its {@link MetaObject}'s {@link MetaObject#id}.
     *
     * Default value is ````undefined````.
     *
     * @type {String[]}
     */
    get includeIds() {
        return this._includeIds;
    }

    /**
     * Sets the whitelist of the specified elements by this XKTLoaderPlugin.
     *
     * When loading models with metadata, causes this XKTLoaderPlugin to only load objects whose ids are in this
     * list. An object's id is indicated by its {@link MetaObject}'s {@link MetaObject#id}.
     *
     * Default value is ````undefined````.
     *
     * @type {String[]}
     */
    set includeIds(value) {
        this._includeIds = value;
    }

    /**
     * Gets whether we load objects that don't have IFC types.
     *
     * When loading models with metadata and this is ````true````, XKTLoaderPlugin will not load objects
     * that don't have IFC types.
     *
     * Default value is ````false````.
     *
     * @type {Boolean}
     */
    get excludeUnclassifiedObjects() {
        return this._excludeUnclassifiedObjects;
    }

    /**
     * Sets whether we load objects that don't have IFC types.
     *
     * When loading models with metadata and this is ````true````, XKTLoaderPlugin will not load objects
     * that don't have IFC types.
     *
     * Default value is ````false````.
     *
     * @type {Boolean}
     */
    set excludeUnclassifiedObjects(value) {
        this._excludeUnclassifiedObjects = !!value;
    }

    /**
     * Gets whether XKTLoaderPlugin globalizes each {@link Entity#id} and {@link MetaObject#id} as it loads a model.
     *
     * Default value is ````false````.
     *
     * @type {Boolean}
     */
    get globalizeObjectIds() {
        return this._globalizeObjectIds;
    }

    /**
     * Sets whether XKTLoaderPlugin globalizes each {@link Entity#id} and {@link MetaObject#id} as it loads a model.
     *
     * Set  this ````true```` when you need to load multiple instances of the same model, to avoid ID clashes
     * between the objects in the different instances.
     *
     * When we load a model with this set ````true````, then each {@link Entity#id} and {@link MetaObject#id} will be
     * prefixed by the ID of the model, ie. ````<modelId>#<objectId>````.
     *
     * {@link Entity#originalSystemId} and {@link MetaObject#originalSystemId} will always hold the original, un-prefixed, ID values.
     *
     * Default value is ````false````.
     *
     * See the main {@link XKTLoaderPlugin} class documentation for usage info.
     *
     * @type {Boolean}
     */
    set globalizeObjectIds(value) {
        this._globalizeObjectIds = !!value;
    }

    /**
     * Gets whether XKTLoaderPlugin enables geometry reuse when loading models.
     *
     * Default value is ````true````.
     *
     * @type {Boolean}
     */
    get reuseGeometries() {
        return this._reuseGeometries;
    }

    /**
     * Sets whether XKTLoaderPlugin enables geometry reuse when loading models.
     *
     * Default value is ````true````.
     *
     * Geometry reuse saves memory, but can impact Viewer performance when there are many reused geometries. For
     * this reason, we can set this ````false```` to disable geometry reuse for models loaded by this XKTLoaderPlugin
     * (which will then "expand" the geometry instances into batches instead).
     *
     * The result will be be less WebGL draw calls (which are expensive), at the cost of increased memory footprint.
     *
     * See [#769](https://github.com/xeokit/xeokit-sdk/issues/769) for more info.
     *
     * @type {Boolean}
     */
    set reuseGeometries(value) {
        this._reuseGeometries = value !== false;
    }

    /**
     * Loads an ````.xkt```` model into this XKTLoaderPlugin's {@link Viewer}.
     *
     * Since xeokit/xeokit-sdk 1.9.0, XKTLoaderPlugin has supported XKT 8, which bundles the metamodel
     * data (eg. an IFC element hierarchy) in the XKT file itself. For XKT 8, we therefore no longer need to
     * load the metamodel data from a separate accompanying JSON file, as we did with previous XKT versions.
     * However, if we do choose to specify a separate metamodel JSON file to load (eg. for backward compatibility
     * in data pipelines), then that metamodel will be loaded and the metamodel in the XKT 8 file will be ignored.
     *
     * @param {*} params Loading parameters.
     * @param {String} [params.id] ID to assign to the root {@link Entity#id}, unique among all components in the Viewer's {@link Scene}, generated automatically by default.
     * @param {String} [params.src] Path or URL to an *````.xkt````* file, as an alternative to the ````xkt```` parameter.
     * @param {ArrayBuffer} [params.xkt] The *````.xkt````* file data, as an alternative to the ````src```` parameter.
     * @param {String} [params.metaModelSrc] Path or URL to an optional metadata file, as an alternative to the ````metaModelData```` parameter.
     * @param {*} [params.metaModelData] JSON model metadata, as an alternative to the ````metaModelSrc```` parameter.
     * @param {String} [params.manifestSrc] Path or URL to a JSON manifest file that provides paths to ````.xkt```` files to load as parts of the model. Use this option to load models that have been split into
     * multiple XKT files. See [tutorial](https://www.notion.so/xeokit/Automatically-Splitting-Large-Models-for-Better-Performance-165fc022e94742cf966ee50003572259) for more info.
     * @param {Object} [params.manifest] A JSON manifest object (as an alternative to a path or URL) that provides paths to ````.xkt```` files to load as parts of the model. Use this option to load models that have been split into
     * multiple XKT files. See [tutorial](https://www.notion.so/xeokit/Automatically-Splitting-Large-Models-for-Better-Performance-165fc022e94742cf966ee50003572259) for more info.
     * @param {{String:Object}} [params.objectDefaults] Map of initial default states for each loaded {@link Entity} that represents an object. Default value is {@link IFCObjectDefaults}.
     * @param {String[]} [params.includeTypes] When loading metadata, only loads objects that have {@link MetaObject}s with {@link MetaObject#type} values in this list.
     * @param {String[]} [params.excludeTypes] When loading metadata, never loads objects that have {@link MetaObject}s with {@link MetaObject#type} values in this list.
     * @param {String[]} [params.includeIds] When loading metadata, only loads objects that have {@link MetaObject}s with {@link MetaObject#id} values in this list.
     * @param {Boolean} [params.edges=false] Whether or not xeokit renders the model with edges emphasized.
     * @param {Number[]} [params.origin=[0,0,0]] The model's World-space double-precision 3D origin. Use this to position the model within xeokit's World coordinate system, using double-precision coordinates.
     * @param {Number[]} [params.position=[0,0,0]] The model single-precision 3D position, relative to the ````origin```` parameter.
     * @param {Number[]} [params.scale=[1,1,1]] The model's scale.
     * @param {Number[]} [params.rotation=[0,0,0]] The model's orientation, given as Euler angles in degrees, for each of the X, Y and Z axis.
     * @param {Number[]} [params.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]] The model's world transform matrix. Overrides the position, scale and rotation parameters. Relative to ````origin````.
     * @param {Boolean} [params.edges=false] Indicates if the model's edges are initially emphasized.
     * @param {Boolean} [params.saoEnabled=true] Indicates if Scalable Ambient Obscurance (SAO) is enabled for the model. SAO is configured by the Scene's {@link SAO} component. Only works when {@link SAO#enabled} is also ````true````
     * @param {Boolean} [params.pbrEnabled=true] Indicates if physically-based rendering (PBR) is enabled for the model. Overrides ````colorTextureEnabled````. Only works when {@link Scene#pbrEnabled} is also ````true````.
     * @param {Boolean} [params.colorTextureEnabled=true] Indicates if base color texture rendering is enabled for the model. Overridden by ````pbrEnabled````.  Only works when {@link Scene#colorTextureEnabled} is also ````true````.
     * @param {Number} [params.backfaces=false] When we set this ````true````, then we force rendering of backfaces for the model. When
     * we leave this ````false````, then we allow the Viewer to decide when to render backfaces. In that case, the
     * Viewer will hide backfaces on watertight meshes, show backfaces on open meshes, and always show backfaces on meshes when we slice them open with {@link SectionPlane}s.
     * @param {Boolean} [params.excludeUnclassifiedObjects=false] When loading metadata and this is ````true````, will only load {@link Entity}s that have {@link MetaObject}s (that are not excluded). This is useful when we don't want Entitys in the Scene that are not represented within IFC navigation components, such as {@link TreeViewPlugin}.
     * @param {Boolean} [params.globalizeObjectIds=false] Indicates whether to globalize each {@link Entity#id} and {@link MetaObject#id}, in case you need to prevent ID clashes with other models. See {@link XKTLoaderPlugin#globalizeObjectIds} for more info.
     * @param {Boolean} [params.reuseGeometries=true] Indicates whether to enable geometry reuse (````true```` by default) or whether to expand
     * all geometry instances into batches (````false````), and not use instancing to render them. Setting this ````false```` can significantly
     * improve Viewer performance for models that have excessive geometry reuse, but may also increases the amount of
     * browser and GPU memory used by the model. See [#769](https://github.com/xeokit/xeokit-sdk/issues/769) for more info.
     * @param {Boolean} [params.dtxEnabled=true] When ````true```` (default) use data textures (DTX), where appropriate, to
     * represent the returned model. Set false to always use vertex buffer objects (VBOs). Note that DTX is only applicable
     * to non-textured triangle meshes, and that VBOs are always used for meshes that have textures, line segments, or point
     * primitives. Only works while {@link DTX#enabled} is also ````true````.
     * @param {Number} [params.renderOrder=0] Specifies the rendering order for the model. This is used to control the order in which
     * SceneModels are drawn when they have transparent objects, to give control over the order in which those objects are blended within the transparent
     * render pass.
     * @returns {Entity} Entity representing the model, which will have {@link Entity#isModel} set ````true```` and will be registered by {@link Entity#id} in {@link Scene#models}.
     */
    load(params = {}) {

        if (params.id && this.viewer.scene.components[params.id]) {
            this.error("Component with this ID already exists in viewer: " + params.id + " - will autogenerate this ID");
            delete params.id;
        }

        if (!params.src && !params.xkt && !params.manifestSrc && !params.manifest)
            throw new Error("XKTLoaderPlugin: load() param expected: src, xkt, manifestSrc or manifestData");

        const options = {};
        const includeTypes = params.includeTypes || this._includeTypes;
        const excludeTypes = params.excludeTypes || this._excludeTypes;
        const includeIds = params.includeIds || this._includeIds;
        const objectDefaults = params.objectDefaults || this._objectDefaults;

        options.reuseGeometries = (params.reuseGeometries !== null && params.reuseGeometries !== undefined) ? params.reuseGeometries : (this._reuseGeometries !== false);

        if (includeTypes) {
            options.includeTypesMap = {};
            for (let i = 0, len = includeTypes.length; i < len; i++) {
                options.includeTypesMap[includeTypes[i]] = true;
            }
        }

        if (excludeTypes) {
            options.excludeTypesMap = {};
            for (let i = 0, len = excludeTypes.length; i < len; i++) {
                options.excludeTypesMap[excludeTypes[i]] = true;
            }
        }

        if (includeIds) {
            options.includeIdsMap = {};
            for (let i = 0, len = includeIds.length; i < len; i++) {
                options.includeIdsMap[includeIds[i]] = true;
            }
        }

        if (objectDefaults) {
            options.objectDefaults = objectDefaults;
        }

        options.excludeUnclassifiedObjects = (params.excludeUnclassifiedObjects !== undefined) ? (!!params.excludeUnclassifiedObjects) : this._excludeUnclassifiedObjects;
        options.globalizeObjectIds = (params.globalizeObjectIds !== undefined && params.globalizeObjectIds !== null) ? (!!params.globalizeObjectIds) : this._globalizeObjectIds;

        const sceneModel = new SceneModel(this.viewer.scene, utils.apply(params, {
            isModel: true,
            textureTranscoder: this._textureTranscoder,
            maxGeometryBatchSize: this._maxGeometryBatchSize,
            origin: params.origin,
            disableVertexWelding: params.disableVertexWelding || false,
            disableIndexRebucketing: params.disableIndexRebucketing || false,
            dtxEnabled: params.dtxEnabled,
            renderOrder: params.renderOrder
        }));

        const modelId = sceneModel.id;  // In case ID was auto-generated

        const metaModel = new MetaModel({
            metaScene: this.viewer.metaScene,
            id: modelId
        });

        this.viewer.scene.canvas.spinner.processes++;

        const finish = () => {
            if (sceneModel.destroyed) {
                return;
            }
            // this._createDefaultMetaModelIfNeeded(sceneModel, params, options);
            sceneModel.finalize();
            metaModel.finalize();
            this.viewer.scene.canvas.spinner.processes--;
            sceneModel.once("destroyed", () => {
                this.viewer.metaScene.destroyMetaModel(metaModel.id);
            });
            this.scheduleTask(() => {
                if (sceneModel.destroyed) {
                    return;
                }
                sceneModel.scene.fire("modelLoaded", sceneModel.id); // FIXME: Assumes listeners know order of these two events
                sceneModel.fire("loaded", true, false); // Don't forget the event, for late subscribers
            });
        };

        const error = (errMsg) => {
            this.viewer.scene.canvas.spinner.processes--;
            this.error(errMsg);
            sceneModel.fire("error", errMsg);
        };

        let nextId = 0;
        const manifestCtx = {
            getNextId: () => {
                return `${modelId}.${nextId++}`;
            }
        };

        if (params.metaModelSrc || params.metaModelData) {

            if (params.metaModelSrc) {

                const metaModelSrc = params.metaModelSrc;

                this._dataSource.getMetaModel(metaModelSrc, (metaModelData) => {
                    if (sceneModel.destroyed) {
                        return;
                    }
                    metaModel.loadData(metaModelData, {
                        includeTypes: includeTypes,
                        excludeTypes: excludeTypes,
                        globalizeObjectIds: options.globalizeObjectIds
                    });
                    if (params.src) {
                        this._loadModel(params.src, options, sceneModel, null, manifestCtx, finish, error);
                    } else {
                        this._parseModel(params.xkt, options, sceneModel, null, manifestCtx);
                        finish();
                    }
                }, (errMsg) => {
                    error(`load(): Failed to load model metadata for model '${modelId} from  '${metaModelSrc}' - ${errMsg}`);
                });

            } else if (params.metaModelData) {
                metaModel.loadData(params.metaModelData, {
                    includeTypes: includeTypes,
                    excludeTypes: excludeTypes,
                    globalizeObjectIds: options.globalizeObjectIds
                });
                if (params.src) {
                    this._loadModel(params.src, options, sceneModel, null, manifestCtx, finish, error);
                } else {
                    this._parseModel(params.xkt, options, sceneModel, null, manifestCtx);
                    finish();
                }
            }


        } else {

            if (params.src) {
                this._loadModel(params.src, options, sceneModel, metaModel, manifestCtx, finish, error);
            } else if (params.xkt) {
                this._parseModel(params.xkt, options, sceneModel, metaModel, manifestCtx);
                finish();
            } else if (params.manifestSrc || params.manifest) {
                const baseDir = params.manifestSrc ? getBaseDirectory$1(params.manifestSrc) : "";
                const loadJSONs = (metaDataFiles, done, error) => {
                    let i = 0;
                    const loadNext = () => {
                        if (sceneModel.destroyed) {
                            done();
                        } else if (i >= metaDataFiles.length) {
                            done();
                        } else {
                            this._dataSource.getMetaModel(`${baseDir}${metaDataFiles[i]}`, (metaModelData) => {
                                metaModel.loadData(metaModelData, {
                                    includeTypes: includeTypes,
                                    excludeTypes: excludeTypes,
                                    globalizeObjectIds: options.globalizeObjectIds
                                });
                                i++;
                                this.scheduleTask(loadNext, 200);
                            }, error);
                        }
                    };
                    loadNext();
                };
                const loadXKTs_excludeTheirMetaModels = (xktFiles, done, error) => { // Load XKTs, ignore metamodels in the XKT
                    let i = 0;
                    const loadNext = () => {
                        if (sceneModel.destroyed) {
                            done();
                        } else if (i >= xktFiles.length) {
                            done();
                        } else {
                            this._dataSource.getXKT(`${baseDir}${xktFiles[i]}`, (arrayBuffer) => {
                                this._parseModel(arrayBuffer, options, sceneModel, null /* Ignore metamodel in XKT */, manifestCtx);
                                sceneModel.preFinalize();
                                i++;
                                this.scheduleTask(loadNext, 200);
                            }, error);
                        }
                    };
                    loadNext();
                };
                const loadXKTs_includeTheirMetaModels = (xktFiles, done, error) => { // Load XKTs, parse metamodels from the XKT
                    let i = 0;
                    const loadNext = () => {
                        if (sceneModel.destroyed) {
                            done();
                        } else if (i >= xktFiles.length) {
                            done();
                        } else {
                            this._dataSource.getXKT(`${baseDir}${xktFiles[i]}`, (arrayBuffer) => {
                                this._parseModel(arrayBuffer, options, sceneModel, metaModel, manifestCtx);
                                sceneModel.preFinalize();
                                i++;
                                this.scheduleTask(loadNext, 200);
                            }, error);
                        }
                    };
                    loadNext();
                };
                if (params.manifest) {
                    const manifestData = params.manifest;
                    const xktFiles = manifestData.xktFiles;
                    if (!xktFiles || xktFiles.length === 0) {
                        error(`load(): Failed to load model manifest - manifest not valid`);
                        return;
                    }
                    const metaModelFiles = manifestData.metaModelFiles;
                    if (metaModelFiles) {
                        loadJSONs(metaModelFiles, () => {
                            loadXKTs_excludeTheirMetaModels(xktFiles, finish, error);
                        }, error);
                    } else {
                        loadXKTs_includeTheirMetaModels(xktFiles, finish, error);
                    }
                } else {
                    this._dataSource.getManifest(params.manifestSrc, (manifestData) => {
                        if (sceneModel.destroyed) {
                            return;
                        }
                        const xktFiles = manifestData.xktFiles;
                        if (!xktFiles || xktFiles.length === 0) {
                            error(`load(): Failed to load model manifest - manifest not valid`);
                            return;
                        }
                        const metaModelFiles = manifestData.metaModelFiles;
                        if (metaModelFiles) {
                            loadJSONs(metaModelFiles, () => {
                                loadXKTs_excludeTheirMetaModels(xktFiles, finish, error);
                            }, error);
                        } else {
                            loadXKTs_includeTheirMetaModels(xktFiles, finish, error);
                        }
                    }, error);
                }
            }
        }

        return sceneModel;
    }

    _loadModel(src, options, sceneModel, metaModel, manifestCtx, done, error) {
        this._dataSource.getXKT(src, (arrayBuffer) => {
            this._parseModel(arrayBuffer, options, sceneModel, metaModel, manifestCtx);
            sceneModel.preFinalize();
            done();
        }, error);
    }

    async _parseModel(arrayBuffer, options, sceneModel, metaModel, manifestCtx) {
        if (sceneModel.destroyed) {
            return;
        }
        const dataView = new DataView(arrayBuffer);
        const dataArray = new Uint8Array(arrayBuffer);
        const firstUint = dataView.getUint32(0, true);
        const xktVersion = firstUint & 0x7fffffff;
        const zipped = (xktVersion < 11) || ((xktVersion >= 12) && (firstUint >>> 31));
        const parser = parsers[xktVersion];
        if (!parser) {
            this.error("Unsupported .XKT file version: " + xktVersion + " - this XKTLoaderPlugin supports versions " + Object.keys(parsers));
            return;
        }
        //   this.log("Loading .xkt V" + xktVersion);

        if (!zipped) {
            parser.parseArrayBuffer(this.viewer, options, arrayBuffer, sceneModel, metaModel, manifestCtx);
            return;
        }

        const numElements = dataView.getUint32(4, true);
        const elements = [];
        let byteOffset = (numElements + 2) * 4;
        for (let i = 0; i < numElements; i++) {
            const elementSize = dataView.getUint32((i + 2) * 4, true);
            elements.push(dataArray.subarray(byteOffset, byteOffset + elementSize));
            byteOffset += elementSize;
        }
        parser.parse(this.viewer, options, elements, sceneModel, metaModel, manifestCtx);
    }
}

function getBaseDirectory$1(filePath) {
    if (filePath.indexOf('?') > -1) {
        filePath = filePath.split('?')[0];
    }

    const pathArray = filePath.split('/');
    pathArray.pop(); // Remove the file name or the last segment of the path
    return pathArray.join('/') + '/';
}

/*
 Copyright (c) 2013 Gildas Lormeau. All rights reserved.

 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are met:

 1. Redistributions of source code must retain the above copyright notice,
 this list of conditions and the following disclaimer.

 2. Redistributions in binary form must reproduce the above copyright
 notice, this list of conditions and the following disclaimer in
 the documentation and/or other materials provided with the distribution.

 3. The names of the authors may not be used to endorse or promote products
 derived from this software without specific prior written permission.

 THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
 INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * @private
 * @type {{}}
 */
var zipLib = {};

(function (obj) {

    var ERR_BAD_FORMAT = "File format is not recognized.";
    var ERR_CRC = "CRC failed.";
    var ERR_ENCRYPTED = "File contains encrypted entry.";
    var ERR_ZIP64 = "File is using Zip64 (4gb+ file size).";
    var ERR_READ = "Error while reading zip file.";
    var ERR_WRITE = "Error while writing zip file.";
    var ERR_WRITE_DATA = "Error while writing file data.";
    var ERR_READ_DATA = "Error while reading file data.";
    var ERR_DUPLICATED_NAME = "File already exists.";
    var CHUNK_SIZE = 512 * 1024;

    var TEXT_PLAIN = "text/plain";

    var appendABViewSupported;
    try {
        appendABViewSupported = new Blob([new DataView(new ArrayBuffer(0))]).size === 0;
    } catch (e) {
    }

    function Crc32() {
        this.crc = -1;
    }

    Crc32.prototype.append = function append(data) {
        var crc = this.crc | 0, table = this.table;
        for (var offset = 0, len = data.length | 0; offset < len; offset++)
            crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF];
        this.crc = crc;
    };
    Crc32.prototype.get = function get() {
        return ~this.crc;
    };
    Crc32.prototype.table = (function () {
        var i, j, t, table = []; // Uint32Array is actually slower than []
        for (i = 0; i < 256; i++) {
            t = i;
            for (j = 0; j < 8; j++)
                if (t & 1)
                    t = (t >>> 1) ^ 0xEDB88320;
                else
                    t = t >>> 1;
            table[i] = t;
        }
        return table;
    })();

    // "no-op" codec
    function NOOP() {
    }

    NOOP.prototype.append = function append(bytes, onprogress) {
        return bytes;
    };
    NOOP.prototype.flush = function flush() {
    };

    function blobSlice(blob, index, length) {
        if (index < 0 || length < 0 || index + length > blob.size)
            throw new RangeError('offset:' + index + ', length:' + length + ', size:' + blob.size);
        if (blob.slice)
            return blob.slice(index, index + length);
        else if (blob.webkitSlice)
            return blob.webkitSlice(index, index + length);
        else if (blob.mozSlice)
            return blob.mozSlice(index, index + length);
        else if (blob.msSlice)
            return blob.msSlice(index, index + length);
    }

    function getDataHelper(byteLength, bytes) {
        var dataBuffer, dataArray;
        dataBuffer = new ArrayBuffer(byteLength);
        dataArray = new Uint8Array(dataBuffer);
        if (bytes)
            dataArray.set(bytes, 0);
        return {
            buffer: dataBuffer,
            array: dataArray,
            view: new DataView(dataBuffer)
        };
    }

    // Readers
    function Reader() {
    }

    function TextReader(text) {
        var that = this, blobReader;

        function init(callback, onerror) {
            var blob = new Blob([text], {
                type: TEXT_PLAIN
            });
            blobReader = new BlobReader(blob);
            blobReader.init(function () {
                that.size = blobReader.size;
                callback();
            }, onerror);
        }

        function readUint8Array(index, length, callback, onerror) {
            blobReader.readUint8Array(index, length, callback, onerror);
        }

        that.size = 0;
        that.init = init;
        that.readUint8Array = readUint8Array;
    }

    TextReader.prototype = new Reader();
    TextReader.prototype.constructor = TextReader;

    function Data64URIReader(dataURI) {
        var that = this, dataStart;

        function init(callback) {
            var dataEnd = dataURI.length;
            while (dataURI.charAt(dataEnd - 1) == "=")
                dataEnd--;
            dataStart = dataURI.indexOf(",") + 1;
            that.size = Math.floor((dataEnd - dataStart) * 0.75);
            callback();
        }

        function readUint8Array(index, length, callback) {
            var i, data = getDataHelper(length);
            var start = Math.floor(index / 3) * 4;
            var end = Math.ceil((index + length) / 3) * 4;
            var bytes = obj.atob(dataURI.substring(start + dataStart, end + dataStart));
            var delta = index - Math.floor(start / 4) * 3;
            for (i = delta; i < delta + length; i++)
                data.array[i - delta] = bytes.charCodeAt(i);
            callback(data.array);
        }

        that.size = 0;
        that.init = init;
        that.readUint8Array = readUint8Array;
    }

    Data64URIReader.prototype = new Reader();
    Data64URIReader.prototype.constructor = Data64URIReader;

    function BlobReader(blob) {
        var that = this;

        function init(callback) {
            that.size = blob.size;
            callback();
        }

        function readUint8Array(index, length, callback, onerror) {
            var reader = new FileReader();
            reader.onload = function (e) {
                callback(new Uint8Array(e.target.result));
            };
            reader.onerror = onerror;
            try {
                reader.readAsArrayBuffer(blobSlice(blob, index, length));
            } catch (e) {
                onerror(e);
            }
        }

        that.size = 0;
        that.init = init;
        that.readUint8Array = readUint8Array;
    }

    BlobReader.prototype = new Reader();
    BlobReader.prototype.constructor = BlobReader;

    // Writers

    function Writer() {
    }

    Writer.prototype.getData = function (callback) {
        callback(this.data);
    };

    function TextWriter(encoding) {
        var that = this, blob;

        function init(callback) {
            blob = new Blob([], {
                type: TEXT_PLAIN
            });
            callback();
        }

        function writeUint8Array(array, callback) {
            blob = new Blob([blob, appendABViewSupported ? array : array.buffer], {
                type: TEXT_PLAIN
            });
            callback();
        }

        function getData(callback, onerror) {
            var reader = new FileReader();
            reader.onload = function (e) {
                callback(e.target.result);
            };
            reader.onerror = onerror;
            reader.readAsText(blob, encoding);
        }

        that.init = init;
        that.writeUint8Array = writeUint8Array;
        that.getData = getData;
    }

    TextWriter.prototype = new Writer();
    TextWriter.prototype.constructor = TextWriter;

    function Data64URIWriter(contentType) {
        var that = this, data = "", pending = "";

        function init(callback) {
            data += "data:" + (contentType || "") + ";base64,";
            callback();
        }

        function writeUint8Array(array, callback) {
            var i, delta = pending.length, dataString = pending;
            pending = "";
            for (i = 0; i < (Math.floor((delta + array.length) / 3) * 3) - delta; i++)
                dataString += String.fromCharCode(array[i]);
            for (; i < array.length; i++)
                pending += String.fromCharCode(array[i]);
            if (dataString.length > 2)
                data += obj.btoa(dataString);
            else
                pending = dataString;
            callback();
        }

        function getData(callback) {
            callback(data + obj.btoa(pending));
        }

        that.init = init;
        that.writeUint8Array = writeUint8Array;
        that.getData = getData;
    }

    Data64URIWriter.prototype = new Writer();
    Data64URIWriter.prototype.constructor = Data64URIWriter;

    function BlobWriter(contentType) {
        var blob, that = this;

        function init(callback) {
            blob = new Blob([], {
                type: contentType
            });
            callback();
        }

        function writeUint8Array(array, callback) {
            blob = new Blob([blob, appendABViewSupported ? array : array.buffer], {
                type: contentType
            });
            callback();
        }

        function getData(callback) {
            callback(blob);
        }

        that.init = init;
        that.writeUint8Array = writeUint8Array;
        that.getData = getData;
    }

    BlobWriter.prototype = new Writer();
    BlobWriter.prototype.constructor = BlobWriter;

    /**
     * inflate/deflate core functions
     * @param worker {Worker} web worker for the task.
     * @param initialMessage {Object} initial message to be sent to the worker. should contain
     *   sn(serial number for distinguishing multiple tasks sent to the worker), and codecClass.
     *   This function may add more properties before sending.
     */
    function launchWorkerProcess(worker, initialMessage, reader, writer, offset, size, onprogress, onend, onreaderror, onwriteerror) {
        var chunkIndex = 0, index, outputSize, sn = initialMessage.sn, crc;

        function onflush() {
            worker.removeEventListener('message', onmessage, false);
            onend(outputSize, crc);
        }

        function onmessage(event) {
            var message = event.data, data = message.data, err = message.error;
            if (err) {
                err.toString = function () {
                    return 'Error: ' + this.message;
                };
                onreaderror(err);
                return;
            }
            if (message.sn !== sn)
                return;
            if (typeof message.codecTime === 'number')
                worker.codecTime += message.codecTime; // should be before onflush()
            if (typeof message.crcTime === 'number')
                worker.crcTime += message.crcTime;

            switch (message.type) {
                case 'append':
                    if (data) {
                        outputSize += data.length;
                        writer.writeUint8Array(data, function () {
                            step();
                        }, onwriteerror);
                    } else
                        step();
                    break;
                case 'flush':
                    crc = message.crc;
                    if (data) {
                        outputSize += data.length;
                        writer.writeUint8Array(data, function () {
                            onflush();
                        }, onwriteerror);
                    } else
                        onflush();
                    break;
                case 'progress':
                    if (onprogress)
                        onprogress(index + message.loaded, size);
                    break;
                case 'importScripts': //no need to handle here
                case 'newTask':
                case 'echo':
                    break;
                default:
                    console.warn('zip.js:launchWorkerProcess: unknown message: ', message);
            }
        }

        function step() {
            index = chunkIndex * CHUNK_SIZE;
            // use `<=` instead of `<`, because `size` may be 0.
            if (index <= size) {
                reader.readUint8Array(offset + index, Math.min(CHUNK_SIZE, size - index), function (array) {
                    if (onprogress)
                        onprogress(index, size);
                    var msg = index === 0 ? initialMessage : {sn: sn};
                    msg.type = 'append';
                    msg.data = array;

                    // posting a message with transferables will fail on IE10
                    try {
                        worker.postMessage(msg, [array.buffer]);
                    } catch (ex) {
                        worker.postMessage(msg); // retry without transferables
                    }
                    chunkIndex++;
                }, onreaderror);
            } else {
                worker.postMessage({
                    sn: sn,
                    type: 'flush'
                });
            }
        }

        outputSize = 0;
        worker.addEventListener('message', onmessage, false);
        step();
    }

    function launchProcess(process, reader, writer, offset, size, crcType, onprogress, onend, onreaderror, onwriteerror) {
        var chunkIndex = 0, index, outputSize = 0,
            crcInput = crcType === 'input',
            crcOutput = crcType === 'output',
            crc = new Crc32();

        function step() {
            var outputData;
            index = chunkIndex * CHUNK_SIZE;
            if (index < size)
                reader.readUint8Array(offset + index, Math.min(CHUNK_SIZE, size - index), function (inputData) {
                    var outputData;
                    try {
                        outputData = process.append(inputData, function (loaded) {
                            if (onprogress)
                                onprogress(index + loaded, size);
                        });
                    } catch (e) {
                        onreaderror(e);
                        return;
                    }
                    if (outputData) {
                        outputSize += outputData.length;
                        writer.writeUint8Array(outputData, function () {
                            chunkIndex++;
                            setTimeout(step, 1);
                        }, onwriteerror);
                        if (crcOutput)
                            crc.append(outputData);
                    } else {
                        chunkIndex++;
                        setTimeout(step, 1);
                    }
                    if (crcInput)
                        crc.append(inputData);
                    if (onprogress)
                        onprogress(index, size);
                }, onreaderror);
            else {
                try {
                    outputData = process.flush();
                } catch (e) {
                    onreaderror(e);
                    return;
                }
                if (outputData) {
                    if (crcOutput)
                        crc.append(outputData);
                    outputSize += outputData.length;
                    writer.writeUint8Array(outputData, function () {
                        onend(outputSize, crc.get());
                    }, onwriteerror);
                } else
                    onend(outputSize, crc.get());
            }
        }

        step();
    }

    function inflate(worker, sn, reader, writer, offset, size, computeCrc32, onend, onprogress, onreaderror, onwriteerror) {
        var crcType = computeCrc32 ? 'output' : 'none';
        if (obj.zip.useWebWorkers) {
            var initialMessage = {
                sn: sn,
                codecClass: 'Inflater',
                crcType: crcType,
            };
            launchWorkerProcess(worker, initialMessage, reader, writer, offset, size, onprogress, onend, onreaderror, onwriteerror);
        } else
            launchProcess(new obj.zip.Inflater(), reader, writer, offset, size, crcType, onprogress, onend, onreaderror, onwriteerror);
    }

    function deflate(worker, sn, reader, writer, level, onend, onprogress, onreaderror, onwriteerror) {
        var crcType = 'input';
        if (obj.zip.useWebWorkers) {
            var initialMessage = {
                sn: sn,
                options: {level: level},
                codecClass: 'Deflater',
                crcType: crcType,
            };
            launchWorkerProcess(worker, initialMessage, reader, writer, 0, reader.size, onprogress, onend, onreaderror, onwriteerror);
        } else
            launchProcess(new obj.zip.Deflater(), reader, writer, 0, reader.size, crcType, onprogress, onend, onreaderror, onwriteerror);
    }

    function copy(worker, sn, reader, writer, offset, size, computeCrc32, onend, onprogress, onreaderror, onwriteerror) {
        var crcType = 'input';
        if (obj.zip.useWebWorkers && computeCrc32) {
            var initialMessage = {
                sn: sn,
                codecClass: 'NOOP',
                crcType: crcType,
            };
            launchWorkerProcess(worker, initialMessage, reader, writer, offset, size, onprogress, onend, onreaderror, onwriteerror);
        } else
            launchProcess(new NOOP(), reader, writer, offset, size, crcType, onprogress, onend, onreaderror, onwriteerror);
    }

    // ZipReader

    function decodeASCII(str) {
        var i, out = "", charCode, extendedASCII = ['\u00C7', '\u00FC', '\u00E9', '\u00E2', '\u00E4', '\u00E0', '\u00E5', '\u00E7', '\u00EA', '\u00EB',
            '\u00E8', '\u00EF', '\u00EE', '\u00EC', '\u00C4', '\u00C5', '\u00C9', '\u00E6', '\u00C6', '\u00F4', '\u00F6', '\u00F2', '\u00FB', '\u00F9',
            '\u00FF', '\u00D6', '\u00DC', '\u00F8', '\u00A3', '\u00D8', '\u00D7', '\u0192', '\u00E1', '\u00ED', '\u00F3', '\u00FA', '\u00F1', '\u00D1',
            '\u00AA', '\u00BA', '\u00BF', '\u00AE', '\u00AC', '\u00BD', '\u00BC', '\u00A1', '\u00AB', '\u00BB', '_', '_', '_', '\u00A6', '\u00A6',
            '\u00C1', '\u00C2', '\u00C0', '\u00A9', '\u00A6', '\u00A6', '+', '+', '\u00A2', '\u00A5', '+', '+', '-', '-', '+', '-', '+', '\u00E3',
            '\u00C3', '+', '+', '-', '-', '\u00A6', '-', '+', '\u00A4', '\u00F0', '\u00D0', '\u00CA', '\u00CB', '\u00C8', 'i', '\u00CD', '\u00CE',
            '\u00CF', '+', '+', '_', '_', '\u00A6', '\u00CC', '_', '\u00D3', '\u00DF', '\u00D4', '\u00D2', '\u00F5', '\u00D5', '\u00B5', '\u00FE',
            '\u00DE', '\u00DA', '\u00DB', '\u00D9', '\u00FD', '\u00DD', '\u00AF', '\u00B4', '\u00AD', '\u00B1', '_', '\u00BE', '\u00B6', '\u00A7',
            '\u00F7', '\u00B8', '\u00B0', '\u00A8', '\u00B7', '\u00B9', '\u00B3', '\u00B2', '_', ' '];
        for (i = 0; i < str.length; i++) {
            charCode = str.charCodeAt(i) & 0xFF;
            if (charCode > 127)
                out += extendedASCII[charCode - 128];
            else
                out += String.fromCharCode(charCode);
        }
        return out;
    }

    function decodeUTF8(string) {
        return decodeURIComponent(escape(string));
    }

    function getString(bytes) {
        var i, str = "";
        for (i = 0; i < bytes.length; i++)
            str += String.fromCharCode(bytes[i]);
        return str;
    }

    function getDate(timeRaw) {
        var date = (timeRaw & 0xffff0000) >> 16, time = timeRaw & 0x0000ffff;
        try {
            return new Date(1980 + ((date & 0xFE00) >> 9), ((date & 0x01E0) >> 5) - 1, date & 0x001F, (time & 0xF800) >> 11, (time & 0x07E0) >> 5,
                (time & 0x001F) * 2, 0);
        } catch (e) {
        }
    }

    function readCommonHeader(entry, data, index, centralDirectory, onerror) {
        entry.version = data.view.getUint16(index, true);
        entry.bitFlag = data.view.getUint16(index + 2, true);
        entry.compressionMethod = data.view.getUint16(index + 4, true);
        entry.lastModDateRaw = data.view.getUint32(index + 6, true);
        entry.lastModDate = getDate(entry.lastModDateRaw);
        if ((entry.bitFlag & 0x01) === 0x01) {
            onerror(ERR_ENCRYPTED);
            return;
        }
        if (centralDirectory || (entry.bitFlag & 0x0008) != 0x0008) {
            entry.crc32 = data.view.getUint32(index + 10, true);
            entry.compressedSize = data.view.getUint32(index + 14, true);
            entry.uncompressedSize = data.view.getUint32(index + 18, true);
        }
        if (entry.compressedSize === 0xFFFFFFFF || entry.uncompressedSize === 0xFFFFFFFF) {
            onerror(ERR_ZIP64);
            return;
        }
        entry.filenameLength = data.view.getUint16(index + 22, true);
        entry.extraFieldLength = data.view.getUint16(index + 24, true);
    }

    function createZipReader(reader, callback, onerror) {
        var inflateSN = 0;

        function Entry() {
        }

        Entry.prototype.getData = function (writer, onend, onprogress, checkCrc32) {
            var that = this;

            function testCrc32(crc32) {
                var dataCrc32 = getDataHelper(4);
                dataCrc32.view.setUint32(0, crc32);
                return that.crc32 == dataCrc32.view.getUint32(0);
            }

            function getWriterData(uncompressedSize, crc32) {
                if (checkCrc32 && !testCrc32(crc32))
                    onerror(ERR_CRC);
                else
                    writer.getData(function (data) {
                        onend(data);
                    });
            }

            function onreaderror(err) {
                onerror(err || ERR_READ_DATA);
            }

            function onwriteerror(err) {
                onerror(err || ERR_WRITE_DATA);
            }

            reader.readUint8Array(that.offset, 30, function (bytes) {
                var data = getDataHelper(bytes.length, bytes), dataOffset;
                if (data.view.getUint32(0) != 0x504b0304) {
                    onerror(ERR_BAD_FORMAT);
                    return;
                }
                readCommonHeader(that, data, 4, false, onerror);
                dataOffset = that.offset + 30 + that.filenameLength + that.extraFieldLength;
                writer.init(function () {
                    if (that.compressionMethod === 0)
                        copy(that._worker, inflateSN++, reader, writer, dataOffset, that.compressedSize, checkCrc32, getWriterData, onprogress, onreaderror, onwriteerror);
                    else
                        inflate(that._worker, inflateSN++, reader, writer, dataOffset, that.compressedSize, checkCrc32, getWriterData, onprogress, onreaderror, onwriteerror);
                }, onwriteerror);
            }, onreaderror);
        };

        function seekEOCDR(eocdrCallback) {
            // "End of central directory record" is the last part of a zip archive, and is at least 22 bytes long.
            // Zip file comment is the last part of EOCDR and has max length of 64KB,
            // so we only have to search the last 64K + 22 bytes of a archive for EOCDR signature (0x06054b50).
            var EOCDR_MIN = 22;
            if (reader.size < EOCDR_MIN) {
                onerror(ERR_BAD_FORMAT);
                return;
            }
            var ZIP_COMMENT_MAX = 256 * 256, EOCDR_MAX = EOCDR_MIN + ZIP_COMMENT_MAX;

            // In most cases, the EOCDR is EOCDR_MIN bytes long
            doSeek(EOCDR_MIN, function () {
                // If not found, try within EOCDR_MAX bytes
                doSeek(Math.min(EOCDR_MAX, reader.size), function () {
                    onerror(ERR_BAD_FORMAT);
                });
            });

            // seek last length bytes of file for EOCDR
            function doSeek(length, eocdrNotFoundCallback) {
                reader.readUint8Array(reader.size - length, length, function (bytes) {
                    for (var i = bytes.length - EOCDR_MIN; i >= 0; i--) {
                        if (bytes[i] === 0x50 && bytes[i + 1] === 0x4b && bytes[i + 2] === 0x05 && bytes[i + 3] === 0x06) {
                            eocdrCallback(new DataView(bytes.buffer, i, EOCDR_MIN));
                            return;
                        }
                    }
                    eocdrNotFoundCallback();
                }, function () {
                    onerror(ERR_READ);
                });
            }
        }

        var zipReader = {
            getEntries: function (callback) {
                var worker = this._worker;
                // look for End of central directory record
                seekEOCDR(function (dataView) {
                    var datalength, fileslength;
                    datalength = dataView.getUint32(16, true);
                    fileslength = dataView.getUint16(8, true);
                    if (datalength < 0 || datalength >= reader.size) {
                        onerror(ERR_BAD_FORMAT);
                        return;
                    }
                    reader.readUint8Array(datalength, reader.size - datalength, function (bytes) {
                        var i, index = 0, entries = [], entry, filename, comment, data = getDataHelper(bytes.length, bytes);
                        for (i = 0; i < fileslength; i++) {
                            entry = new Entry();
                            entry._worker = worker;
                            if (data.view.getUint32(index) != 0x504b0102) {
                                onerror(ERR_BAD_FORMAT);
                                return;
                            }
                            readCommonHeader(entry, data, index + 6, true, onerror);
                            entry.commentLength = data.view.getUint16(index + 32, true);
                            entry.directory = ((data.view.getUint8(index + 38) & 0x10) == 0x10);
                            entry.offset = data.view.getUint32(index + 42, true);
                            filename = getString(data.array.subarray(index + 46, index + 46 + entry.filenameLength));
                            entry.filename = ((entry.bitFlag & 0x0800) === 0x0800) ? decodeUTF8(filename) : decodeASCII(filename);
                            if (!entry.directory && entry.filename.charAt(entry.filename.length - 1) == "/")
                                entry.directory = true;
                            comment = getString(data.array.subarray(index + 46 + entry.filenameLength + entry.extraFieldLength, index + 46
                                + entry.filenameLength + entry.extraFieldLength + entry.commentLength));
                            entry.comment = ((entry.bitFlag & 0x0800) === 0x0800) ? decodeUTF8(comment) : decodeASCII(comment);
                            entries.push(entry);
                            index += 46 + entry.filenameLength + entry.extraFieldLength + entry.commentLength;
                        }
                        callback(entries);
                    }, function () {
                        onerror(ERR_READ);
                    });
                });
            },
            close: function (callback) {
                if (this._worker) {
                    this._worker.terminate();
                    this._worker = null;
                }
                if (callback)
                    callback();
            },
            _worker: null
        };

        if (!obj.zip.useWebWorkers)
            callback(zipReader);
        else {
            createWorker('inflater',
                function (worker) {
                    zipReader._worker = worker;
                    callback(zipReader);
                },
                function (err) {
                    onerror(err);
                }
            );
        }
    }

    // ZipWriter

    function encodeUTF8(string) {
        return unescape(encodeURIComponent(string));
    }

    function getBytes(str) {
        var i, array = [];
        for (i = 0; i < str.length; i++)
            array.push(str.charCodeAt(i));
        return array;
    }

    function createZipWriter(writer, callback, onerror, dontDeflate) {
        var files = {}, filenames = [], datalength = 0;
        var deflateSN = 0;

        function onwriteerror(err) {
            onerror(err || ERR_WRITE);
        }

        function onreaderror(err) {
            onerror(err || ERR_READ_DATA);
        }

        var zipWriter = {
            add: function (name, reader, onend, onprogress, options) {
                var header, filename, date;
                var worker = this._worker;

                function writeHeader(callback) {
                    var data;
                    date = options.lastModDate || new Date();
                    header = getDataHelper(26);
                    files[name] = {
                        headerArray: header.array,
                        directory: options.directory,
                        filename: filename,
                        offset: datalength,
                        comment: getBytes(encodeUTF8(options.comment || ""))
                    };
                    header.view.setUint32(0, 0x14000808);
                    if (options.version)
                        header.view.setUint8(0, options.version);
                    if (!dontDeflate && options.level !== 0 && !options.directory)
                        header.view.setUint16(4, 0x0800);
                    header.view.setUint16(6, (((date.getHours() << 6) | date.getMinutes()) << 5) | date.getSeconds() / 2, true);
                    header.view.setUint16(8, ((((date.getFullYear() - 1980) << 4) | (date.getMonth() + 1)) << 5) | date.getDate(), true);
                    header.view.setUint16(22, filename.length, true);
                    data = getDataHelper(30 + filename.length);
                    data.view.setUint32(0, 0x504b0304);
                    data.array.set(header.array, 4);
                    data.array.set(filename, 30);
                    datalength += data.array.length;
                    writer.writeUint8Array(data.array, callback, onwriteerror);
                }

                function writeFooter(compressedLength, crc32) {
                    var footer = getDataHelper(16);
                    datalength += compressedLength || 0;
                    footer.view.setUint32(0, 0x504b0708);
                    if (typeof crc32 != "undefined") {
                        header.view.setUint32(10, crc32, true);
                        footer.view.setUint32(4, crc32, true);
                    }
                    if (reader) {
                        footer.view.setUint32(8, compressedLength, true);
                        header.view.setUint32(14, compressedLength, true);
                        footer.view.setUint32(12, reader.size, true);
                        header.view.setUint32(18, reader.size, true);
                    }
                    writer.writeUint8Array(footer.array, function () {
                        datalength += 16;
                        onend();
                    }, onwriteerror);
                }

                function writeFile() {
                    options = options || {};
                    name = name.trim();
                    if (options.directory && name.charAt(name.length - 1) != "/")
                        name += "/";
                    if (files.hasOwnProperty(name)) {
                        onerror(ERR_DUPLICATED_NAME);
                        return;
                    }
                    filename = getBytes(encodeUTF8(name));
                    filenames.push(name);
                    writeHeader(function () {
                        if (reader)
                            if (dontDeflate || options.level === 0)
                                copy(worker, deflateSN++, reader, writer, 0, reader.size, true, writeFooter, onprogress, onreaderror, onwriteerror);
                            else
                                deflate(worker, deflateSN++, reader, writer, options.level, writeFooter, onprogress, onreaderror, onwriteerror);
                        else
                            writeFooter();
                    });
                }

                if (reader)
                    reader.init(writeFile, onreaderror);
                else
                    writeFile();
            },
            close: function (callback) {
                if (this._worker) {
                    this._worker.terminate();
                    this._worker = null;
                }

                var data, length = 0, index = 0, indexFilename, file;
                for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {
                    file = files[filenames[indexFilename]];
                    length += 46 + file.filename.length + file.comment.length;
                }
                data = getDataHelper(length + 22);
                for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {
                    file = files[filenames[indexFilename]];
                    data.view.setUint32(index, 0x504b0102);
                    data.view.setUint16(index + 4, 0x1400);
                    data.array.set(file.headerArray, index + 6);
                    data.view.setUint16(index + 32, file.comment.length, true);
                    if (file.directory)
                        data.view.setUint8(index + 38, 0x10);
                    data.view.setUint32(index + 42, file.offset, true);
                    data.array.set(file.filename, index + 46);
                    data.array.set(file.comment, index + 46 + file.filename.length);
                    index += 46 + file.filename.length + file.comment.length;
                }
                data.view.setUint32(index, 0x504b0506);
                data.view.setUint16(index + 8, filenames.length, true);
                data.view.setUint16(index + 10, filenames.length, true);
                data.view.setUint32(index + 12, length, true);
                data.view.setUint32(index + 16, datalength, true);
                writer.writeUint8Array(data.array, function () {
                    writer.getData(callback);
                }, onwriteerror);
            },
            _worker: null
        };

        if (!obj.zip.useWebWorkers)
            callback(zipWriter);
        else {
            createWorker('deflater',
                function (worker) {
                    zipWriter._worker = worker;
                    callback(zipWriter);
                },
                function (err) {
                    onerror(err);
                }
            );
        }
    }

    function resolveURLs(urls) {
        var a = document.createElement('a');
        return urls.map(function (url) {
            a.href = url;
            return a.href;
        });
    }

    var DEFAULT_WORKER_SCRIPTS = {
        deflater: ['z-worker.js', 'deflate.js'],
        inflater: ['z-worker.js', 'inflate.js']
    };

    function createWorker(type, callback, onerror) {
        if (obj.zip.workerScripts !== null && obj.zip.workerScriptsPath !== null) {
            onerror(new Error('Either zip.workerScripts or zip.workerScriptsPath may be set, not both.'));
            return;
        }
        var scripts;
        if (obj.zip.workerScripts) {
            scripts = obj.zip.workerScripts[type];
            if (!Array.isArray(scripts)) {
                onerror(new Error('zip.workerScripts.' + type + ' is not an array!'));
                return;
            }
            scripts = resolveURLs(scripts);
        } else {
            scripts = DEFAULT_WORKER_SCRIPTS[type].slice(0);
            scripts[0] = (obj.zip.workerScriptsPath || '') + scripts[0];
        }
        var worker = new Worker(scripts[0]);
        // record total consumed time by inflater/deflater/crc32 in this worker
        worker.codecTime = worker.crcTime = 0;
        worker.postMessage({type: 'importScripts', scripts: scripts.slice(1)});
        worker.addEventListener('message', onmessage);
        function onmessage(ev) {
            var msg = ev.data;
            if (msg.error) {
                worker.terminate(); // should before onerror(), because onerror() may throw.
                onerror(msg.error);
                return;
            }
            if (msg.type === 'importScripts') {
                worker.removeEventListener('message', onmessage);
                worker.removeEventListener('error', errorHandler);
                callback(worker);
            }
        }

        // catch entry script loading error and other unhandled errors
        worker.addEventListener('error', errorHandler);
        function errorHandler(err) {
            worker.terminate();
            onerror(err);
        }
    }

    function onerror_default(error) {
        console.error(error);
    }

    obj.zip = {
        Reader: Reader,
        Writer: Writer,
        BlobReader: BlobReader,
        Data64URIReader: Data64URIReader,
        TextReader: TextReader,
        BlobWriter: BlobWriter,
        Data64URIWriter: Data64URIWriter,
        TextWriter: TextWriter,
        createReader: function (reader, callback, onerror) {
            onerror = onerror || onerror_default;

            reader.init(function () {
                createZipReader(reader, callback, onerror);
            }, onerror);
        },
        createWriter: function (writer, callback, onerror, dontDeflate) {
            onerror = onerror || onerror_default;
            dontDeflate = !!dontDeflate;

            writer.init(function () {
                createZipWriter(writer, callback, onerror, dontDeflate);
            }, onerror);
        },
        useWebWorkers: true,
        /**
         * Directory containing the default worker scripts (z-worker.js, deflate.js, and inflate.js), relative to current base url.
         * E.g.: zip.workerScripts = './';
         */
        workerScriptsPath: null,
        /**
         * Advanced option to control which scripts are loaded in the Web worker. If this option is specified, then workerScriptsPath must not be set.
         * workerScripts.deflater/workerScripts.inflater should be arrays of urls to scripts for deflater/inflater, respectively.
         * Scripts in the array are executed in order, and the first one should be z-worker.js, which is used to start the worker.
         * All urls are relative to current base url.
         * E.g.:
         * zip.workerScripts = {
		 *   deflater: ['z-worker.js', 'deflate.js'],
		 *   inflater: ['z-worker.js', 'inflate.js']
		 * };
         */
        workerScripts: null,
    };

})(zipLib);

/*
 Copyright (c) 2013 Gildas Lormeau. All rights reserved.

 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are met:

 1. Redistributions of source code must retain the above copyright notice,
 this list of conditions and the following disclaimer.

 2. Redistributions in binary form must reproduce the above copyright
 notice, this list of conditions and the following disclaimer in
 the documentation and/or other materials provided with the distribution.

 3. The names of the authors may not be used to endorse or promote products
 derived from this software without specific prior written permission.

 THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
 INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * @private
 */
const zipExt = function(zipObj) {

	var ERR_HTTP_RANGE = "HTTP Range not supported.";

	var Reader = zipObj.Reader;
	var Writer = zipObj.Writer;

	var ZipDirectoryEntry;

	var appendABViewSupported;
	try {
		appendABViewSupported = new Blob([new DataView(new ArrayBuffer(0))]).size === 0;
	} catch (e) {
	}

	function isHttpFamily(url) {
		var a = document.createElement("a");
		a.href = url;
		return a.protocol === "http:" || a.protocol === "https:";
	}

	function HttpReader(url) {
		var that = this;

		function getData(callback, onerror) {
			var request;
			if (!that.data) {
				request = new XMLHttpRequest();
				request.addEventListener("load", function () {
					if (!that.size)
						that.size = Number(request.getResponseHeader("Content-Length")) || Number(request.response.byteLength);
					that.data = new Uint8Array(request.response);
					callback();
				}, false);
				request.addEventListener("error", onerror, false);
				request.open("GET", url);
				request.responseType = "arraybuffer";
				request.send();
			} else
				callback();
		}

		function init(callback, onerror) {
			if (!isHttpFamily(url)) {
				// For schemas other than http(s), HTTP HEAD may be unavailable,
				// so use HTTP GET instead.
				getData(callback, onerror);
				return;
			}
			var request = new XMLHttpRequest();
			request.addEventListener("load", function () {
				that.size = Number(request.getResponseHeader("Content-Length"));
				// If response header doesn't return size then prefetch the content.
				if (!that.size) {
					getData(callback, onerror);
				} else {
					callback();
				}
			}, false);
			request.addEventListener("error", onerror, false);
			request.open("HEAD", url);
			request.send();
		}

		function readUint8Array(index, length, callback, onerror) {
			getData(function () {
				callback(new Uint8Array(that.data.subarray(index, index + length)));
			}, onerror);
		}

		that.size = 0;
		that.init = init;
		that.readUint8Array = readUint8Array;
	}

	HttpReader.prototype = new Reader();
	HttpReader.prototype.constructor = HttpReader;

	function HttpRangeReader(url) {
		var that = this;

		function init(callback, onerror) {
			var request = new XMLHttpRequest();
			request.addEventListener("load", function () {
				that.size = Number(request.getResponseHeader("Content-Length"));
				if (request.getResponseHeader("Accept-Ranges") == "bytes")
					callback();
				else
					onerror(ERR_HTTP_RANGE);
			}, false);
			request.addEventListener("error", onerror, false);
			request.open("HEAD", url);
			request.send();
		}

		function readArrayBuffer(index, length, callback, onerror) {
			var request = new XMLHttpRequest();
			request.open("GET", url);
			request.responseType = "arraybuffer";
			request.setRequestHeader("Range", "bytes=" + index + "-" + (index + length - 1));
			request.addEventListener("load", function () {
				callback(request.response);
			}, false);
			request.addEventListener("error", onerror, false);
			request.send();
		}

		function readUint8Array(index, length, callback, onerror) {
			readArrayBuffer(index, length, function (arraybuffer) {
				callback(new Uint8Array(arraybuffer));
			}, onerror);
		}

		that.size = 0;
		that.init = init;
		that.readUint8Array = readUint8Array;
	}

	HttpRangeReader.prototype = new Reader();
	HttpRangeReader.prototype.constructor = HttpRangeReader;

	function ArrayBufferReader(arrayBuffer) {
		var that = this;

		function init(callback, onerror) {
			that.size = arrayBuffer.byteLength;
			callback();
		}

		function readUint8Array(index, length, callback, onerror) {
			callback(new Uint8Array(arrayBuffer.slice(index, index + length)));
		}

		that.size = 0;
		that.init = init;
		that.readUint8Array = readUint8Array;
	}

	ArrayBufferReader.prototype = new Reader();
	ArrayBufferReader.prototype.constructor = ArrayBufferReader;

	function ArrayBufferWriter() {
		var array, that = this;

		function init(callback, onerror) {
			array = new Uint8Array();
			callback();
		}

		function writeUint8Array(arr, callback, onerror) {
			var tmpArray = new Uint8Array(array.length + arr.length);
			tmpArray.set(array);
			tmpArray.set(arr, array.length);
			array = tmpArray;
			callback();
		}

		function getData(callback) {
			callback(array.buffer);
		}

		that.init = init;
		that.writeUint8Array = writeUint8Array;
		that.getData = getData;
	}

	ArrayBufferWriter.prototype = new Writer();
	ArrayBufferWriter.prototype.constructor = ArrayBufferWriter;

	function FileWriter(fileEntry, contentType) {
		var writer, that = this;

		function init(callback, onerror) {
			fileEntry.createWriter(function (fileWriter) {
				writer = fileWriter;
				callback();
			}, onerror);
		}

		function writeUint8Array(array, callback, onerror) {
			var blob = new Blob([appendABViewSupported ? array : array.buffer], {
				type: contentType
			});
			writer.onwrite = function () {
				writer.onwrite = null;
				callback();
			};
			writer.onerror = onerror;
			writer.write(blob);
		}

		function getData(callback) {
			fileEntry.file(callback);
		}

		that.init = init;
		that.writeUint8Array = writeUint8Array;
		that.getData = getData;
	}

	FileWriter.prototype = new Writer();
	FileWriter.prototype.constructor = FileWriter;

	zipObj.FileWriter = FileWriter;
	zipObj.HttpReader = HttpReader;
	zipObj.HttpRangeReader = HttpRangeReader;
	zipObj.ArrayBufferReader = ArrayBufferReader;
	zipObj.ArrayBufferWriter = ArrayBufferWriter;

	if (zipObj.fs) {
		ZipDirectoryEntry = zipObj.fs.ZipDirectoryEntry;
		ZipDirectoryEntry.prototype.addHttpContent = function (name, URL, useRangeHeader) {
			function addChild(parent, name, params, directory) {
				if (parent.directory)
					return directory ? new ZipDirectoryEntry(parent.fs, name, params, parent) : new zipObj.fs.ZipFileEntry(parent.fs, name, params, parent);
				else
					throw "Parent entry is not a directory.";
			}

			return addChild(this, name, {
				data: URL,
				Reader: useRangeHeader ? HttpRangeReader : HttpReader
			});
		};
		ZipDirectoryEntry.prototype.importHttpContent = function (URL, useRangeHeader, onend, onerror) {
			this.importZip(useRangeHeader ? new HttpRangeReader(URL) : new HttpReader(URL), onend, onerror);
		};
		zipObj.fs.FS.prototype.importHttpContent = function (URL, useRangeHeader, onend, onerror) {
			this.entries = [];
			this.root = new ZipDirectoryEntry(this);
			this.root.importHttpContent(URL, useRangeHeader, onend, onerror);
		};
	}
};

const zip = zipLib.zip;
zipExt(zip);
const version = "0.0.3-alpha";
const packageJSON = {
	version: version};

var __defProp$2 = Object.defineProperty;
var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$2 = (obj, key, value) => {
  __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
  return value;
};
var IfcDbWasm = /* @__PURE__ */ (() => {
  return async function(moduleArg = {}) {
    var moduleRtn;
    var Module = moduleArg;
    var readyPromiseResolve, readyPromiseReject;
    var readyPromise = new Promise((resolve, reject) => {
      readyPromiseResolve = resolve;
      readyPromiseReject = reject;
    });
    var arguments_ = [];
    var thisProgram = "./this.program";
    var quit_ = (status, toThrow) => {
      throw toThrow;
    };
    var _scriptName = import.meta.url;
    var scriptDirectory = "";
    function locateFile(path) {
      if (Module["locateFile"]) {
        return Module["locateFile"](path, scriptDirectory);
      }
      return scriptDirectory + path;
    }
    var readAsync, readBinary;
    {
      try {
        scriptDirectory = new URL(".", _scriptName).href;
      } catch {
      }
      {
        readAsync = async (url) => {
          var response = await fetch(url, { credentials: "same-origin" });
          if (response.ok) {
            return response.arrayBuffer();
          }
          throw new Error(response.status + " : " + response.url);
        };
      }
    }
    var out = console.log.bind(console);
    var err = console.error.bind(console);
    var wasmBinary;
    var wasmMemory;
    var ABORT = false;
    var EXITSTATUS;
    var HEAP8, HEAPU8, HEAP16, HEAPU16, HEAP32, HEAPU32, HEAPF32, HEAP64, HEAPU64, HEAPF64;
    function updateMemoryViews() {
      var b = wasmMemory.buffer;
      HEAP8 = new Int8Array(b);
      HEAP16 = new Int16Array(b);
      HEAPU8 = new Uint8Array(b);
      HEAPU16 = new Uint16Array(b);
      HEAP32 = new Int32Array(b);
      HEAPU32 = new Uint32Array(b);
      HEAPF32 = new Float32Array(b);
      HEAPF64 = new Float64Array(b);
      HEAP64 = new BigInt64Array(b);
      HEAPU64 = new BigUint64Array(b);
    }
    function preRun() {
      if (Module["preRun"]) {
        if (typeof Module["preRun"] == "function")
          Module["preRun"] = [Module["preRun"]];
        while (Module["preRun"].length) {
          addOnPreRun(Module["preRun"].shift());
        }
      }
      callRuntimeCallbacks(onPreRuns);
    }
    function initRuntime() {
      if (!Module["noFSInit"] && !FS.initialized)
        FS.init();
      wasmExports["V"]();
      FS.ignorePermissions = false;
    }
    function postRun() {
      if (Module["postRun"]) {
        if (typeof Module["postRun"] == "function")
          Module["postRun"] = [Module["postRun"]];
        while (Module["postRun"].length) {
          addOnPostRun(Module["postRun"].shift());
        }
      }
      callRuntimeCallbacks(onPostRuns);
    }
    var runDependencies = 0;
    var dependenciesFulfilled = null;
    function addRunDependency(id) {
      runDependencies++;
      Module["monitorRunDependencies"]?.(runDependencies);
    }
    function removeRunDependency(id) {
      runDependencies--;
      Module["monitorRunDependencies"]?.(runDependencies);
      if (runDependencies == 0) {
        if (dependenciesFulfilled) {
          var callback = dependenciesFulfilled;
          dependenciesFulfilled = null;
          callback();
        }
      }
    }
    function abort(what) {
      Module["onAbort"]?.(what);
      what = "Aborted(" + what + ")";
      err(what);
      ABORT = true;
      what += ". Build with -sASSERTIONS for more info.";
      var e = new WebAssembly.RuntimeError(what);
      readyPromiseReject(e);
      throw e;
    }
    var wasmBinaryFile;
    function findWasmBinary() {
      if (Module["locateFile"]) {
        return locateFile("ifcdb.wasm");
      }
      return new URL("ifcdb.wasm", import.meta.url).href;
    }
    function getBinarySync(file) {
      if (file == wasmBinaryFile && wasmBinary) {
        return new Uint8Array(wasmBinary);
      }
      throw "both async and sync fetching of the wasm failed";
    }
    async function getWasmBinary(binaryFile) {
      if (!wasmBinary) {
        try {
          var response = await readAsync(binaryFile);
          return new Uint8Array(response);
        } catch {
        }
      }
      return getBinarySync(binaryFile);
    }
    async function instantiateArrayBuffer(binaryFile, imports) {
      try {
        var binary = await getWasmBinary(binaryFile);
        var instance = await WebAssembly.instantiate(binary, imports);
        return instance;
      } catch (reason) {
        err(`failed to asynchronously prepare wasm: ${reason}`);
        abort(reason);
      }
    }
    async function instantiateAsync(binary, binaryFile, imports) {
      if (!binary && typeof WebAssembly.instantiateStreaming == "function") {
        try {
          var response = fetch(binaryFile, { credentials: "same-origin" });
          var instantiationResult = await WebAssembly.instantiateStreaming(response, imports);
          return instantiationResult;
        } catch (reason) {
          err(`wasm streaming compile failed: ${reason}`);
          err("falling back to ArrayBuffer instantiation");
        }
      }
      return instantiateArrayBuffer(binaryFile, imports);
    }
    function getWasmImports() {
      return { a: wasmImports };
    }
    async function createWasm() {
      function receiveInstance(instance, module) {
        wasmExports = instance.exports;
        wasmExports = Asyncify.instrumentWasmExports(wasmExports);
        wasmMemory = wasmExports["U"];
        updateMemoryViews();
        removeRunDependency();
        return wasmExports;
      }
      addRunDependency();
      function receiveInstantiationResult(result2) {
        return receiveInstance(result2["instance"]);
      }
      var info = getWasmImports();
      if (Module["instantiateWasm"]) {
        return new Promise((resolve, reject) => {
          Module["instantiateWasm"](info, (mod, inst) => {
            resolve(receiveInstance(mod));
          });
        });
      }
      wasmBinaryFile ?? (wasmBinaryFile = findWasmBinary());
      try {
        var result = await instantiateAsync(wasmBinary, wasmBinaryFile, info);
        var exports = receiveInstantiationResult(result);
        return exports;
      } catch (e) {
        readyPromiseReject(e);
        return Promise.reject(e);
      }
    }
    class ExitStatus {
      constructor(status) {
        __publicField$2(this, "name", "ExitStatus");
        this.message = `Program terminated with exit(${status})`;
        this.status = status;
      }
    }
    var callRuntimeCallbacks = (callbacks) => {
      while (callbacks.length > 0) {
        callbacks.shift()(Module);
      }
    };
    var onPostRuns = [];
    var addOnPostRun = (cb) => onPostRuns.push(cb);
    var onPreRuns = [];
    var addOnPreRun = (cb) => onPreRuns.push(cb);
    var dynCallLegacy = (sig, ptr, args) => {
      sig = sig.replace(/p/g, "i");
      var f = Module["dynCall_" + sig];
      return f(ptr, ...args);
    };
    var dynCall = (sig, ptr, args = [], promising = false) => {
      var rtn = dynCallLegacy(sig, ptr, args);
      function convert(rtn2) {
        return rtn2;
      }
      return convert(rtn);
    };
    var noExitRuntime = true;
    class ExceptionInfo {
      constructor(excPtr) {
        this.excPtr = excPtr;
        this.ptr = excPtr - 24;
      }
      set_type(type) {
        HEAPU32[this.ptr + 4 >> 2] = type;
      }
      get_type() {
        return HEAPU32[this.ptr + 4 >> 2];
      }
      set_destructor(destructor) {
        HEAPU32[this.ptr + 8 >> 2] = destructor;
      }
      get_destructor() {
        return HEAPU32[this.ptr + 8 >> 2];
      }
      set_caught(caught) {
        caught = caught ? 1 : 0;
        HEAP8[this.ptr + 12] = caught;
      }
      get_caught() {
        return HEAP8[this.ptr + 12] != 0;
      }
      set_rethrown(rethrown) {
        rethrown = rethrown ? 1 : 0;
        HEAP8[this.ptr + 13] = rethrown;
      }
      get_rethrown() {
        return HEAP8[this.ptr + 13] != 0;
      }
      init(type, destructor) {
        this.set_adjusted_ptr(0);
        this.set_type(type);
        this.set_destructor(destructor);
      }
      set_adjusted_ptr(adjustedPtr) {
        HEAPU32[this.ptr + 16 >> 2] = adjustedPtr;
      }
      get_adjusted_ptr() {
        return HEAPU32[this.ptr + 16 >> 2];
      }
    }
    var exceptionLast = 0;
    var ___cxa_throw = (ptr, type, destructor) => {
      var info = new ExceptionInfo(ptr);
      info.init(type, destructor);
      exceptionLast = ptr;
      throw exceptionLast;
    };
    var syscallGetVarargI = () => {
      var ret = HEAP32[+SYSCALLS.varargs >> 2];
      SYSCALLS.varargs += 4;
      return ret;
    };
    var syscallGetVarargP = syscallGetVarargI;
    var PATH = { isAbs: (path) => path.charAt(0) === "/", splitPath: (filename) => {
      var splitPathRe = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;
      return splitPathRe.exec(filename).slice(1);
    }, normalizeArray: (parts, allowAboveRoot) => {
      var up = 0;
      for (var i = parts.length - 1; i >= 0; i--) {
        var last = parts[i];
        if (last === ".") {
          parts.splice(i, 1);
        } else if (last === "..") {
          parts.splice(i, 1);
          up++;
        } else if (up) {
          parts.splice(i, 1);
          up--;
        }
      }
      if (allowAboveRoot) {
        for (; up; up--) {
          parts.unshift("..");
        }
      }
      return parts;
    }, normalize: (path) => {
      var isAbsolute = PATH.isAbs(path), trailingSlash = path.slice(-1) === "/";
      path = PATH.normalizeArray(path.split("/").filter((p) => !!p), !isAbsolute).join("/");
      if (!path && !isAbsolute) {
        path = ".";
      }
      if (path && trailingSlash) {
        path += "/";
      }
      return (isAbsolute ? "/" : "") + path;
    }, dirname: (path) => {
      var result = PATH.splitPath(path), root = result[0], dir = result[1];
      if (!root && !dir) {
        return ".";
      }
      if (dir) {
        dir = dir.slice(0, -1);
      }
      return root + dir;
    }, basename: (path) => path && path.match(/([^\/]+|\/)\/*$/)[1], join: (...paths) => PATH.normalize(paths.join("/")), join2: (l, r) => PATH.normalize(l + "/" + r) };
    var initRandomFill = () => (view) => crypto.getRandomValues(view);
    var randomFill = (view) => {
      (randomFill = initRandomFill())(view);
    };
    var PATH_FS = { resolve: (...args) => {
      var resolvedPath = "", resolvedAbsolute = false;
      for (var i = args.length - 1; i >= -1 && !resolvedAbsolute; i--) {
        var path = i >= 0 ? args[i] : FS.cwd();
        if (typeof path != "string") {
          throw new TypeError("Arguments to path.resolve must be strings");
        } else if (!path) {
          return "";
        }
        resolvedPath = path + "/" + resolvedPath;
        resolvedAbsolute = PATH.isAbs(path);
      }
      resolvedPath = PATH.normalizeArray(resolvedPath.split("/").filter((p) => !!p), !resolvedAbsolute).join("/");
      return (resolvedAbsolute ? "/" : "") + resolvedPath || ".";
    }, relative: (from, to) => {
      from = PATH_FS.resolve(from).slice(1);
      to = PATH_FS.resolve(to).slice(1);
      function trim(arr) {
        var start = 0;
        for (; start < arr.length; start++) {
          if (arr[start] !== "")
            break;
        }
        var end = arr.length - 1;
        for (; end >= 0; end--) {
          if (arr[end] !== "")
            break;
        }
        if (start > end)
          return [];
        return arr.slice(start, end - start + 1);
      }
      var fromParts = trim(from.split("/"));
      var toParts = trim(to.split("/"));
      var length = Math.min(fromParts.length, toParts.length);
      var samePartsLength = length;
      for (var i = 0; i < length; i++) {
        if (fromParts[i] !== toParts[i]) {
          samePartsLength = i;
          break;
        }
      }
      var outputParts = [];
      for (var i = samePartsLength; i < fromParts.length; i++) {
        outputParts.push("..");
      }
      outputParts = outputParts.concat(toParts.slice(samePartsLength));
      return outputParts.join("/");
    } };
    var UTF8Decoder = typeof TextDecoder != "undefined" ? new TextDecoder() : void 0;
    var UTF8ArrayToString = (heapOrArray, idx = 0, maxBytesToRead = NaN) => {
      var endIdx = idx + maxBytesToRead;
      var endPtr = idx;
      while (heapOrArray[endPtr] && !(endPtr >= endIdx))
        ++endPtr;
      if (endPtr - idx > 16 && heapOrArray.buffer && UTF8Decoder) {
        return UTF8Decoder.decode(heapOrArray.subarray(idx, endPtr));
      }
      var str = "";
      while (idx < endPtr) {
        var u0 = heapOrArray[idx++];
        if (!(u0 & 128)) {
          str += String.fromCharCode(u0);
          continue;
        }
        var u1 = heapOrArray[idx++] & 63;
        if ((u0 & 224) == 192) {
          str += String.fromCharCode((u0 & 31) << 6 | u1);
          continue;
        }
        var u2 = heapOrArray[idx++] & 63;
        if ((u0 & 240) == 224) {
          u0 = (u0 & 15) << 12 | u1 << 6 | u2;
        } else {
          u0 = (u0 & 7) << 18 | u1 << 12 | u2 << 6 | heapOrArray[idx++] & 63;
        }
        if (u0 < 65536) {
          str += String.fromCharCode(u0);
        } else {
          var ch = u0 - 65536;
          str += String.fromCharCode(55296 | ch >> 10, 56320 | ch & 1023);
        }
      }
      return str;
    };
    var FS_stdin_getChar_buffer = [];
    var lengthBytesUTF8 = (str) => {
      var len = 0;
      for (var i = 0; i < str.length; ++i) {
        var c = str.charCodeAt(i);
        if (c <= 127) {
          len++;
        } else if (c <= 2047) {
          len += 2;
        } else if (c >= 55296 && c <= 57343) {
          len += 4;
          ++i;
        } else {
          len += 3;
        }
      }
      return len;
    };
    var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => {
      if (!(maxBytesToWrite > 0))
        return 0;
      var startIdx = outIdx;
      var endIdx = outIdx + maxBytesToWrite - 1;
      for (var i = 0; i < str.length; ++i) {
        var u = str.charCodeAt(i);
        if (u >= 55296 && u <= 57343) {
          var u1 = str.charCodeAt(++i);
          u = 65536 + ((u & 1023) << 10) | u1 & 1023;
        }
        if (u <= 127) {
          if (outIdx >= endIdx)
            break;
          heap[outIdx++] = u;
        } else if (u <= 2047) {
          if (outIdx + 1 >= endIdx)
            break;
          heap[outIdx++] = 192 | u >> 6;
          heap[outIdx++] = 128 | u & 63;
        } else if (u <= 65535) {
          if (outIdx + 2 >= endIdx)
            break;
          heap[outIdx++] = 224 | u >> 12;
          heap[outIdx++] = 128 | u >> 6 & 63;
          heap[outIdx++] = 128 | u & 63;
        } else {
          if (outIdx + 3 >= endIdx)
            break;
          heap[outIdx++] = 240 | u >> 18;
          heap[outIdx++] = 128 | u >> 12 & 63;
          heap[outIdx++] = 128 | u >> 6 & 63;
          heap[outIdx++] = 128 | u & 63;
        }
      }
      heap[outIdx] = 0;
      return outIdx - startIdx;
    };
    var intArrayFromString = (stringy, dontAddNull, length) => {
      var len = lengthBytesUTF8(stringy) + 1;
      var u8array = new Array(len);
      var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length);
      u8array.length = numBytesWritten;
      return u8array;
    };
    var FS_stdin_getChar = () => {
      if (!FS_stdin_getChar_buffer.length) {
        var result = null;
        if (typeof window != "undefined" && typeof window.prompt == "function") {
          result = window.prompt("Input: ");
          if (result !== null) {
            result += "\n";
          }
        }
        if (!result) {
          return null;
        }
        FS_stdin_getChar_buffer = intArrayFromString(result);
      }
      return FS_stdin_getChar_buffer.shift();
    };
    var TTY = { ttys: [], init() {
    }, shutdown() {
    }, register(dev, ops) {
      TTY.ttys[dev] = { input: [], output: [], ops };
      FS.registerDevice(dev, TTY.stream_ops);
    }, stream_ops: { open(stream) {
      var tty = TTY.ttys[stream.node.rdev];
      if (!tty) {
        throw new FS.ErrnoError(43);
      }
      stream.tty = tty;
      stream.seekable = false;
    }, close(stream) {
      stream.tty.ops.fsync(stream.tty);
    }, fsync(stream) {
      stream.tty.ops.fsync(stream.tty);
    }, read(stream, buffer, offset, length, pos) {
      if (!stream.tty || !stream.tty.ops.get_char) {
        throw new FS.ErrnoError(60);
      }
      var bytesRead = 0;
      for (var i = 0; i < length; i++) {
        var result;
        try {
          result = stream.tty.ops.get_char(stream.tty);
        } catch (e) {
          throw new FS.ErrnoError(29);
        }
        if (result === void 0 && bytesRead === 0) {
          throw new FS.ErrnoError(6);
        }
        if (result === null || result === void 0)
          break;
        bytesRead++;
        buffer[offset + i] = result;
      }
      if (bytesRead) {
        stream.node.atime = Date.now();
      }
      return bytesRead;
    }, write(stream, buffer, offset, length, pos) {
      if (!stream.tty || !stream.tty.ops.put_char) {
        throw new FS.ErrnoError(60);
      }
      try {
        for (var i = 0; i < length; i++) {
          stream.tty.ops.put_char(stream.tty, buffer[offset + i]);
        }
      } catch (e) {
        throw new FS.ErrnoError(29);
      }
      if (length) {
        stream.node.mtime = stream.node.ctime = Date.now();
      }
      return i;
    } }, default_tty_ops: { get_char(tty) {
      return FS_stdin_getChar();
    }, put_char(tty, val) {
      if (val === null || val === 10) {
        out(UTF8ArrayToString(tty.output));
        tty.output = [];
      } else {
        if (val != 0)
          tty.output.push(val);
      }
    }, fsync(tty) {
      if (tty.output?.length > 0) {
        out(UTF8ArrayToString(tty.output));
        tty.output = [];
      }
    }, ioctl_tcgets(tty) {
      return { c_iflag: 25856, c_oflag: 5, c_cflag: 191, c_lflag: 35387, c_cc: [3, 28, 127, 21, 4, 0, 1, 0, 17, 19, 26, 0, 18, 15, 23, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] };
    }, ioctl_tcsets(tty, optional_actions, data) {
      return 0;
    }, ioctl_tiocgwinsz(tty) {
      return [24, 80];
    } }, default_tty1_ops: { put_char(tty, val) {
      if (val === null || val === 10) {
        err(UTF8ArrayToString(tty.output));
        tty.output = [];
      } else {
        if (val != 0)
          tty.output.push(val);
      }
    }, fsync(tty) {
      if (tty.output?.length > 0) {
        err(UTF8ArrayToString(tty.output));
        tty.output = [];
      }
    } } };
    var mmapAlloc = (size) => {
      abort();
    };
    var MEMFS = { ops_table: null, mount(mount) {
      return MEMFS.createNode(null, "/", 16895, 0);
    }, createNode(parent, name, mode, dev) {
      if (FS.isBlkdev(mode) || FS.isFIFO(mode)) {
        throw new FS.ErrnoError(63);
      }
      MEMFS.ops_table || (MEMFS.ops_table = { dir: { node: { getattr: MEMFS.node_ops.getattr, setattr: MEMFS.node_ops.setattr, lookup: MEMFS.node_ops.lookup, mknod: MEMFS.node_ops.mknod, rename: MEMFS.node_ops.rename, unlink: MEMFS.node_ops.unlink, rmdir: MEMFS.node_ops.rmdir, readdir: MEMFS.node_ops.readdir, symlink: MEMFS.node_ops.symlink }, stream: { llseek: MEMFS.stream_ops.llseek } }, file: { node: { getattr: MEMFS.node_ops.getattr, setattr: MEMFS.node_ops.setattr }, stream: { llseek: MEMFS.stream_ops.llseek, read: MEMFS.stream_ops.read, write: MEMFS.stream_ops.write, mmap: MEMFS.stream_ops.mmap, msync: MEMFS.stream_ops.msync } }, link: { node: { getattr: MEMFS.node_ops.getattr, setattr: MEMFS.node_ops.setattr, readlink: MEMFS.node_ops.readlink }, stream: {} }, chrdev: { node: { getattr: MEMFS.node_ops.getattr, setattr: MEMFS.node_ops.setattr }, stream: FS.chrdev_stream_ops } });
      var node = FS.createNode(parent, name, mode, dev);
      if (FS.isDir(node.mode)) {
        node.node_ops = MEMFS.ops_table.dir.node;
        node.stream_ops = MEMFS.ops_table.dir.stream;
        node.contents = {};
      } else if (FS.isFile(node.mode)) {
        node.node_ops = MEMFS.ops_table.file.node;
        node.stream_ops = MEMFS.ops_table.file.stream;
        node.usedBytes = 0;
        node.contents = null;
      } else if (FS.isLink(node.mode)) {
        node.node_ops = MEMFS.ops_table.link.node;
        node.stream_ops = MEMFS.ops_table.link.stream;
      } else if (FS.isChrdev(node.mode)) {
        node.node_ops = MEMFS.ops_table.chrdev.node;
        node.stream_ops = MEMFS.ops_table.chrdev.stream;
      }
      node.atime = node.mtime = node.ctime = Date.now();
      if (parent) {
        parent.contents[name] = node;
        parent.atime = parent.mtime = parent.ctime = node.atime;
      }
      return node;
    }, getFileDataAsTypedArray(node) {
      if (!node.contents)
        return new Uint8Array(0);
      if (node.contents.subarray)
        return node.contents.subarray(0, node.usedBytes);
      return new Uint8Array(node.contents);
    }, expandFileStorage(node, newCapacity) {
      var prevCapacity = node.contents ? node.contents.length : 0;
      if (prevCapacity >= newCapacity)
        return;
      var CAPACITY_DOUBLING_MAX = 1024 * 1024;
      newCapacity = Math.max(newCapacity, prevCapacity * (prevCapacity < CAPACITY_DOUBLING_MAX ? 2 : 1.125) >>> 0);
      if (prevCapacity != 0)
        newCapacity = Math.max(newCapacity, 256);
      var oldContents = node.contents;
      node.contents = new Uint8Array(newCapacity);
      if (node.usedBytes > 0)
        node.contents.set(oldContents.subarray(0, node.usedBytes), 0);
    }, resizeFileStorage(node, newSize) {
      if (node.usedBytes == newSize)
        return;
      if (newSize == 0) {
        node.contents = null;
        node.usedBytes = 0;
      } else {
        var oldContents = node.contents;
        node.contents = new Uint8Array(newSize);
        if (oldContents) {
          node.contents.set(oldContents.subarray(0, Math.min(newSize, node.usedBytes)));
        }
        node.usedBytes = newSize;
      }
    }, node_ops: { getattr(node) {
      var attr = {};
      attr.dev = FS.isChrdev(node.mode) ? node.id : 1;
      attr.ino = node.id;
      attr.mode = node.mode;
      attr.nlink = 1;
      attr.uid = 0;
      attr.gid = 0;
      attr.rdev = node.rdev;
      if (FS.isDir(node.mode)) {
        attr.size = 4096;
      } else if (FS.isFile(node.mode)) {
        attr.size = node.usedBytes;
      } else if (FS.isLink(node.mode)) {
        attr.size = node.link.length;
      } else {
        attr.size = 0;
      }
      attr.atime = new Date(node.atime);
      attr.mtime = new Date(node.mtime);
      attr.ctime = new Date(node.ctime);
      attr.blksize = 4096;
      attr.blocks = Math.ceil(attr.size / attr.blksize);
      return attr;
    }, setattr(node, attr) {
      for (const key of ["mode", "atime", "mtime", "ctime"]) {
        if (attr[key] != null) {
          node[key] = attr[key];
        }
      }
      if (attr.size !== void 0) {
        MEMFS.resizeFileStorage(node, attr.size);
      }
    }, lookup(parent, name) {
      throw MEMFS.doesNotExistError;
    }, mknod(parent, name, mode, dev) {
      return MEMFS.createNode(parent, name, mode, dev);
    }, rename(old_node, new_dir, new_name) {
      var new_node;
      try {
        new_node = FS.lookupNode(new_dir, new_name);
      } catch (e) {
      }
      if (new_node) {
        if (FS.isDir(old_node.mode)) {
          for (var i in new_node.contents) {
            throw new FS.ErrnoError(55);
          }
        }
        FS.hashRemoveNode(new_node);
      }
      delete old_node.parent.contents[old_node.name];
      new_dir.contents[new_name] = old_node;
      old_node.name = new_name;
      new_dir.ctime = new_dir.mtime = old_node.parent.ctime = old_node.parent.mtime = Date.now();
    }, unlink(parent, name) {
      delete parent.contents[name];
      parent.ctime = parent.mtime = Date.now();
    }, rmdir(parent, name) {
      var node = FS.lookupNode(parent, name);
      for (var i in node.contents) {
        throw new FS.ErrnoError(55);
      }
      delete parent.contents[name];
      parent.ctime = parent.mtime = Date.now();
    }, readdir(node) {
      return [".", "..", ...Object.keys(node.contents)];
    }, symlink(parent, newname, oldpath) {
      var node = MEMFS.createNode(parent, newname, 511 | 40960, 0);
      node.link = oldpath;
      return node;
    }, readlink(node) {
      if (!FS.isLink(node.mode)) {
        throw new FS.ErrnoError(28);
      }
      return node.link;
    } }, stream_ops: { read(stream, buffer, offset, length, position) {
      var contents = stream.node.contents;
      if (position >= stream.node.usedBytes)
        return 0;
      var size = Math.min(stream.node.usedBytes - position, length);
      if (size > 8 && contents.subarray) {
        buffer.set(contents.subarray(position, position + size), offset);
      } else {
        for (var i = 0; i < size; i++)
          buffer[offset + i] = contents[position + i];
      }
      return size;
    }, write(stream, buffer, offset, length, position, canOwn) {
      if (buffer.buffer === HEAP8.buffer) {
        canOwn = false;
      }
      if (!length)
        return 0;
      var node = stream.node;
      node.mtime = node.ctime = Date.now();
      if (buffer.subarray && (!node.contents || node.contents.subarray)) {
        if (canOwn) {
          node.contents = buffer.subarray(offset, offset + length);
          node.usedBytes = length;
          return length;
        } else if (node.usedBytes === 0 && position === 0) {
          node.contents = buffer.slice(offset, offset + length);
          node.usedBytes = length;
          return length;
        } else if (position + length <= node.usedBytes) {
          node.contents.set(buffer.subarray(offset, offset + length), position);
          return length;
        }
      }
      MEMFS.expandFileStorage(node, position + length);
      if (node.contents.subarray && buffer.subarray) {
        node.contents.set(buffer.subarray(offset, offset + length), position);
      } else {
        for (var i = 0; i < length; i++) {
          node.contents[position + i] = buffer[offset + i];
        }
      }
      node.usedBytes = Math.max(node.usedBytes, position + length);
      return length;
    }, llseek(stream, offset, whence) {
      var position = offset;
      if (whence === 1) {
        position += stream.position;
      } else if (whence === 2) {
        if (FS.isFile(stream.node.mode)) {
          position += stream.node.usedBytes;
        }
      }
      if (position < 0) {
        throw new FS.ErrnoError(28);
      }
      return position;
    }, mmap(stream, length, position, prot, flags) {
      if (!FS.isFile(stream.node.mode)) {
        throw new FS.ErrnoError(43);
      }
      var ptr;
      var allocated;
      var contents = stream.node.contents;
      if (!(flags & 2) && contents && contents.buffer === HEAP8.buffer) {
        allocated = false;
        ptr = contents.byteOffset;
      } else {
        allocated = true;
        ptr = mmapAlloc();
        if (!ptr) {
          throw new FS.ErrnoError(48);
        }
        if (contents) {
          if (position > 0 || position + length < contents.length) {
            if (contents.subarray) {
              contents = contents.subarray(position, position + length);
            } else {
              contents = Array.prototype.slice.call(contents, position, position + length);
            }
          }
          HEAP8.set(contents, ptr);
        }
      }
      return { ptr, allocated };
    }, msync(stream, buffer, offset, length, mmapFlags) {
      MEMFS.stream_ops.write(stream, buffer, 0, length, offset, false);
      return 0;
    } } };
    var asyncLoad = async (url) => {
      var arrayBuffer = await readAsync(url);
      return new Uint8Array(arrayBuffer);
    };
    asyncLoad.isAsync = true;
    var FS_createDataFile = (...args) => FS.createDataFile(...args);
    var preloadPlugins = [];
    var FS_handledByPreloadPlugin = (byteArray, fullname, finish, onerror) => {
      if (typeof Browser != "undefined")
        Browser.init();
      var handled = false;
      preloadPlugins.forEach((plugin) => {
        if (handled)
          return;
        if (plugin["canHandle"](fullname)) {
          plugin["handle"](byteArray, fullname, finish, onerror);
          handled = true;
        }
      });
      return handled;
    };
    var FS_createPreloadedFile = (parent, name, url, canRead, canWrite, onload, onerror, dontCreateFile, canOwn, preFinish) => {
      var fullname = name ? PATH_FS.resolve(PATH.join2(parent, name)) : parent;
      function processData(byteArray) {
        function finish(byteArray2) {
          preFinish?.();
          if (!dontCreateFile) {
            FS_createDataFile(parent, name, byteArray2, canRead, canWrite, canOwn);
          }
          onload?.();
          removeRunDependency();
        }
        if (FS_handledByPreloadPlugin(byteArray, fullname, finish, () => {
          onerror?.();
          removeRunDependency();
        })) {
          return;
        }
        finish(byteArray);
      }
      addRunDependency();
      if (typeof url == "string") {
        asyncLoad(url).then(processData, onerror);
      } else {
        processData(url);
      }
    };
    var FS_modeStringToFlags = (str) => {
      var flagModes = { r: 0, "r+": 2, w: 512 | 64 | 1, "w+": 512 | 64 | 2, a: 1024 | 64 | 1, "a+": 1024 | 64 | 2 };
      var flags = flagModes[str];
      if (typeof flags == "undefined") {
        throw new Error(`Unknown file open mode: ${str}`);
      }
      return flags;
    };
    var FS_getMode = (canRead, canWrite) => {
      var mode = 0;
      if (canRead)
        mode |= 292 | 73;
      if (canWrite)
        mode |= 146;
      return mode;
    };
    var FS = { root: null, mounts: [], devices: {}, streams: [], nextInode: 1, nameTable: null, currentPath: "/", initialized: false, ignorePermissions: true, filesystems: null, syncFSRequests: 0, readFiles: {}, ErrnoError: class {
      constructor(errno) {
        __publicField$2(this, "name", "ErrnoError");
        this.errno = errno;
      }
    }, FSStream: class {
      constructor() {
        __publicField$2(this, "shared", {});
      }
      get object() {
        return this.node;
      }
      set object(val) {
        this.node = val;
      }
      get isRead() {
        return (this.flags & 2097155) !== 1;
      }
      get isWrite() {
        return (this.flags & 2097155) !== 0;
      }
      get isAppend() {
        return this.flags & 1024;
      }
      get flags() {
        return this.shared.flags;
      }
      set flags(val) {
        this.shared.flags = val;
      }
      get position() {
        return this.shared.position;
      }
      set position(val) {
        this.shared.position = val;
      }
    }, FSNode: class {
      constructor(parent, name, mode, rdev) {
        __publicField$2(this, "node_ops", {});
        __publicField$2(this, "stream_ops", {});
        __publicField$2(this, "readMode", 292 | 73);
        __publicField$2(this, "writeMode", 146);
        __publicField$2(this, "mounted", null);
        if (!parent) {
          parent = this;
        }
        this.parent = parent;
        this.mount = parent.mount;
        this.id = FS.nextInode++;
        this.name = name;
        this.mode = mode;
        this.rdev = rdev;
        this.atime = this.mtime = this.ctime = Date.now();
      }
      get read() {
        return (this.mode & this.readMode) === this.readMode;
      }
      set read(val) {
        val ? this.mode |= this.readMode : this.mode &= ~this.readMode;
      }
      get write() {
        return (this.mode & this.writeMode) === this.writeMode;
      }
      set write(val) {
        val ? this.mode |= this.writeMode : this.mode &= ~this.writeMode;
      }
      get isFolder() {
        return FS.isDir(this.mode);
      }
      get isDevice() {
        return FS.isChrdev(this.mode);
      }
    }, lookupPath(path, opts = {}) {
      if (!path) {
        throw new FS.ErrnoError(44);
      }
      opts.follow_mount ?? (opts.follow_mount = true);
      if (!PATH.isAbs(path)) {
        path = FS.cwd() + "/" + path;
      }
      linkloop:
        for (var nlinks = 0; nlinks < 40; nlinks++) {
          var parts = path.split("/").filter((p) => !!p);
          var current = FS.root;
          var current_path = "/";
          for (var i = 0; i < parts.length; i++) {
            var islast = i === parts.length - 1;
            if (islast && opts.parent) {
              break;
            }
            if (parts[i] === ".") {
              continue;
            }
            if (parts[i] === "..") {
              current_path = PATH.dirname(current_path);
              if (FS.isRoot(current)) {
                path = current_path + "/" + parts.slice(i + 1).join("/");
                continue linkloop;
              } else {
                current = current.parent;
              }
              continue;
            }
            current_path = PATH.join2(current_path, parts[i]);
            try {
              current = FS.lookupNode(current, parts[i]);
            } catch (e) {
              if (e?.errno === 44 && islast && opts.noent_okay) {
                return { path: current_path };
              }
              throw e;
            }
            if (FS.isMountpoint(current) && (!islast || opts.follow_mount)) {
              current = current.mounted.root;
            }
            if (FS.isLink(current.mode) && (!islast || opts.follow)) {
              if (!current.node_ops.readlink) {
                throw new FS.ErrnoError(52);
              }
              var link = current.node_ops.readlink(current);
              if (!PATH.isAbs(link)) {
                link = PATH.dirname(current_path) + "/" + link;
              }
              path = link + "/" + parts.slice(i + 1).join("/");
              continue linkloop;
            }
          }
          return { path: current_path, node: current };
        }
      throw new FS.ErrnoError(32);
    }, getPath(node) {
      var path;
      while (true) {
        if (FS.isRoot(node)) {
          var mount = node.mount.mountpoint;
          if (!path)
            return mount;
          return mount[mount.length - 1] !== "/" ? `${mount}/${path}` : mount + path;
        }
        path = path ? `${node.name}/${path}` : node.name;
        node = node.parent;
      }
    }, hashName(parentid, name) {
      var hash = 0;
      for (var i = 0; i < name.length; i++) {
        hash = (hash << 5) - hash + name.charCodeAt(i) | 0;
      }
      return (parentid + hash >>> 0) % FS.nameTable.length;
    }, hashAddNode(node) {
      var hash = FS.hashName(node.parent.id, node.name);
      node.name_next = FS.nameTable[hash];
      FS.nameTable[hash] = node;
    }, hashRemoveNode(node) {
      var hash = FS.hashName(node.parent.id, node.name);
      if (FS.nameTable[hash] === node) {
        FS.nameTable[hash] = node.name_next;
      } else {
        var current = FS.nameTable[hash];
        while (current) {
          if (current.name_next === node) {
            current.name_next = node.name_next;
            break;
          }
          current = current.name_next;
        }
      }
    }, lookupNode(parent, name) {
      var errCode = FS.mayLookup(parent);
      if (errCode) {
        throw new FS.ErrnoError(errCode);
      }
      var hash = FS.hashName(parent.id, name);
      for (var node = FS.nameTable[hash]; node; node = node.name_next) {
        var nodeName = node.name;
        if (node.parent.id === parent.id && nodeName === name) {
          return node;
        }
      }
      return FS.lookup(parent, name);
    }, createNode(parent, name, mode, rdev) {
      var node = new FS.FSNode(parent, name, mode, rdev);
      FS.hashAddNode(node);
      return node;
    }, destroyNode(node) {
      FS.hashRemoveNode(node);
    }, isRoot(node) {
      return node === node.parent;
    }, isMountpoint(node) {
      return !!node.mounted;
    }, isFile(mode) {
      return (mode & 61440) === 32768;
    }, isDir(mode) {
      return (mode & 61440) === 16384;
    }, isLink(mode) {
      return (mode & 61440) === 40960;
    }, isChrdev(mode) {
      return (mode & 61440) === 8192;
    }, isBlkdev(mode) {
      return (mode & 61440) === 24576;
    }, isFIFO(mode) {
      return (mode & 61440) === 4096;
    }, isSocket(mode) {
      return (mode & 49152) === 49152;
    }, flagsToPermissionString(flag) {
      var perms = ["r", "w", "rw"][flag & 3];
      if (flag & 512) {
        perms += "w";
      }
      return perms;
    }, nodePermissions(node, perms) {
      if (FS.ignorePermissions) {
        return 0;
      }
      if (perms.includes("r") && !(node.mode & 292)) {
        return 2;
      } else if (perms.includes("w") && !(node.mode & 146)) {
        return 2;
      } else if (perms.includes("x") && !(node.mode & 73)) {
        return 2;
      }
      return 0;
    }, mayLookup(dir) {
      if (!FS.isDir(dir.mode))
        return 54;
      var errCode = FS.nodePermissions(dir, "x");
      if (errCode)
        return errCode;
      if (!dir.node_ops.lookup)
        return 2;
      return 0;
    }, mayCreate(dir, name) {
      if (!FS.isDir(dir.mode)) {
        return 54;
      }
      try {
        var node = FS.lookupNode(dir, name);
        return 20;
      } catch (e) {
      }
      return FS.nodePermissions(dir, "wx");
    }, mayDelete(dir, name, isdir) {
      var node;
      try {
        node = FS.lookupNode(dir, name);
      } catch (e) {
        return e.errno;
      }
      var errCode = FS.nodePermissions(dir, "wx");
      if (errCode) {
        return errCode;
      }
      if (isdir) {
        if (!FS.isDir(node.mode)) {
          return 54;
        }
        if (FS.isRoot(node) || FS.getPath(node) === FS.cwd()) {
          return 10;
        }
      } else {
        if (FS.isDir(node.mode)) {
          return 31;
        }
      }
      return 0;
    }, mayOpen(node, flags) {
      if (!node) {
        return 44;
      }
      if (FS.isLink(node.mode)) {
        return 32;
      } else if (FS.isDir(node.mode)) {
        if (FS.flagsToPermissionString(flags) !== "r" || flags & (512 | 64)) {
          return 31;
        }
      }
      return FS.nodePermissions(node, FS.flagsToPermissionString(flags));
    }, checkOpExists(op, err2) {
      if (!op) {
        throw new FS.ErrnoError(err2);
      }
      return op;
    }, MAX_OPEN_FDS: 4096, nextfd() {
      for (var fd = 0; fd <= FS.MAX_OPEN_FDS; fd++) {
        if (!FS.streams[fd]) {
          return fd;
        }
      }
      throw new FS.ErrnoError(33);
    }, getStreamChecked(fd) {
      var stream = FS.getStream(fd);
      if (!stream) {
        throw new FS.ErrnoError(8);
      }
      return stream;
    }, getStream: (fd) => FS.streams[fd], createStream(stream, fd = -1) {
      stream = Object.assign(new FS.FSStream(), stream);
      if (fd == -1) {
        fd = FS.nextfd();
      }
      stream.fd = fd;
      FS.streams[fd] = stream;
      return stream;
    }, closeStream(fd) {
      FS.streams[fd] = null;
    }, dupStream(origStream, fd = -1) {
      var stream = FS.createStream(origStream, fd);
      stream.stream_ops?.dup?.(stream);
      return stream;
    }, doSetAttr(stream, node, attr) {
      var setattr = stream?.stream_ops.setattr;
      var arg = setattr ? stream : node;
      setattr ?? (setattr = node.node_ops.setattr);
      FS.checkOpExists(setattr, 63);
      setattr(arg, attr);
    }, chrdev_stream_ops: { open(stream) {
      var device = FS.getDevice(stream.node.rdev);
      stream.stream_ops = device.stream_ops;
      stream.stream_ops.open?.(stream);
    }, llseek() {
      throw new FS.ErrnoError(70);
    } }, major: (dev) => dev >> 8, minor: (dev) => dev & 255, makedev: (ma, mi) => ma << 8 | mi, registerDevice(dev, ops) {
      FS.devices[dev] = { stream_ops: ops };
    }, getDevice: (dev) => FS.devices[dev], getMounts(mount) {
      var mounts = [];
      var check = [mount];
      while (check.length) {
        var m = check.pop();
        mounts.push(m);
        check.push(...m.mounts);
      }
      return mounts;
    }, syncfs(populate, callback) {
      if (typeof populate == "function") {
        callback = populate;
        populate = false;
      }
      FS.syncFSRequests++;
      if (FS.syncFSRequests > 1) {
        err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`);
      }
      var mounts = FS.getMounts(FS.root.mount);
      var completed = 0;
      function doCallback(errCode) {
        FS.syncFSRequests--;
        return callback(errCode);
      }
      function done(errCode) {
        if (errCode) {
          if (!done.errored) {
            done.errored = true;
            return doCallback(errCode);
          }
          return;
        }
        if (++completed >= mounts.length) {
          doCallback(null);
        }
      }
      mounts.forEach((mount) => {
        if (!mount.type.syncfs) {
          return done(null);
        }
        mount.type.syncfs(mount, populate, done);
      });
    }, mount(type, opts, mountpoint) {
      var root = mountpoint === "/";
      var pseudo = !mountpoint;
      var node;
      if (root && FS.root) {
        throw new FS.ErrnoError(10);
      } else if (!root && !pseudo) {
        var lookup = FS.lookupPath(mountpoint, { follow_mount: false });
        mountpoint = lookup.path;
        node = lookup.node;
        if (FS.isMountpoint(node)) {
          throw new FS.ErrnoError(10);
        }
        if (!FS.isDir(node.mode)) {
          throw new FS.ErrnoError(54);
        }
      }
      var mount = { type, opts, mountpoint, mounts: [] };
      var mountRoot = type.mount(mount);
      mountRoot.mount = mount;
      mount.root = mountRoot;
      if (root) {
        FS.root = mountRoot;
      } else if (node) {
        node.mounted = mount;
        if (node.mount) {
          node.mount.mounts.push(mount);
        }
      }
      return mountRoot;
    }, unmount(mountpoint) {
      var lookup = FS.lookupPath(mountpoint, { follow_mount: false });
      if (!FS.isMountpoint(lookup.node)) {
        throw new FS.ErrnoError(28);
      }
      var node = lookup.node;
      var mount = node.mounted;
      var mounts = FS.getMounts(mount);
      Object.keys(FS.nameTable).forEach((hash) => {
        var current = FS.nameTable[hash];
        while (current) {
          var next = current.name_next;
          if (mounts.includes(current.mount)) {
            FS.destroyNode(current);
          }
          current = next;
        }
      });
      node.mounted = null;
      var idx = node.mount.mounts.indexOf(mount);
      node.mount.mounts.splice(idx, 1);
    }, lookup(parent, name) {
      return parent.node_ops.lookup(parent, name);
    }, mknod(path, mode, dev) {
      var lookup = FS.lookupPath(path, { parent: true });
      var parent = lookup.node;
      var name = PATH.basename(path);
      if (!name) {
        throw new FS.ErrnoError(28);
      }
      if (name === "." || name === "..") {
        throw new FS.ErrnoError(20);
      }
      var errCode = FS.mayCreate(parent, name);
      if (errCode) {
        throw new FS.ErrnoError(errCode);
      }
      if (!parent.node_ops.mknod) {
        throw new FS.ErrnoError(63);
      }
      return parent.node_ops.mknod(parent, name, mode, dev);
    }, statfs(path) {
      return FS.statfsNode(FS.lookupPath(path, { follow: true }).node);
    }, statfsStream(stream) {
      return FS.statfsNode(stream.node);
    }, statfsNode(node) {
      var rtn = { bsize: 4096, frsize: 4096, blocks: 1e6, bfree: 5e5, bavail: 5e5, files: FS.nextInode, ffree: FS.nextInode - 1, fsid: 42, flags: 2, namelen: 255 };
      if (node.node_ops.statfs) {
        Object.assign(rtn, node.node_ops.statfs(node.mount.opts.root));
      }
      return rtn;
    }, create(path, mode = 438) {
      mode &= 4095;
      mode |= 32768;
      return FS.mknod(path, mode, 0);
    }, mkdir(path, mode = 511) {
      mode &= 511 | 512;
      mode |= 16384;
      return FS.mknod(path, mode, 0);
    }, mkdirTree(path, mode) {
      var dirs = path.split("/");
      var d = "";
      for (var dir of dirs) {
        if (!dir)
          continue;
        if (d || PATH.isAbs(path))
          d += "/";
        d += dir;
        try {
          FS.mkdir(d, mode);
        } catch (e) {
          if (e.errno != 20)
            throw e;
        }
      }
    }, mkdev(path, mode, dev) {
      if (typeof dev == "undefined") {
        dev = mode;
        mode = 438;
      }
      mode |= 8192;
      return FS.mknod(path, mode, dev);
    }, symlink(oldpath, newpath) {
      if (!PATH_FS.resolve(oldpath)) {
        throw new FS.ErrnoError(44);
      }
      var lookup = FS.lookupPath(newpath, { parent: true });
      var parent = lookup.node;
      if (!parent) {
        throw new FS.ErrnoError(44);
      }
      var newname = PATH.basename(newpath);
      var errCode = FS.mayCreate(parent, newname);
      if (errCode) {
        throw new FS.ErrnoError(errCode);
      }
      if (!parent.node_ops.symlink) {
        throw new FS.ErrnoError(63);
      }
      return parent.node_ops.symlink(parent, newname, oldpath);
    }, rename(old_path, new_path) {
      var old_dirname = PATH.dirname(old_path);
      var new_dirname = PATH.dirname(new_path);
      var old_name = PATH.basename(old_path);
      var new_name = PATH.basename(new_path);
      var lookup, old_dir, new_dir;
      lookup = FS.lookupPath(old_path, { parent: true });
      old_dir = lookup.node;
      lookup = FS.lookupPath(new_path, { parent: true });
      new_dir = lookup.node;
      if (!old_dir || !new_dir)
        throw new FS.ErrnoError(44);
      if (old_dir.mount !== new_dir.mount) {
        throw new FS.ErrnoError(75);
      }
      var old_node = FS.lookupNode(old_dir, old_name);
      var relative = PATH_FS.relative(old_path, new_dirname);
      if (relative.charAt(0) !== ".") {
        throw new FS.ErrnoError(28);
      }
      relative = PATH_FS.relative(new_path, old_dirname);
      if (relative.charAt(0) !== ".") {
        throw new FS.ErrnoError(55);
      }
      var new_node;
      try {
        new_node = FS.lookupNode(new_dir, new_name);
      } catch (e) {
      }
      if (old_node === new_node) {
        return;
      }
      var isdir = FS.isDir(old_node.mode);
      var errCode = FS.mayDelete(old_dir, old_name, isdir);
      if (errCode) {
        throw new FS.ErrnoError(errCode);
      }
      errCode = new_node ? FS.mayDelete(new_dir, new_name, isdir) : FS.mayCreate(new_dir, new_name);
      if (errCode) {
        throw new FS.ErrnoError(errCode);
      }
      if (!old_dir.node_ops.rename) {
        throw new FS.ErrnoError(63);
      }
      if (FS.isMountpoint(old_node) || new_node && FS.isMountpoint(new_node)) {
        throw new FS.ErrnoError(10);
      }
      if (new_dir !== old_dir) {
        errCode = FS.nodePermissions(old_dir, "w");
        if (errCode) {
          throw new FS.ErrnoError(errCode);
        }
      }
      FS.hashRemoveNode(old_node);
      try {
        old_dir.node_ops.rename(old_node, new_dir, new_name);
        old_node.parent = new_dir;
      } catch (e) {
        throw e;
      } finally {
        FS.hashAddNode(old_node);
      }
    }, rmdir(path) {
      var lookup = FS.lookupPath(path, { parent: true });
      var parent = lookup.node;
      var name = PATH.basename(path);
      var node = FS.lookupNode(parent, name);
      var errCode = FS.mayDelete(parent, name, true);
      if (errCode) {
        throw new FS.ErrnoError(errCode);
      }
      if (!parent.node_ops.rmdir) {
        throw new FS.ErrnoError(63);
      }
      if (FS.isMountpoint(node)) {
        throw new FS.ErrnoError(10);
      }
      parent.node_ops.rmdir(parent, name);
      FS.destroyNode(node);
    }, readdir(path) {
      var lookup = FS.lookupPath(path, { follow: true });
      var node = lookup.node;
      var readdir = FS.checkOpExists(node.node_ops.readdir, 54);
      return readdir(node);
    }, unlink(path) {
      var lookup = FS.lookupPath(path, { parent: true });
      var parent = lookup.node;
      if (!parent) {
        throw new FS.ErrnoError(44);
      }
      var name = PATH.basename(path);
      var node = FS.lookupNode(parent, name);
      var errCode = FS.mayDelete(parent, name, false);
      if (errCode) {
        throw new FS.ErrnoError(errCode);
      }
      if (!parent.node_ops.unlink) {
        throw new FS.ErrnoError(63);
      }
      if (FS.isMountpoint(node)) {
        throw new FS.ErrnoError(10);
      }
      parent.node_ops.unlink(parent, name);
      FS.destroyNode(node);
    }, readlink(path) {
      var lookup = FS.lookupPath(path);
      var link = lookup.node;
      if (!link) {
        throw new FS.ErrnoError(44);
      }
      if (!link.node_ops.readlink) {
        throw new FS.ErrnoError(28);
      }
      return link.node_ops.readlink(link);
    }, stat(path, dontFollow) {
      var lookup = FS.lookupPath(path, { follow: !dontFollow });
      var node = lookup.node;
      var getattr = FS.checkOpExists(node.node_ops.getattr, 63);
      return getattr(node);
    }, fstat(fd) {
      var stream = FS.getStreamChecked(fd);
      var node = stream.node;
      var getattr = stream.stream_ops.getattr;
      var arg = getattr ? stream : node;
      getattr ?? (getattr = node.node_ops.getattr);
      FS.checkOpExists(getattr, 63);
      return getattr(arg);
    }, lstat(path) {
      return FS.stat(path, true);
    }, doChmod(stream, node, mode, dontFollow) {
      FS.doSetAttr(stream, node, { mode: mode & 4095 | node.mode & -4096, ctime: Date.now(), dontFollow });
    }, chmod(path, mode, dontFollow) {
      var node;
      if (typeof path == "string") {
        var lookup = FS.lookupPath(path, { follow: !dontFollow });
        node = lookup.node;
      } else {
        node = path;
      }
      FS.doChmod(null, node, mode, dontFollow);
    }, lchmod(path, mode) {
      FS.chmod(path, mode, true);
    }, fchmod(fd, mode) {
      var stream = FS.getStreamChecked(fd);
      FS.doChmod(stream, stream.node, mode, false);
    }, doChown(stream, node, dontFollow) {
      FS.doSetAttr(stream, node, { timestamp: Date.now(), dontFollow });
    }, chown(path, uid, gid, dontFollow) {
      var node;
      if (typeof path == "string") {
        var lookup = FS.lookupPath(path, { follow: !dontFollow });
        node = lookup.node;
      } else {
        node = path;
      }
      FS.doChown(null, node, dontFollow);
    }, lchown(path, uid, gid) {
      FS.chown(path, uid, gid, true);
    }, fchown(fd, uid, gid) {
      var stream = FS.getStreamChecked(fd);
      FS.doChown(stream, stream.node, false);
    }, doTruncate(stream, node, len) {
      if (FS.isDir(node.mode)) {
        throw new FS.ErrnoError(31);
      }
      if (!FS.isFile(node.mode)) {
        throw new FS.ErrnoError(28);
      }
      var errCode = FS.nodePermissions(node, "w");
      if (errCode) {
        throw new FS.ErrnoError(errCode);
      }
      FS.doSetAttr(stream, node, { size: len, timestamp: Date.now() });
    }, truncate(path, len) {
      if (len < 0) {
        throw new FS.ErrnoError(28);
      }
      var node;
      if (typeof path == "string") {
        var lookup = FS.lookupPath(path, { follow: true });
        node = lookup.node;
      } else {
        node = path;
      }
      FS.doTruncate(null, node, len);
    }, ftruncate(fd, len) {
      var stream = FS.getStreamChecked(fd);
      if (len < 0 || (stream.flags & 2097155) === 0) {
        throw new FS.ErrnoError(28);
      }
      FS.doTruncate(stream, stream.node, len);
    }, utime(path, atime, mtime) {
      var lookup = FS.lookupPath(path, { follow: true });
      var node = lookup.node;
      var setattr = FS.checkOpExists(node.node_ops.setattr, 63);
      setattr(node, { atime, mtime });
    }, open(path, flags, mode = 438) {
      if (path === "") {
        throw new FS.ErrnoError(44);
      }
      flags = typeof flags == "string" ? FS_modeStringToFlags(flags) : flags;
      if (flags & 64) {
        mode = mode & 4095 | 32768;
      } else {
        mode = 0;
      }
      var node;
      var isDirPath;
      if (typeof path == "object") {
        node = path;
      } else {
        isDirPath = path.endsWith("/");
        var lookup = FS.lookupPath(path, { follow: !(flags & 131072), noent_okay: true });
        node = lookup.node;
        path = lookup.path;
      }
      var created = false;
      if (flags & 64) {
        if (node) {
          if (flags & 128) {
            throw new FS.ErrnoError(20);
          }
        } else if (isDirPath) {
          throw new FS.ErrnoError(31);
        } else {
          node = FS.mknod(path, mode | 511, 0);
          created = true;
        }
      }
      if (!node) {
        throw new FS.ErrnoError(44);
      }
      if (FS.isChrdev(node.mode)) {
        flags &= -513;
      }
      if (flags & 65536 && !FS.isDir(node.mode)) {
        throw new FS.ErrnoError(54);
      }
      if (!created) {
        var errCode = FS.mayOpen(node, flags);
        if (errCode) {
          throw new FS.ErrnoError(errCode);
        }
      }
      if (flags & 512 && !created) {
        FS.truncate(node, 0);
      }
      flags &= -131713;
      var stream = FS.createStream({ node, path: FS.getPath(node), flags, seekable: true, position: 0, stream_ops: node.stream_ops, ungotten: [], error: false });
      if (stream.stream_ops.open) {
        stream.stream_ops.open(stream);
      }
      if (created) {
        FS.chmod(node, mode & 511);
      }
      if (Module["logReadFiles"] && !(flags & 1)) {
        if (!(path in FS.readFiles)) {
          FS.readFiles[path] = 1;
        }
      }
      return stream;
    }, close(stream) {
      if (FS.isClosed(stream)) {
        throw new FS.ErrnoError(8);
      }
      if (stream.getdents)
        stream.getdents = null;
      try {
        if (stream.stream_ops.close) {
          stream.stream_ops.close(stream);
        }
      } catch (e) {
        throw e;
      } finally {
        FS.closeStream(stream.fd);
      }
      stream.fd = null;
    }, isClosed(stream) {
      return stream.fd === null;
    }, llseek(stream, offset, whence) {
      if (FS.isClosed(stream)) {
        throw new FS.ErrnoError(8);
      }
      if (!stream.seekable || !stream.stream_ops.llseek) {
        throw new FS.ErrnoError(70);
      }
      if (whence != 0 && whence != 1 && whence != 2) {
        throw new FS.ErrnoError(28);
      }
      stream.position = stream.stream_ops.llseek(stream, offset, whence);
      stream.ungotten = [];
      return stream.position;
    }, read(stream, buffer, offset, length, position) {
      if (length < 0 || position < 0) {
        throw new FS.ErrnoError(28);
      }
      if (FS.isClosed(stream)) {
        throw new FS.ErrnoError(8);
      }
      if ((stream.flags & 2097155) === 1) {
        throw new FS.ErrnoError(8);
      }
      if (FS.isDir(stream.node.mode)) {
        throw new FS.ErrnoError(31);
      }
      if (!stream.stream_ops.read) {
        throw new FS.ErrnoError(28);
      }
      var seeking = typeof position != "undefined";
      if (!seeking) {
        position = stream.position;
      } else if (!stream.seekable) {
        throw new FS.ErrnoError(70);
      }
      var bytesRead = stream.stream_ops.read(stream, buffer, offset, length, position);
      if (!seeking)
        stream.position += bytesRead;
      return bytesRead;
    }, write(stream, buffer, offset, length, position, canOwn) {
      if (length < 0 || position < 0) {
        throw new FS.ErrnoError(28);
      }
      if (FS.isClosed(stream)) {
        throw new FS.ErrnoError(8);
      }
      if ((stream.flags & 2097155) === 0) {
        throw new FS.ErrnoError(8);
      }
      if (FS.isDir(stream.node.mode)) {
        throw new FS.ErrnoError(31);
      }
      if (!stream.stream_ops.write) {
        throw new FS.ErrnoError(28);
      }
      if (stream.seekable && stream.flags & 1024) {
        FS.llseek(stream, 0, 2);
      }
      var seeking = typeof position != "undefined";
      if (!seeking) {
        position = stream.position;
      } else if (!stream.seekable) {
        throw new FS.ErrnoError(70);
      }
      var bytesWritten = stream.stream_ops.write(stream, buffer, offset, length, position, canOwn);
      if (!seeking)
        stream.position += bytesWritten;
      return bytesWritten;
    }, mmap(stream, length, position, prot, flags) {
      if ((prot & 2) !== 0 && (flags & 2) === 0 && (stream.flags & 2097155) !== 2) {
        throw new FS.ErrnoError(2);
      }
      if ((stream.flags & 2097155) === 1) {
        throw new FS.ErrnoError(2);
      }
      if (!stream.stream_ops.mmap) {
        throw new FS.ErrnoError(43);
      }
      if (!length) {
        throw new FS.ErrnoError(28);
      }
      return stream.stream_ops.mmap(stream, length, position, prot, flags);
    }, msync(stream, buffer, offset, length, mmapFlags) {
      if (!stream.stream_ops.msync) {
        return 0;
      }
      return stream.stream_ops.msync(stream, buffer, offset, length, mmapFlags);
    }, ioctl(stream, cmd, arg) {
      if (!stream.stream_ops.ioctl) {
        throw new FS.ErrnoError(59);
      }
      return stream.stream_ops.ioctl(stream, cmd, arg);
    }, readFile(path, opts = {}) {
      opts.flags = opts.flags || 0;
      opts.encoding = opts.encoding || "binary";
      if (opts.encoding !== "utf8" && opts.encoding !== "binary") {
        throw new Error(`Invalid encoding type "${opts.encoding}"`);
      }
      var ret;
      var stream = FS.open(path, opts.flags);
      var stat = FS.stat(path);
      var length = stat.size;
      var buf = new Uint8Array(length);
      FS.read(stream, buf, 0, length, 0);
      if (opts.encoding === "utf8") {
        ret = UTF8ArrayToString(buf);
      } else if (opts.encoding === "binary") {
        ret = buf;
      }
      FS.close(stream);
      return ret;
    }, writeFile(path, data, opts = {}) {
      opts.flags = opts.flags || 577;
      var stream = FS.open(path, opts.flags, opts.mode);
      if (typeof data == "string") {
        var buf = new Uint8Array(lengthBytesUTF8(data) + 1);
        var actualNumBytes = stringToUTF8Array(data, buf, 0, buf.length);
        FS.write(stream, buf, 0, actualNumBytes, void 0, opts.canOwn);
      } else if (ArrayBuffer.isView(data)) {
        FS.write(stream, data, 0, data.byteLength, void 0, opts.canOwn);
      } else {
        throw new Error("Unsupported data type");
      }
      FS.close(stream);
    }, cwd: () => FS.currentPath, chdir(path) {
      var lookup = FS.lookupPath(path, { follow: true });
      if (lookup.node === null) {
        throw new FS.ErrnoError(44);
      }
      if (!FS.isDir(lookup.node.mode)) {
        throw new FS.ErrnoError(54);
      }
      var errCode = FS.nodePermissions(lookup.node, "x");
      if (errCode) {
        throw new FS.ErrnoError(errCode);
      }
      FS.currentPath = lookup.path;
    }, createDefaultDirectories() {
      FS.mkdir("/tmp");
      FS.mkdir("/home");
      FS.mkdir("/home/web_user");
    }, createDefaultDevices() {
      FS.mkdir("/dev");
      FS.registerDevice(FS.makedev(1, 3), { read: () => 0, write: (stream, buffer, offset, length, pos) => length, llseek: () => 0 });
      FS.mkdev("/dev/null", FS.makedev(1, 3));
      TTY.register(FS.makedev(5, 0), TTY.default_tty_ops);
      TTY.register(FS.makedev(6, 0), TTY.default_tty1_ops);
      FS.mkdev("/dev/tty", FS.makedev(5, 0));
      FS.mkdev("/dev/tty1", FS.makedev(6, 0));
      var randomBuffer = new Uint8Array(1024), randomLeft = 0;
      var randomByte = () => {
        if (randomLeft === 0) {
          randomFill(randomBuffer);
          randomLeft = randomBuffer.byteLength;
        }
        return randomBuffer[--randomLeft];
      };
      FS.createDevice("/dev", "random", randomByte);
      FS.createDevice("/dev", "urandom", randomByte);
      FS.mkdir("/dev/shm");
      FS.mkdir("/dev/shm/tmp");
    }, createSpecialDirectories() {
      FS.mkdir("/proc");
      var proc_self = FS.mkdir("/proc/self");
      FS.mkdir("/proc/self/fd");
      FS.mount({ mount() {
        var node = FS.createNode(proc_self, "fd", 16895, 73);
        node.stream_ops = { llseek: MEMFS.stream_ops.llseek };
        node.node_ops = { lookup(parent, name) {
          var fd = +name;
          var stream = FS.getStreamChecked(fd);
          var ret = { parent: null, mount: { mountpoint: "fake" }, node_ops: { readlink: () => stream.path }, id: fd + 1 };
          ret.parent = ret;
          return ret;
        }, readdir() {
          return Array.from(FS.streams.entries()).filter(([k, v]) => v).map(([k, v]) => k.toString());
        } };
        return node;
      } }, {}, "/proc/self/fd");
    }, createStandardStreams(input, output, error) {
      if (input) {
        FS.createDevice("/dev", "stdin", input);
      } else {
        FS.symlink("/dev/tty", "/dev/stdin");
      }
      if (output) {
        FS.createDevice("/dev", "stdout", null, output);
      } else {
        FS.symlink("/dev/tty", "/dev/stdout");
      }
      if (error) {
        FS.createDevice("/dev", "stderr", null, error);
      } else {
        FS.symlink("/dev/tty1", "/dev/stderr");
      }
      FS.open("/dev/stdin", 0);
      FS.open("/dev/stdout", 1);
      FS.open("/dev/stderr", 1);
    }, staticInit() {
      FS.nameTable = new Array(4096);
      FS.mount(MEMFS, {}, "/");
      FS.createDefaultDirectories();
      FS.createDefaultDevices();
      FS.createSpecialDirectories();
      FS.filesystems = { MEMFS };
    }, init(input, output, error) {
      FS.initialized = true;
      input ?? (input = Module["stdin"]);
      output ?? (output = Module["stdout"]);
      error ?? (error = Module["stderr"]);
      FS.createStandardStreams(input, output, error);
    }, quit() {
      FS.initialized = false;
      for (var stream of FS.streams) {
        if (stream) {
          FS.close(stream);
        }
      }
    }, findObject(path, dontResolveLastLink) {
      var ret = FS.analyzePath(path, dontResolveLastLink);
      if (!ret.exists) {
        return null;
      }
      return ret.object;
    }, analyzePath(path, dontResolveLastLink) {
      try {
        var lookup = FS.lookupPath(path, { follow: !dontResolveLastLink });
        path = lookup.path;
      } catch (e) {
      }
      var ret = { isRoot: false, exists: false, error: 0, name: null, path: null, object: null, parentExists: false, parentPath: null, parentObject: null };
      try {
        var lookup = FS.lookupPath(path, { parent: true });
        ret.parentExists = true;
        ret.parentPath = lookup.path;
        ret.parentObject = lookup.node;
        ret.name = PATH.basename(path);
        lookup = FS.lookupPath(path, { follow: !dontResolveLastLink });
        ret.exists = true;
        ret.path = lookup.path;
        ret.object = lookup.node;
        ret.name = lookup.node.name;
        ret.isRoot = lookup.path === "/";
      } catch (e) {
        ret.error = e.errno;
      }
      return ret;
    }, createPath(parent, path, canRead, canWrite) {
      parent = typeof parent == "string" ? parent : FS.getPath(parent);
      var parts = path.split("/").reverse();
      while (parts.length) {
        var part = parts.pop();
        if (!part)
          continue;
        var current = PATH.join2(parent, part);
        try {
          FS.mkdir(current);
        } catch (e) {
          if (e.errno != 20)
            throw e;
        }
        parent = current;
      }
      return current;
    }, createFile(parent, name, properties, canRead, canWrite) {
      var path = PATH.join2(typeof parent == "string" ? parent : FS.getPath(parent), name);
      var mode = FS_getMode(canRead, canWrite);
      return FS.create(path, mode);
    }, createDataFile(parent, name, data, canRead, canWrite, canOwn) {
      var path = name;
      if (parent) {
        parent = typeof parent == "string" ? parent : FS.getPath(parent);
        path = name ? PATH.join2(parent, name) : parent;
      }
      var mode = FS_getMode(canRead, canWrite);
      var node = FS.create(path, mode);
      if (data) {
        if (typeof data == "string") {
          var arr = new Array(data.length);
          for (var i = 0, len = data.length; i < len; ++i)
            arr[i] = data.charCodeAt(i);
          data = arr;
        }
        FS.chmod(node, mode | 146);
        var stream = FS.open(node, 577);
        FS.write(stream, data, 0, data.length, 0, canOwn);
        FS.close(stream);
        FS.chmod(node, mode);
      }
    }, createDevice(parent, name, input, output) {
      var _a;
      var path = PATH.join2(typeof parent == "string" ? parent : FS.getPath(parent), name);
      var mode = FS_getMode(!!input, !!output);
      (_a = FS.createDevice).major ?? (_a.major = 64);
      var dev = FS.makedev(FS.createDevice.major++, 0);
      FS.registerDevice(dev, { open(stream) {
        stream.seekable = false;
      }, close(stream) {
        if (output?.buffer?.length) {
          output(10);
        }
      }, read(stream, buffer, offset, length, pos) {
        var bytesRead = 0;
        for (var i = 0; i < length; i++) {
          var result;
          try {
            result = input();
          } catch (e) {
            throw new FS.ErrnoError(29);
          }
          if (result === void 0 && bytesRead === 0) {
            throw new FS.ErrnoError(6);
          }
          if (result === null || result === void 0)
            break;
          bytesRead++;
          buffer[offset + i] = result;
        }
        if (bytesRead) {
          stream.node.atime = Date.now();
        }
        return bytesRead;
      }, write(stream, buffer, offset, length, pos) {
        for (var i = 0; i < length; i++) {
          try {
            output(buffer[offset + i]);
          } catch (e) {
            throw new FS.ErrnoError(29);
          }
        }
        if (length) {
          stream.node.mtime = stream.node.ctime = Date.now();
        }
        return i;
      } });
      return FS.mkdev(path, mode, dev);
    }, forceLoadFile(obj) {
      if (obj.isDevice || obj.isFolder || obj.link || obj.contents)
        return true;
      if (typeof XMLHttpRequest != "undefined") {
        throw new Error("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread.");
      } else {
        try {
          obj.contents = readBinary(obj.url);
          obj.usedBytes = obj.contents.length;
        } catch (e) {
          throw new FS.ErrnoError(29);
        }
      }
    }, createLazyFile(parent, name, url, canRead, canWrite) {
      class LazyUint8Array {
        constructor() {
          __publicField$2(this, "lengthKnown", false);
          __publicField$2(this, "chunks", []);
        }
        get(idx) {
          if (idx > this.length - 1 || idx < 0) {
            return void 0;
          }
          var chunkOffset = idx % this.chunkSize;
          var chunkNum = idx / this.chunkSize | 0;
          return this.getter(chunkNum)[chunkOffset];
        }
        setDataGetter(getter) {
          this.getter = getter;
        }
        cacheLength() {
          var xhr = new XMLHttpRequest();
          xhr.open("HEAD", url, false);
          xhr.send(null);
          if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304))
            throw new Error("Couldn't load " + url + ". Status: " + xhr.status);
          var datalength = Number(xhr.getResponseHeader("Content-length"));
          var header;
          var hasByteServing = (header = xhr.getResponseHeader("Accept-Ranges")) && header === "bytes";
          var usesGzip = (header = xhr.getResponseHeader("Content-Encoding")) && header === "gzip";
          var chunkSize = 1024 * 1024;
          if (!hasByteServing)
            chunkSize = datalength;
          var doXHR = (from, to) => {
            if (from > to)
              throw new Error("invalid range (" + from + ", " + to + ") or no bytes requested!");
            if (to > datalength - 1)
              throw new Error("only " + datalength + " bytes available! programmer error!");
            var xhr2 = new XMLHttpRequest();
            xhr2.open("GET", url, false);
            if (datalength !== chunkSize)
              xhr2.setRequestHeader("Range", "bytes=" + from + "-" + to);
            xhr2.responseType = "arraybuffer";
            if (xhr2.overrideMimeType) {
              xhr2.overrideMimeType("text/plain; charset=x-user-defined");
            }
            xhr2.send(null);
            if (!(xhr2.status >= 200 && xhr2.status < 300 || xhr2.status === 304))
              throw new Error("Couldn't load " + url + ". Status: " + xhr2.status);
            if (xhr2.response !== void 0) {
              return new Uint8Array(xhr2.response || []);
            }
            return intArrayFromString(xhr2.responseText || "");
          };
          var lazyArray2 = this;
          lazyArray2.setDataGetter((chunkNum) => {
            var start = chunkNum * chunkSize;
            var end = (chunkNum + 1) * chunkSize - 1;
            end = Math.min(end, datalength - 1);
            if (typeof lazyArray2.chunks[chunkNum] == "undefined") {
              lazyArray2.chunks[chunkNum] = doXHR(start, end);
            }
            if (typeof lazyArray2.chunks[chunkNum] == "undefined")
              throw new Error("doXHR failed!");
            return lazyArray2.chunks[chunkNum];
          });
          if (usesGzip || !datalength) {
            chunkSize = datalength = 1;
            datalength = this.getter(0).length;
            chunkSize = datalength;
            out("LazyFiles on gzip forces download of the whole file when length is accessed");
          }
          this._length = datalength;
          this._chunkSize = chunkSize;
          this.lengthKnown = true;
        }
        get length() {
          if (!this.lengthKnown) {
            this.cacheLength();
          }
          return this._length;
        }
        get chunkSize() {
          if (!this.lengthKnown) {
            this.cacheLength();
          }
          return this._chunkSize;
        }
      }
      if (typeof XMLHttpRequest != "undefined") {
        throw "Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";
        var lazyArray = new LazyUint8Array();
        var properties = { isDevice: false, contents: lazyArray };
      } else {
        var properties = { isDevice: false, url };
      }
      var node = FS.createFile(parent, name, properties, canRead, canWrite);
      if (properties.contents) {
        node.contents = properties.contents;
      } else if (properties.url) {
        node.contents = null;
        node.url = properties.url;
      }
      Object.defineProperties(node, { usedBytes: { get: function() {
        return this.contents.length;
      } } });
      var stream_ops = {};
      var keys = Object.keys(node.stream_ops);
      keys.forEach((key) => {
        var fn = node.stream_ops[key];
        stream_ops[key] = (...args) => {
          FS.forceLoadFile(node);
          return fn(...args);
        };
      });
      function writeChunks(stream, buffer, offset, length, position) {
        var contents = stream.node.contents;
        if (position >= contents.length)
          return 0;
        var size = Math.min(contents.length - position, length);
        if (contents.slice) {
          for (var i = 0; i < size; i++) {
            buffer[offset + i] = contents[position + i];
          }
        } else {
          for (var i = 0; i < size; i++) {
            buffer[offset + i] = contents.get(position + i);
          }
        }
        return size;
      }
      stream_ops.read = (stream, buffer, offset, length, position) => {
        FS.forceLoadFile(node);
        return writeChunks(stream, buffer, offset, length, position);
      };
      stream_ops.mmap = (stream, length, position, prot, flags) => {
        FS.forceLoadFile(node);
        var ptr = mmapAlloc();
        if (!ptr) {
          throw new FS.ErrnoError(48);
        }
        writeChunks(stream, HEAP8, ptr, length, position);
        return { ptr, allocated: true };
      };
      node.stream_ops = stream_ops;
      return node;
    } };
    var UTF8ToString = (ptr, maxBytesToRead) => ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead) : "";
    var SYSCALLS = { DEFAULT_POLLMASK: 5, calculateAt(dirfd, path, allowEmpty) {
      if (PATH.isAbs(path)) {
        return path;
      }
      var dir;
      if (dirfd === -100) {
        dir = FS.cwd();
      } else {
        var dirstream = SYSCALLS.getStreamFromFD(dirfd);
        dir = dirstream.path;
      }
      if (path.length == 0) {
        if (!allowEmpty) {
          throw new FS.ErrnoError(44);
        }
        return dir;
      }
      return dir + "/" + path;
    }, writeStat(buf, stat) {
      HEAP32[buf >> 2] = stat.dev;
      HEAP32[buf + 4 >> 2] = stat.mode;
      HEAPU32[buf + 8 >> 2] = stat.nlink;
      HEAP32[buf + 12 >> 2] = stat.uid;
      HEAP32[buf + 16 >> 2] = stat.gid;
      HEAP32[buf + 20 >> 2] = stat.rdev;
      HEAP64[buf + 24 >> 3] = BigInt(stat.size);
      HEAP32[buf + 32 >> 2] = 4096;
      HEAP32[buf + 36 >> 2] = stat.blocks;
      var atime = stat.atime.getTime();
      var mtime = stat.mtime.getTime();
      var ctime = stat.ctime.getTime();
      HEAP64[buf + 40 >> 3] = BigInt(Math.floor(atime / 1e3));
      HEAPU32[buf + 48 >> 2] = atime % 1e3 * 1e3 * 1e3;
      HEAP64[buf + 56 >> 3] = BigInt(Math.floor(mtime / 1e3));
      HEAPU32[buf + 64 >> 2] = mtime % 1e3 * 1e3 * 1e3;
      HEAP64[buf + 72 >> 3] = BigInt(Math.floor(ctime / 1e3));
      HEAPU32[buf + 80 >> 2] = ctime % 1e3 * 1e3 * 1e3;
      HEAP64[buf + 88 >> 3] = BigInt(stat.ino);
      return 0;
    }, writeStatFs(buf, stats) {
      HEAP32[buf + 4 >> 2] = stats.bsize;
      HEAP32[buf + 40 >> 2] = stats.bsize;
      HEAP32[buf + 8 >> 2] = stats.blocks;
      HEAP32[buf + 12 >> 2] = stats.bfree;
      HEAP32[buf + 16 >> 2] = stats.bavail;
      HEAP32[buf + 20 >> 2] = stats.files;
      HEAP32[buf + 24 >> 2] = stats.ffree;
      HEAP32[buf + 28 >> 2] = stats.fsid;
      HEAP32[buf + 44 >> 2] = stats.flags;
      HEAP32[buf + 36 >> 2] = stats.namelen;
    }, doMsync(addr, stream, len, flags, offset) {
      if (!FS.isFile(stream.node.mode)) {
        throw new FS.ErrnoError(43);
      }
      if (flags & 2) {
        return 0;
      }
      var buffer = HEAPU8.slice(addr, addr + len);
      FS.msync(stream, buffer, offset, len, flags);
    }, getStreamFromFD(fd) {
      var stream = FS.getStreamChecked(fd);
      return stream;
    }, varargs: void 0, getStr(ptr) {
      var ret = UTF8ToString(ptr);
      return ret;
    } };
    function ___syscall_fcntl64(fd, cmd, varargs) {
      SYSCALLS.varargs = varargs;
      try {
        var stream = SYSCALLS.getStreamFromFD(fd);
        switch (cmd) {
          case 0: {
            var arg = syscallGetVarargI();
            if (arg < 0) {
              return -28;
            }
            while (FS.streams[arg]) {
              arg++;
            }
            var newStream;
            newStream = FS.dupStream(stream, arg);
            return newStream.fd;
          }
          case 1:
          case 2:
            return 0;
          case 3:
            return stream.flags;
          case 4: {
            var arg = syscallGetVarargI();
            stream.flags |= arg;
            return 0;
          }
          case 12: {
            var arg = syscallGetVarargP();
            var offset = 0;
            HEAP16[arg + offset >> 1] = 2;
            return 0;
          }
          case 13:
          case 14:
            return 0;
        }
        return -28;
      } catch (e) {
        if (typeof FS == "undefined" || !(e.name === "ErrnoError"))
          throw e;
        return -e.errno;
      }
    }
    function ___syscall_ioctl(fd, op, varargs) {
      SYSCALLS.varargs = varargs;
      try {
        var stream = SYSCALLS.getStreamFromFD(fd);
        switch (op) {
          case 21509: {
            if (!stream.tty)
              return -59;
            return 0;
          }
          case 21505: {
            if (!stream.tty)
              return -59;
            if (stream.tty.ops.ioctl_tcgets) {
              var termios = stream.tty.ops.ioctl_tcgets(stream);
              var argp = syscallGetVarargP();
              HEAP32[argp >> 2] = termios.c_iflag || 0;
              HEAP32[argp + 4 >> 2] = termios.c_oflag || 0;
              HEAP32[argp + 8 >> 2] = termios.c_cflag || 0;
              HEAP32[argp + 12 >> 2] = termios.c_lflag || 0;
              for (var i = 0; i < 32; i++) {
                HEAP8[argp + i + 17] = termios.c_cc[i] || 0;
              }
              return 0;
            }
            return 0;
          }
          case 21510:
          case 21511:
          case 21512: {
            if (!stream.tty)
              return -59;
            return 0;
          }
          case 21506:
          case 21507:
          case 21508: {
            if (!stream.tty)
              return -59;
            if (stream.tty.ops.ioctl_tcsets) {
              var argp = syscallGetVarargP();
              var c_iflag = HEAP32[argp >> 2];
              var c_oflag = HEAP32[argp + 4 >> 2];
              var c_cflag = HEAP32[argp + 8 >> 2];
              var c_lflag = HEAP32[argp + 12 >> 2];
              var c_cc = [];
              for (var i = 0; i < 32; i++) {
                c_cc.push(HEAP8[argp + i + 17]);
              }
              return stream.tty.ops.ioctl_tcsets(stream.tty, op, { c_iflag, c_oflag, c_cflag, c_lflag, c_cc });
            }
            return 0;
          }
          case 21519: {
            if (!stream.tty)
              return -59;
            var argp = syscallGetVarargP();
            HEAP32[argp >> 2] = 0;
            return 0;
          }
          case 21520: {
            if (!stream.tty)
              return -59;
            return -28;
          }
          case 21531: {
            var argp = syscallGetVarargP();
            return FS.ioctl(stream, op, argp);
          }
          case 21523: {
            if (!stream.tty)
              return -59;
            if (stream.tty.ops.ioctl_tiocgwinsz) {
              var winsize = stream.tty.ops.ioctl_tiocgwinsz(stream.tty);
              var argp = syscallGetVarargP();
              HEAP16[argp >> 1] = winsize[0];
              HEAP16[argp + 2 >> 1] = winsize[1];
            }
            return 0;
          }
          case 21524: {
            if (!stream.tty)
              return -59;
            return 0;
          }
          case 21515: {
            if (!stream.tty)
              return -59;
            return 0;
          }
          default:
            return -28;
        }
      } catch (e) {
        if (typeof FS == "undefined" || !(e.name === "ErrnoError"))
          throw e;
        return -e.errno;
      }
    }
    function ___syscall_openat(dirfd, path, flags, varargs) {
      SYSCALLS.varargs = varargs;
      try {
        path = SYSCALLS.getStr(path);
        path = SYSCALLS.calculateAt(dirfd, path);
        var mode = varargs ? syscallGetVarargI() : 0;
        return FS.open(path, flags, mode).fd;
      } catch (e) {
        if (typeof FS == "undefined" || !(e.name === "ErrnoError"))
          throw e;
        return -e.errno;
      }
    }
    function ___syscall_renameat(olddirfd, oldpath, newdirfd, newpath) {
      try {
        oldpath = SYSCALLS.getStr(oldpath);
        newpath = SYSCALLS.getStr(newpath);
        oldpath = SYSCALLS.calculateAt(olddirfd, oldpath);
        newpath = SYSCALLS.calculateAt(newdirfd, newpath);
        FS.rename(oldpath, newpath);
        return 0;
      } catch (e) {
        if (typeof FS == "undefined" || !(e.name === "ErrnoError"))
          throw e;
        return -e.errno;
      }
    }
    function ___syscall_rmdir(path) {
      try {
        path = SYSCALLS.getStr(path);
        FS.rmdir(path);
        return 0;
      } catch (e) {
        if (typeof FS == "undefined" || !(e.name === "ErrnoError"))
          throw e;
        return -e.errno;
      }
    }
    function ___syscall_stat64(path, buf) {
      try {
        path = SYSCALLS.getStr(path);
        return SYSCALLS.writeStat(buf, FS.stat(path));
      } catch (e) {
        if (typeof FS == "undefined" || !(e.name === "ErrnoError"))
          throw e;
        return -e.errno;
      }
    }
    function ___syscall_unlinkat(dirfd, path, flags) {
      try {
        path = SYSCALLS.getStr(path);
        path = SYSCALLS.calculateAt(dirfd, path);
        if (!flags) {
          FS.unlink(path);
        } else if (flags === 512) {
          FS.rmdir(path);
        } else {
          return -28;
        }
        return 0;
      } catch (e) {
        if (typeof FS == "undefined" || !(e.name === "ErrnoError"))
          throw e;
        return -e.errno;
      }
    }
    var __abort_js = () => abort("");
    var structRegistrations = {};
    var runDestructors = (destructors) => {
      while (destructors.length) {
        var ptr = destructors.pop();
        var del = destructors.pop();
        del(ptr);
      }
    };
    function readPointer(pointer) {
      return this["fromWireType"](HEAPU32[pointer >> 2]);
    }
    var awaitingDependencies = {};
    var registeredTypes = {};
    var typeDependencies = {};
    var InternalError = class InternalError extends Error {
      constructor(message) {
        super(message);
        this.name = "InternalError";
      }
    };
    var throwInternalError = (message) => {
      throw new InternalError(message);
    };
    var whenDependentTypesAreResolved = (myTypes, dependentTypes, getTypeConverters) => {
      myTypes.forEach((type) => typeDependencies[type] = dependentTypes);
      function onComplete(typeConverters2) {
        var myTypeConverters = getTypeConverters(typeConverters2);
        if (myTypeConverters.length !== myTypes.length) {
          throwInternalError("Mismatched type converter count");
        }
        for (var i = 0; i < myTypes.length; ++i) {
          registerType(myTypes[i], myTypeConverters[i]);
        }
      }
      var typeConverters = new Array(dependentTypes.length);
      var unregisteredTypes = [];
      var registered = 0;
      dependentTypes.forEach((dt, i) => {
        if (registeredTypes.hasOwnProperty(dt)) {
          typeConverters[i] = registeredTypes[dt];
        } else {
          unregisteredTypes.push(dt);
          if (!awaitingDependencies.hasOwnProperty(dt)) {
            awaitingDependencies[dt] = [];
          }
          awaitingDependencies[dt].push(() => {
            typeConverters[i] = registeredTypes[dt];
            ++registered;
            if (registered === unregisteredTypes.length) {
              onComplete(typeConverters);
            }
          });
        }
      });
      if (0 === unregisteredTypes.length) {
        onComplete(typeConverters);
      }
    };
    var __embind_finalize_value_object = (structType) => {
      var reg = structRegistrations[structType];
      delete structRegistrations[structType];
      var rawConstructor = reg.rawConstructor;
      var rawDestructor = reg.rawDestructor;
      var fieldRecords = reg.fields;
      var fieldTypes = fieldRecords.map((field) => field.getterReturnType).concat(fieldRecords.map((field) => field.setterArgumentType));
      whenDependentTypesAreResolved([structType], fieldTypes, (fieldTypes2) => {
        var fields = {};
        fieldRecords.forEach((field, i) => {
          var fieldName = field.fieldName;
          var getterReturnType = fieldTypes2[i];
          var optional = fieldTypes2[i].optional;
          var getter = field.getter;
          var getterContext = field.getterContext;
          var setterArgumentType = fieldTypes2[i + fieldRecords.length];
          var setter = field.setter;
          var setterContext = field.setterContext;
          fields[fieldName] = { read: (ptr) => getterReturnType["fromWireType"](getter(getterContext, ptr)), write: (ptr, o) => {
            var destructors = [];
            setter(setterContext, ptr, setterArgumentType["toWireType"](destructors, o));
            runDestructors(destructors);
          }, optional };
        });
        return [{ name: reg.name, fromWireType: (ptr) => {
          var rv = {};
          for (var i in fields) {
            rv[i] = fields[i].read(ptr);
          }
          rawDestructor(ptr);
          return rv;
        }, toWireType: (destructors, o) => {
          for (var fieldName in fields) {
            if (!(fieldName in o) && !fields[fieldName].optional) {
              throw new TypeError(`Missing field: "${fieldName}"`);
            }
          }
          var ptr = rawConstructor();
          for (fieldName in fields) {
            fields[fieldName].write(ptr, o[fieldName]);
          }
          if (destructors !== null) {
            destructors.push(rawDestructor, ptr);
          }
          return ptr;
        }, argPackAdvance: GenericWireTypeSize, readValueFromPointer: readPointer, destructorFunction: rawDestructor }];
      });
    };
    var embind_init_charCodes = () => {
      var codes = new Array(256);
      for (var i = 0; i < 256; ++i) {
        codes[i] = String.fromCharCode(i);
      }
      embind_charCodes = codes;
    };
    var embind_charCodes;
    var readLatin1String = (ptr) => {
      var ret = "";
      var c = ptr;
      while (HEAPU8[c]) {
        ret += embind_charCodes[HEAPU8[c++]];
      }
      return ret;
    };
    var BindingError = class BindingError extends Error {
      constructor(message) {
        super(message);
        this.name = "BindingError";
      }
    };
    var throwBindingError = (message) => {
      throw new BindingError(message);
    };
    function sharedRegisterType(rawType, registeredInstance, options = {}) {
      var name = registeredInstance.name;
      if (!rawType) {
        throwBindingError(`type "${name}" must have a positive integer typeid pointer`);
      }
      if (registeredTypes.hasOwnProperty(rawType)) {
        if (options.ignoreDuplicateRegistrations) {
          return;
        } else {
          throwBindingError(`Cannot register type '${name}' twice`);
        }
      }
      registeredTypes[rawType] = registeredInstance;
      delete typeDependencies[rawType];
      if (awaitingDependencies.hasOwnProperty(rawType)) {
        var callbacks = awaitingDependencies[rawType];
        delete awaitingDependencies[rawType];
        callbacks.forEach((cb) => cb());
      }
    }
    function registerType(rawType, registeredInstance, options = {}) {
      return sharedRegisterType(rawType, registeredInstance, options);
    }
    var integerReadValueFromPointer = (name, width, signed) => {
      switch (width) {
        case 1:
          return signed ? (pointer) => HEAP8[pointer] : (pointer) => HEAPU8[pointer];
        case 2:
          return signed ? (pointer) => HEAP16[pointer >> 1] : (pointer) => HEAPU16[pointer >> 1];
        case 4:
          return signed ? (pointer) => HEAP32[pointer >> 2] : (pointer) => HEAPU32[pointer >> 2];
        case 8:
          return signed ? (pointer) => HEAP64[pointer >> 3] : (pointer) => HEAPU64[pointer >> 3];
        default:
          throw new TypeError(`invalid integer width (${width}): ${name}`);
      }
    };
    var __embind_register_bigint = (primitiveType, name, size, minRange, maxRange) => {
      name = readLatin1String(name);
      const isUnsignedType = minRange === 0n;
      let fromWireType = (value) => value;
      if (isUnsignedType) {
        const bitSize = size * 8;
        fromWireType = (value) => BigInt.asUintN(bitSize, value);
        maxRange = fromWireType(maxRange);
      }
      registerType(primitiveType, { name, fromWireType, toWireType: (destructors, value) => {
        if (typeof value == "number") {
          value = BigInt(value);
        }
        return value;
      }, argPackAdvance: GenericWireTypeSize, readValueFromPointer: integerReadValueFromPointer(name, size, !isUnsignedType), destructorFunction: null });
    };
    var GenericWireTypeSize = 8;
    var __embind_register_bool = (rawType, name, trueValue, falseValue) => {
      name = readLatin1String(name);
      registerType(rawType, { name, fromWireType: function(wt) {
        return !!wt;
      }, toWireType: function(destructors, o) {
        return o ? trueValue : falseValue;
      }, argPackAdvance: GenericWireTypeSize, readValueFromPointer: function(pointer) {
        return this["fromWireType"](HEAPU8[pointer]);
      }, destructorFunction: null });
    };
    var emval_freelist = [];
    var emval_handles = [0, 1, , 1, null, 1, true, 1, false, 1];
    var __emval_decref = (handle) => {
      if (handle > 9 && 0 === --emval_handles[handle + 1]) {
        emval_handles[handle] = void 0;
        emval_freelist.push(handle);
      }
    };
    var Emval = { toValue: (handle) => {
      if (!handle) {
        throwBindingError(`Cannot use deleted val. handle = ${handle}`);
      }
      return emval_handles[handle];
    }, toHandle: (value) => {
      switch (value) {
        case void 0:
          return 2;
        case null:
          return 4;
        case true:
          return 6;
        case false:
          return 8;
        default: {
          const handle = emval_freelist.pop() || emval_handles.length;
          emval_handles[handle] = value;
          emval_handles[handle + 1] = 1;
          return handle;
        }
      }
    } };
    var EmValType = { name: "emscripten::val", fromWireType: (handle) => {
      var rv = Emval.toValue(handle);
      __emval_decref(handle);
      return rv;
    }, toWireType: (destructors, value) => Emval.toHandle(value), argPackAdvance: GenericWireTypeSize, readValueFromPointer: readPointer, destructorFunction: null };
    var __embind_register_emval = (rawType) => registerType(rawType, EmValType);
    var floatReadValueFromPointer = (name, width) => {
      switch (width) {
        case 4:
          return function(pointer) {
            return this["fromWireType"](HEAPF32[pointer >> 2]);
          };
        case 8:
          return function(pointer) {
            return this["fromWireType"](HEAPF64[pointer >> 3]);
          };
        default:
          throw new TypeError(`invalid float width (${width}): ${name}`);
      }
    };
    var __embind_register_float = (rawType, name, size) => {
      name = readLatin1String(name);
      registerType(rawType, { name, fromWireType: (value) => value, toWireType: (destructors, value) => value, argPackAdvance: GenericWireTypeSize, readValueFromPointer: floatReadValueFromPointer(name, size), destructorFunction: null });
    };
    var createNamedFunction = (name, func) => Object.defineProperty(func, "name", { value: name });
    function usesDestructorStack(argTypes) {
      for (var i = 1; i < argTypes.length; ++i) {
        if (argTypes[i] !== null && argTypes[i].destructorFunction === void 0) {
          return true;
        }
      }
      return false;
    }
    function createJsInvoker(argTypes, isClassMethodFunc, returns, isAsync) {
      var needsDestructorStack = usesDestructorStack(argTypes);
      var argCount = argTypes.length - 2;
      var argsList = [];
      var argsListWired = ["fn"];
      for (var i = 0; i < argCount; ++i) {
        argsList.push(`arg${i}`);
        argsListWired.push(`arg${i}Wired`);
      }
      argsList = argsList.join(",");
      argsListWired = argsListWired.join(",");
      var invokerFnBody = `return function (${argsList}) {
`;
      if (needsDestructorStack) {
        invokerFnBody += "var destructors = [];\n";
      }
      var dtorStack = needsDestructorStack ? "destructors" : "null";
      var args1 = ["humanName", "throwBindingError", "invoker", "fn", "runDestructors", "retType", "classParam"];
      for (var i = 0; i < argCount; ++i) {
        invokerFnBody += `var arg${i}Wired = argType${i}['toWireType'](${dtorStack}, arg${i});
`;
        args1.push(`argType${i}`);
      }
      invokerFnBody += (returns || isAsync ? "var rv = " : "") + `invoker(${argsListWired});
`;
      var returnVal = returns ? "rv" : "";
      args1.push("Asyncify");
      invokerFnBody += `function onDone(${returnVal}) {
`;
      if (needsDestructorStack) {
        invokerFnBody += "runDestructors(destructors);\n";
      } else {
        for (var i = 2; i < argTypes.length; ++i) {
          var paramName = i === 1 ? "thisWired" : "arg" + (i - 2) + "Wired";
          if (argTypes[i].destructorFunction !== null) {
            invokerFnBody += `${paramName}_dtor(${paramName});
`;
            args1.push(`${paramName}_dtor`);
          }
        }
      }
      if (returns) {
        invokerFnBody += "var ret = retType['fromWireType'](rv);\nreturn ret;\n";
      }
      invokerFnBody += "}\n";
      invokerFnBody += `return Asyncify.currData ? Asyncify.whenDone().then(onDone) : onDone(${returnVal});
`;
      invokerFnBody += "}\n";
      return [args1, invokerFnBody];
    }
    var runAndAbortIfError = (func) => {
      try {
        return func();
      } catch (e) {
        abort(e);
      }
    };
    var handleException = (e) => {
      if (e instanceof ExitStatus || e == "unwind") {
        return EXITSTATUS;
      }
      quit_(1, e);
    };
    var runtimeKeepaliveCounter = 0;
    var keepRuntimeAlive = () => noExitRuntime || runtimeKeepaliveCounter > 0;
    var _proc_exit = (code) => {
      EXITSTATUS = code;
      if (!keepRuntimeAlive()) {
        Module["onExit"]?.(code);
        ABORT = true;
      }
      quit_(code, new ExitStatus(code));
    };
    var exitJS = (status, implicit) => {
      EXITSTATUS = status;
      _proc_exit(status);
    };
    var _exit = exitJS;
    var maybeExit = () => {
      if (!keepRuntimeAlive()) {
        try {
          _exit(EXITSTATUS);
        } catch (e) {
          handleException(e);
        }
      }
    };
    var callUserCallback = (func) => {
      if (ABORT) {
        return;
      }
      try {
        func();
        maybeExit();
      } catch (e) {
        handleException(e);
      }
    };
    var Asyncify = { instrumentWasmImports(imports) {
      var importPattern = /^(invoke_.*|__asyncjs__.*)$/;
      for (let [x, original] of Object.entries(imports)) {
        if (typeof original == "function") {
          original.isAsync || importPattern.test(x);
        }
      }
    }, instrumentWasmExports(exports) {
      var ret = {};
      for (let [x, original] of Object.entries(exports)) {
        if (typeof original == "function") {
          ret[x] = (...args) => {
            Asyncify.exportCallStack.push(x);
            try {
              return original(...args);
            } finally {
              if (!ABORT) {
                Asyncify.exportCallStack.pop();
                Asyncify.maybeStopUnwind();
              }
            }
          };
        } else {
          ret[x] = original;
        }
      }
      return ret;
    }, State: { Normal: 0, Unwinding: 1, Rewinding: 2, Disabled: 3 }, state: 0, StackSize: 4096, currData: null, handleSleepReturnValue: 0, exportCallStack: [], callStackNameToId: {}, callStackIdToName: {}, callStackId: 0, asyncPromiseHandlers: null, sleepCallbacks: [], getCallStackId(funcName) {
      var id = Asyncify.callStackNameToId[funcName];
      if (id === void 0) {
        id = Asyncify.callStackId++;
        Asyncify.callStackNameToId[funcName] = id;
        Asyncify.callStackIdToName[id] = funcName;
      }
      return id;
    }, maybeStopUnwind() {
      if (Asyncify.currData && Asyncify.state === Asyncify.State.Unwinding && Asyncify.exportCallStack.length === 0) {
        Asyncify.state = Asyncify.State.Normal;
        runAndAbortIfError(_asyncify_stop_unwind);
        if (typeof Fibers != "undefined") {
          Fibers.trampoline();
        }
      }
    }, whenDone() {
      return new Promise((resolve, reject) => {
        Asyncify.asyncPromiseHandlers = { resolve, reject };
      });
    }, allocateData() {
      var ptr = _malloc(12 + Asyncify.StackSize);
      Asyncify.setDataHeader(ptr, ptr + 12, Asyncify.StackSize);
      Asyncify.setDataRewindFunc(ptr);
      return ptr;
    }, setDataHeader(ptr, stack, stackSize) {
      HEAPU32[ptr >> 2] = stack;
      HEAPU32[ptr + 4 >> 2] = stack + stackSize;
    }, setDataRewindFunc(ptr) {
      var bottomOfCallStack = Asyncify.exportCallStack[0];
      var rewindId = Asyncify.getCallStackId(bottomOfCallStack);
      HEAP32[ptr + 8 >> 2] = rewindId;
    }, getDataRewindFuncName(ptr) {
      var id = HEAP32[ptr + 8 >> 2];
      var name = Asyncify.callStackIdToName[id];
      return name;
    }, getDataRewindFunc(name) {
      var func = wasmExports[name];
      return func;
    }, doRewind(ptr) {
      var name = Asyncify.getDataRewindFuncName(ptr);
      var func = Asyncify.getDataRewindFunc(name);
      return func();
    }, handleSleep(startAsync) {
      if (ABORT)
        return;
      if (Asyncify.state === Asyncify.State.Normal) {
        var reachedCallback = false;
        var reachedAfterCallback = false;
        startAsync((handleSleepReturnValue = 0) => {
          if (ABORT)
            return;
          Asyncify.handleSleepReturnValue = handleSleepReturnValue;
          reachedCallback = true;
          if (!reachedAfterCallback) {
            return;
          }
          Asyncify.state = Asyncify.State.Rewinding;
          runAndAbortIfError(() => _asyncify_start_rewind(Asyncify.currData));
          if (typeof MainLoop != "undefined" && MainLoop.func) {
            MainLoop.resume();
          }
          var asyncWasmReturnValue, isError = false;
          try {
            asyncWasmReturnValue = Asyncify.doRewind(Asyncify.currData);
          } catch (err2) {
            asyncWasmReturnValue = err2;
            isError = true;
          }
          var handled = false;
          if (!Asyncify.currData) {
            var asyncPromiseHandlers = Asyncify.asyncPromiseHandlers;
            if (asyncPromiseHandlers) {
              Asyncify.asyncPromiseHandlers = null;
              (isError ? asyncPromiseHandlers.reject : asyncPromiseHandlers.resolve)(asyncWasmReturnValue);
              handled = true;
            }
          }
          if (isError && !handled) {
            throw asyncWasmReturnValue;
          }
        });
        reachedAfterCallback = true;
        if (!reachedCallback) {
          Asyncify.state = Asyncify.State.Unwinding;
          Asyncify.currData = Asyncify.allocateData();
          if (typeof MainLoop != "undefined" && MainLoop.func) {
            MainLoop.pause();
          }
          runAndAbortIfError(() => _asyncify_start_unwind(Asyncify.currData));
        }
      } else if (Asyncify.state === Asyncify.State.Rewinding) {
        Asyncify.state = Asyncify.State.Normal;
        runAndAbortIfError(_asyncify_stop_rewind);
        _free(Asyncify.currData);
        Asyncify.currData = null;
        Asyncify.sleepCallbacks.forEach(callUserCallback);
      } else {
        abort(`invalid state: ${Asyncify.state}`);
      }
      return Asyncify.handleSleepReturnValue;
    }, handleAsync: (startAsync) => Asyncify.handleSleep((wakeUp) => {
      startAsync().then(wakeUp);
    }) };
    function craftInvokerFunction(humanName, argTypes, classType, cppInvokerFunc, cppTargetFunc, isAsync) {
      var argCount = argTypes.length;
      if (argCount < 2) {
        throwBindingError("argTypes array size mismatch! Must at least get return value and 'this' types!");
      }
      var isClassMethodFunc = argTypes[1] !== null && classType !== null;
      var needsDestructorStack = usesDestructorStack(argTypes);
      var returns = argTypes[0].name !== "void";
      var closureArgs = [humanName, throwBindingError, cppInvokerFunc, cppTargetFunc, runDestructors, argTypes[0], argTypes[1]];
      for (var i = 0; i < argCount - 2; ++i) {
        closureArgs.push(argTypes[i + 2]);
      }
      closureArgs.push(Asyncify);
      if (!needsDestructorStack) {
        for (var i = 2; i < argTypes.length; ++i) {
          if (argTypes[i].destructorFunction !== null) {
            closureArgs.push(argTypes[i].destructorFunction);
          }
        }
      }
      let [args, invokerFnBody] = createJsInvoker(argTypes, isClassMethodFunc, returns, isAsync);
      var invokerFn = new Function(...args, invokerFnBody)(...closureArgs);
      return createNamedFunction(humanName, invokerFn);
    }
    var ensureOverloadTable = (proto, methodName, humanName) => {
      if (void 0 === proto[methodName].overloadTable) {
        var prevFunc = proto[methodName];
        proto[methodName] = function(...args) {
          if (!proto[methodName].overloadTable.hasOwnProperty(args.length)) {
            throwBindingError(`Function '${humanName}' called with an invalid number of arguments (${args.length}) - expects one of (${proto[methodName].overloadTable})!`);
          }
          return proto[methodName].overloadTable[args.length].apply(this, args);
        };
        proto[methodName].overloadTable = [];
        proto[methodName].overloadTable[prevFunc.argCount] = prevFunc;
      }
    };
    var exposePublicSymbol = (name, value, numArguments) => {
      if (Module.hasOwnProperty(name)) {
        if (void 0 === numArguments || void 0 !== Module[name].overloadTable && void 0 !== Module[name].overloadTable[numArguments]) {
          throwBindingError(`Cannot register public name '${name}' twice`);
        }
        ensureOverloadTable(Module, name, name);
        if (Module[name].overloadTable.hasOwnProperty(numArguments)) {
          throwBindingError(`Cannot register multiple overloads of a function with the same number of arguments (${numArguments})!`);
        }
        Module[name].overloadTable[numArguments] = value;
      } else {
        Module[name] = value;
        Module[name].argCount = numArguments;
      }
    };
    var heap32VectorToArray = (count, firstElement) => {
      var array = [];
      for (var i = 0; i < count; i++) {
        array.push(HEAPU32[firstElement + i * 4 >> 2]);
      }
      return array;
    };
    var replacePublicSymbol = (name, value, numArguments) => {
      if (!Module.hasOwnProperty(name)) {
        throwInternalError("Replacing nonexistent public symbol");
      }
      if (void 0 !== Module[name].overloadTable && void 0 !== numArguments) {
        Module[name].overloadTable[numArguments] = value;
      } else {
        Module[name] = value;
        Module[name].argCount = numArguments;
      }
    };
    var getDynCaller = (sig, ptr, promising = false) => (...args) => dynCall(sig, ptr, args, promising);
    var embind__requireFunction = (signature, rawFunction, isAsync = false) => {
      signature = readLatin1String(signature);
      function makeDynCaller() {
        return getDynCaller(signature, rawFunction);
      }
      var fp = makeDynCaller();
      if (typeof fp != "function") {
        throwBindingError(`unknown function pointer with signature ${signature}: ${rawFunction}`);
      }
      return fp;
    };
    class UnboundTypeError extends Error {
    }
    var getTypeName = (type) => {
      var ptr = ___getTypeName(type);
      var rv = readLatin1String(ptr);
      _free(ptr);
      return rv;
    };
    var throwUnboundTypeError = (message, types) => {
      var unboundTypes = [];
      var seen = {};
      function visit(type) {
        if (seen[type]) {
          return;
        }
        if (registeredTypes[type]) {
          return;
        }
        if (typeDependencies[type]) {
          typeDependencies[type].forEach(visit);
          return;
        }
        unboundTypes.push(type);
        seen[type] = true;
      }
      types.forEach(visit);
      throw new UnboundTypeError(`${message}: ` + unboundTypes.map(getTypeName).join([", "]));
    };
    var getFunctionName = (signature) => {
      signature = signature.trim();
      const argsIndex = signature.indexOf("(");
      if (argsIndex === -1)
        return signature;
      return signature.slice(0, argsIndex);
    };
    var __embind_register_function = (name, argCount, rawArgTypesAddr, signature, rawInvoker, fn, isAsync, isNonnullReturn) => {
      var argTypes = heap32VectorToArray(argCount, rawArgTypesAddr);
      name = readLatin1String(name);
      name = getFunctionName(name);
      rawInvoker = embind__requireFunction(signature, rawInvoker, isAsync);
      exposePublicSymbol(name, function() {
        throwUnboundTypeError(`Cannot call ${name} due to unbound types`, argTypes);
      }, argCount - 1);
      whenDependentTypesAreResolved([], argTypes, (argTypes2) => {
        var invokerArgsArray = [argTypes2[0], null].concat(argTypes2.slice(1));
        replacePublicSymbol(name, craftInvokerFunction(name, invokerArgsArray, null, rawInvoker, fn, isAsync), argCount - 1);
        return [];
      });
    };
    var __embind_register_integer = (primitiveType, name, size, minRange, maxRange) => {
      name = readLatin1String(name);
      const isUnsignedType = minRange === 0;
      let fromWireType = (value) => value;
      if (isUnsignedType) {
        var bitshift = 32 - 8 * size;
        fromWireType = (value) => value << bitshift >>> bitshift;
        maxRange = fromWireType(maxRange);
      }
      registerType(primitiveType, { name, fromWireType, toWireType: (destructors, value) => value, argPackAdvance: GenericWireTypeSize, readValueFromPointer: integerReadValueFromPointer(name, size, minRange !== 0), destructorFunction: null });
    };
    var __embind_register_memory_view = (rawType, dataTypeIndex, name) => {
      var typeMapping = [Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, BigInt64Array, BigUint64Array];
      var TA = typeMapping[dataTypeIndex];
      function decodeMemoryView(handle) {
        var size = HEAPU32[handle >> 2];
        var data = HEAPU32[handle + 4 >> 2];
        return new TA(HEAP8.buffer, data, size);
      }
      name = readLatin1String(name);
      registerType(rawType, { name, fromWireType: decodeMemoryView, argPackAdvance: GenericWireTypeSize, readValueFromPointer: decodeMemoryView }, { ignoreDuplicateRegistrations: true });
    };
    var stringToUTF8 = (str, outPtr, maxBytesToWrite) => stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite);
    var __embind_register_std_string = (rawType, name) => {
      name = readLatin1String(name);
      registerType(rawType, { name, fromWireType(value) {
        var length = HEAPU32[value >> 2];
        var payload = value + 4;
        var str;
        var i; {
          var decodeStartPtr = payload;
          for (var i = 0; i <= length; ++i) {
            var currentBytePtr = payload + i;
            if (i == length || HEAPU8[currentBytePtr] == 0) {
              var maxRead = currentBytePtr - decodeStartPtr;
              var stringSegment = UTF8ToString(decodeStartPtr, maxRead);
              if (str === void 0) {
                str = stringSegment;
              } else {
                str += String.fromCharCode(0);
                str += stringSegment;
              }
              decodeStartPtr = currentBytePtr + 1;
            }
          }
        }
        _free(value);
        return str;
      }, toWireType(destructors, value) {
        if (value instanceof ArrayBuffer) {
          value = new Uint8Array(value);
        }
        var length;
        var valueIsOfTypeString = typeof value == "string";
        if (!(valueIsOfTypeString || ArrayBuffer.isView(value) && value.BYTES_PER_ELEMENT == 1)) {
          throwBindingError("Cannot pass non-string to std::string");
        }
        if (valueIsOfTypeString) {
          length = lengthBytesUTF8(value);
        } else {
          length = value.length;
        }
        var base = _malloc(4 + length + 1);
        var ptr = base + 4;
        HEAPU32[base >> 2] = length;
        if (valueIsOfTypeString) {
          {
            stringToUTF8(value, ptr, length + 1);
          }
        } else {
          HEAPU8.set(value, ptr);
        }
        if (destructors !== null) {
          destructors.push(_free, base);
        }
        return base;
      }, argPackAdvance: GenericWireTypeSize, readValueFromPointer: readPointer, destructorFunction(ptr) {
        _free(ptr);
      } });
    };
    var UTF16Decoder = typeof TextDecoder != "undefined" ? new TextDecoder("utf-16le") : void 0;
    var UTF16ToString = (ptr, maxBytesToRead) => {
      var idx = ptr >> 1;
      var maxIdx = idx + maxBytesToRead / 2;
      var endIdx = idx;
      while (!(endIdx >= maxIdx) && HEAPU16[endIdx])
        ++endIdx;
      if (endIdx - idx > 16 && UTF16Decoder)
        return UTF16Decoder.decode(HEAPU16.subarray(idx, endIdx));
      var str = "";
      for (var i = idx; !(i >= maxIdx); ++i) {
        var codeUnit = HEAPU16[i];
        if (codeUnit == 0)
          break;
        str += String.fromCharCode(codeUnit);
      }
      return str;
    };
    var stringToUTF16 = (str, outPtr, maxBytesToWrite) => {
      maxBytesToWrite ?? (maxBytesToWrite = 2147483647);
      if (maxBytesToWrite < 2)
        return 0;
      maxBytesToWrite -= 2;
      var startPtr = outPtr;
      var numCharsToWrite = maxBytesToWrite < str.length * 2 ? maxBytesToWrite / 2 : str.length;
      for (var i = 0; i < numCharsToWrite; ++i) {
        var codeUnit = str.charCodeAt(i);
        HEAP16[outPtr >> 1] = codeUnit;
        outPtr += 2;
      }
      HEAP16[outPtr >> 1] = 0;
      return outPtr - startPtr;
    };
    var lengthBytesUTF16 = (str) => str.length * 2;
    var UTF32ToString = (ptr, maxBytesToRead) => {
      var i = 0;
      var str = "";
      while (!(i >= maxBytesToRead / 4)) {
        var utf32 = HEAP32[ptr + i * 4 >> 2];
        if (utf32 == 0)
          break;
        ++i;
        if (utf32 >= 65536) {
          var ch = utf32 - 65536;
          str += String.fromCharCode(55296 | ch >> 10, 56320 | ch & 1023);
        } else {
          str += String.fromCharCode(utf32);
        }
      }
      return str;
    };
    var stringToUTF32 = (str, outPtr, maxBytesToWrite) => {
      maxBytesToWrite ?? (maxBytesToWrite = 2147483647);
      if (maxBytesToWrite < 4)
        return 0;
      var startPtr = outPtr;
      var endPtr = startPtr + maxBytesToWrite - 4;
      for (var i = 0; i < str.length; ++i) {
        var codeUnit = str.charCodeAt(i);
        if (codeUnit >= 55296 && codeUnit <= 57343) {
          var trailSurrogate = str.charCodeAt(++i);
          codeUnit = 65536 + ((codeUnit & 1023) << 10) | trailSurrogate & 1023;
        }
        HEAP32[outPtr >> 2] = codeUnit;
        outPtr += 4;
        if (outPtr + 4 > endPtr)
          break;
      }
      HEAP32[outPtr >> 2] = 0;
      return outPtr - startPtr;
    };
    var lengthBytesUTF32 = (str) => {
      var len = 0;
      for (var i = 0; i < str.length; ++i) {
        var codeUnit = str.charCodeAt(i);
        if (codeUnit >= 55296 && codeUnit <= 57343)
          ++i;
        len += 4;
      }
      return len;
    };
    var __embind_register_std_wstring = (rawType, charSize, name) => {
      name = readLatin1String(name);
      var decodeString, encodeString, readCharAt, lengthBytesUTF;
      if (charSize === 2) {
        decodeString = UTF16ToString;
        encodeString = stringToUTF16;
        lengthBytesUTF = lengthBytesUTF16;
        readCharAt = (pointer) => HEAPU16[pointer >> 1];
      } else if (charSize === 4) {
        decodeString = UTF32ToString;
        encodeString = stringToUTF32;
        lengthBytesUTF = lengthBytesUTF32;
        readCharAt = (pointer) => HEAPU32[pointer >> 2];
      }
      registerType(rawType, { name, fromWireType: (value) => {
        var length = HEAPU32[value >> 2];
        var str;
        var decodeStartPtr = value + 4;
        for (var i = 0; i <= length; ++i) {
          var currentBytePtr = value + 4 + i * charSize;
          if (i == length || readCharAt(currentBytePtr) == 0) {
            var maxReadBytes = currentBytePtr - decodeStartPtr;
            var stringSegment = decodeString(decodeStartPtr, maxReadBytes);
            if (str === void 0) {
              str = stringSegment;
            } else {
              str += String.fromCharCode(0);
              str += stringSegment;
            }
            decodeStartPtr = currentBytePtr + charSize;
          }
        }
        _free(value);
        return str;
      }, toWireType: (destructors, value) => {
        if (!(typeof value == "string")) {
          throwBindingError(`Cannot pass non-string to C++ string type ${name}`);
        }
        var length = lengthBytesUTF(value);
        var ptr = _malloc(4 + length + charSize);
        HEAPU32[ptr >> 2] = length / charSize;
        encodeString(value, ptr + 4, length + charSize);
        if (destructors !== null) {
          destructors.push(_free, ptr);
        }
        return ptr;
      }, argPackAdvance: GenericWireTypeSize, readValueFromPointer: readPointer, destructorFunction(ptr) {
        _free(ptr);
      } });
    };
    var __embind_register_value_object = (rawType, name, constructorSignature, rawConstructor, destructorSignature, rawDestructor) => {
      structRegistrations[rawType] = { name: readLatin1String(name), rawConstructor: embind__requireFunction(constructorSignature, rawConstructor), rawDestructor: embind__requireFunction(destructorSignature, rawDestructor), fields: [] };
    };
    var __embind_register_value_object_field = (structType, fieldName, getterReturnType, getterSignature, getter, getterContext, setterArgumentType, setterSignature, setter, setterContext) => {
      structRegistrations[structType].fields.push({ fieldName: readLatin1String(fieldName), getterReturnType, getter: embind__requireFunction(getterSignature, getter), getterContext, setterArgumentType, setter: embind__requireFunction(setterSignature, setter), setterContext });
    };
    var __embind_register_void = (rawType, name) => {
      name = readLatin1String(name);
      registerType(rawType, { isVoid: true, name, argPackAdvance: 0, fromWireType: () => void 0, toWireType: (destructors, o) => void 0 });
    };
    var requireRegisteredType = (rawType, humanName) => {
      var impl = registeredTypes[rawType];
      if (void 0 === impl) {
        throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`);
      }
      return impl;
    };
    var emval_returnValue = (returnType, destructorsRef, handle) => {
      var destructors = [];
      var result = returnType["toWireType"](destructors, handle);
      if (destructors.length) {
        HEAPU32[destructorsRef >> 2] = Emval.toHandle(destructors);
      }
      return result;
    };
    var __emval_as = (handle, returnType, destructorsRef) => {
      handle = Emval.toValue(handle);
      returnType = requireRegisteredType(returnType, "emval::as");
      return emval_returnValue(returnType, destructorsRef, handle);
    };
    var emval_methodCallers = [];
    var __emval_call = (caller, handle, destructorsRef, args) => {
      caller = emval_methodCallers[caller];
      handle = Emval.toValue(handle);
      return caller(null, handle, destructorsRef, args);
    };
    var emval_symbols = {};
    var getStringOrSymbol = (address) => {
      var symbol = emval_symbols[address];
      if (symbol === void 0) {
        return readLatin1String(address);
      }
      return symbol;
    };
    var emval_get_global = () => {
      if (typeof globalThis == "object") {
        return globalThis;
      }
      return (/* @__PURE__ */ function() {
        return Function;
      }())("return this")();
    };
    var __emval_get_global = (name) => {
      if (name === 0) {
        return Emval.toHandle(emval_get_global());
      } else {
        name = getStringOrSymbol(name);
        return Emval.toHandle(emval_get_global()[name]);
      }
    };
    var emval_addMethodCaller = (caller) => {
      var id = emval_methodCallers.length;
      emval_methodCallers.push(caller);
      return id;
    };
    var emval_lookupTypes = (argCount, argTypes) => {
      var a = new Array(argCount);
      for (var i = 0; i < argCount; ++i) {
        a[i] = requireRegisteredType(HEAPU32[argTypes + i * 4 >> 2], `parameter ${i}`);
      }
      return a;
    };
    var __emval_get_method_caller = (argCount, argTypes, kind) => {
      var types = emval_lookupTypes(argCount, argTypes);
      var retType = types.shift();
      argCount--;
      var functionBody = `return function (obj, func, destructorsRef, args) {
`;
      var offset = 0;
      var argsList = [];
      if (kind === 0) {
        argsList.push("obj");
      }
      var params = ["retType"];
      var args = [retType];
      for (var i = 0; i < argCount; ++i) {
        argsList.push(`arg${i}`);
        params.push(`argType${i}`);
        args.push(types[i]);
        functionBody += `  var arg${i} = argType${i}.readValueFromPointer(args${offset ? "+" + offset : ""});
`;
        offset += types[i].argPackAdvance;
      }
      var invoker = kind === 1 ? "new func" : "func.call";
      functionBody += `  var rv = ${invoker}(${argsList.join(", ")});
`;
      if (!retType.isVoid) {
        params.push("emval_returnValue");
        args.push(emval_returnValue);
        functionBody += "  return emval_returnValue(retType, destructorsRef, rv);\n";
      }
      functionBody += "};\n";
      var invokerFunction = new Function(...params, functionBody)(...args);
      var functionName = `methodCaller<(${types.map((t) => t.name).join(", ")}) => ${retType.name}>`;
      return emval_addMethodCaller(createNamedFunction(functionName, invokerFunction));
    };
    var __emval_incref = (handle) => {
      if (handle > 9) {
        emval_handles[handle + 1] += 1;
      }
    };
    var __emval_is_string = (handle) => {
      handle = Emval.toValue(handle);
      return typeof handle == "string";
    };
    var __emval_run_destructors = (handle) => {
      var destructors = Emval.toValue(handle);
      runDestructors(destructors);
      __emval_decref(handle);
    };
    var __emval_take_value = (type, arg) => {
      type = requireRegisteredType(type, "_emval_take_value");
      var v = type["readValueFromPointer"](arg);
      return Emval.toHandle(v);
    };
    var INT53_MAX = 9007199254740992;
    var INT53_MIN = -9007199254740992;
    var bigintToI53Checked = (num) => num < INT53_MIN || num > INT53_MAX ? NaN : Number(num);
    function __gmtime_js(time, tmPtr) {
      time = bigintToI53Checked(time);
      var date = new Date(time * 1e3);
      HEAP32[tmPtr >> 2] = date.getUTCSeconds();
      HEAP32[tmPtr + 4 >> 2] = date.getUTCMinutes();
      HEAP32[tmPtr + 8 >> 2] = date.getUTCHours();
      HEAP32[tmPtr + 12 >> 2] = date.getUTCDate();
      HEAP32[tmPtr + 16 >> 2] = date.getUTCMonth();
      HEAP32[tmPtr + 20 >> 2] = date.getUTCFullYear() - 1900;
      HEAP32[tmPtr + 24 >> 2] = date.getUTCDay();
      var start = Date.UTC(date.getUTCFullYear(), 0, 1, 0, 0, 0, 0);
      var yday = (date.getTime() - start) / (1e3 * 60 * 60 * 24) | 0;
      HEAP32[tmPtr + 28 >> 2] = yday;
    }
    var isLeapYear = (year) => year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
    var MONTH_DAYS_LEAP_CUMULATIVE = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335];
    var MONTH_DAYS_REGULAR_CUMULATIVE = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
    var ydayFromDate = (date) => {
      var leap = isLeapYear(date.getFullYear());
      var monthDaysCumulative = leap ? MONTH_DAYS_LEAP_CUMULATIVE : MONTH_DAYS_REGULAR_CUMULATIVE;
      var yday = monthDaysCumulative[date.getMonth()] + date.getDate() - 1;
      return yday;
    };
    function __localtime_js(time, tmPtr) {
      time = bigintToI53Checked(time);
      var date = new Date(time * 1e3);
      HEAP32[tmPtr >> 2] = date.getSeconds();
      HEAP32[tmPtr + 4 >> 2] = date.getMinutes();
      HEAP32[tmPtr + 8 >> 2] = date.getHours();
      HEAP32[tmPtr + 12 >> 2] = date.getDate();
      HEAP32[tmPtr + 16 >> 2] = date.getMonth();
      HEAP32[tmPtr + 20 >> 2] = date.getFullYear() - 1900;
      HEAP32[tmPtr + 24 >> 2] = date.getDay();
      var yday = ydayFromDate(date) | 0;
      HEAP32[tmPtr + 28 >> 2] = yday;
      HEAP32[tmPtr + 36 >> 2] = -(date.getTimezoneOffset() * 60);
      var start = new Date(date.getFullYear(), 0, 1);
      var summerOffset = new Date(date.getFullYear(), 6, 1).getTimezoneOffset();
      var winterOffset = start.getTimezoneOffset();
      var dst = (summerOffset != winterOffset && date.getTimezoneOffset() == Math.min(winterOffset, summerOffset)) | 0;
      HEAP32[tmPtr + 32 >> 2] = dst;
    }
    var __tzset_js = (timezone, daylight, std_name, dst_name) => {
      var currentYear = (/* @__PURE__ */ new Date()).getFullYear();
      var winter = new Date(currentYear, 0, 1);
      var summer = new Date(currentYear, 6, 1);
      var winterOffset = winter.getTimezoneOffset();
      var summerOffset = summer.getTimezoneOffset();
      var stdTimezoneOffset = Math.max(winterOffset, summerOffset);
      HEAPU32[timezone >> 2] = stdTimezoneOffset * 60;
      HEAP32[daylight >> 2] = Number(winterOffset != summerOffset);
      var extractZone = (timezoneOffset) => {
        var sign = timezoneOffset >= 0 ? "-" : "+";
        var absOffset = Math.abs(timezoneOffset);
        var hours = String(Math.floor(absOffset / 60)).padStart(2, "0");
        var minutes = String(absOffset % 60).padStart(2, "0");
        return `UTC${sign}${hours}${minutes}`;
      };
      var winterName = extractZone(winterOffset);
      var summerName = extractZone(summerOffset);
      if (summerOffset < winterOffset) {
        stringToUTF8(winterName, std_name, 17);
        stringToUTF8(summerName, dst_name, 17);
      } else {
        stringToUTF8(winterName, dst_name, 17);
        stringToUTF8(summerName, std_name, 17);
      }
    };
    var _emscripten_get_now = () => performance.now();
    var _emscripten_date_now = () => Date.now();
    var checkWasiClock = (clock_id) => clock_id >= 0 && clock_id <= 3;
    function _clock_time_get(clk_id, ignored_precision, ptime) {
      if (!checkWasiClock(clk_id)) {
        return 28;
      }
      var now;
      if (clk_id === 0) {
        now = _emscripten_date_now();
      } else {
        now = _emscripten_get_now();
      }
      var nsec = Math.round(now * 1e3 * 1e3);
      HEAP64[ptime >> 3] = BigInt(nsec);
      return 0;
    }
    var readEmAsmArgsArray = [];
    var readEmAsmArgs = (sigPtr, buf) => {
      readEmAsmArgsArray.length = 0;
      var ch;
      while (ch = HEAPU8[sigPtr++]) {
        var wide = ch != 105;
        wide &= ch != 112;
        buf += wide && buf % 8 ? 4 : 0;
        readEmAsmArgsArray.push(ch == 112 ? HEAPU32[buf >> 2] : ch == 106 ? HEAP64[buf >> 3] : ch == 105 ? HEAP32[buf >> 2] : HEAPF64[buf >> 3]);
        buf += wide ? 8 : 4;
      }
      return readEmAsmArgsArray;
    };
    var runEmAsmFunction = (code, sigPtr, argbuf) => {
      var args = readEmAsmArgs(sigPtr, argbuf);
      return ASM_CONSTS[code](...args);
    };
    var _emscripten_asm_const_int = (code, sigPtr, argbuf) => runEmAsmFunction(code, sigPtr, argbuf);
    var getHeapMax = () => 2147483648;
    var alignMemory = (size, alignment) => Math.ceil(size / alignment) * alignment;
    var growMemory = (size) => {
      var b = wasmMemory.buffer;
      var pages = (size - b.byteLength + 65535) / 65536 | 0;
      try {
        wasmMemory.grow(pages);
        updateMemoryViews();
        return 1;
      } catch (e) {
      }
    };
    var _emscripten_resize_heap = (requestedSize) => {
      var oldSize = HEAPU8.length;
      requestedSize >>>= 0;
      var maxHeapSize = getHeapMax();
      if (requestedSize > maxHeapSize) {
        return false;
      }
      for (var cutDown = 1; cutDown <= 4; cutDown *= 2) {
        var overGrownHeapSize = oldSize * (1 + 0.2 / cutDown);
        overGrownHeapSize = Math.min(overGrownHeapSize, requestedSize + 100663296);
        var newSize = Math.min(maxHeapSize, alignMemory(Math.max(requestedSize, overGrownHeapSize), 65536));
        var replacement = growMemory(newSize);
        if (replacement) {
          return true;
        }
      }
      return false;
    };
    var safeSetTimeout = (func, timeout) => setTimeout(() => {
      callUserCallback(func);
    }, timeout);
    var _emscripten_sleep = (ms) => Asyncify.handleSleep((wakeUp) => safeSetTimeout(wakeUp, ms));
    _emscripten_sleep.isAsync = true;
    var ENV = {};
    var getExecutableName = () => thisProgram || "./this.program";
    var getEnvStrings = () => {
      if (!getEnvStrings.strings) {
        var lang = (typeof navigator == "object" && navigator.languages && navigator.languages[0] || "C").replace("-", "_") + ".UTF-8";
        var env = { USER: "web_user", LOGNAME: "web_user", PATH: "/", PWD: "/", HOME: "/home/web_user", LANG: lang, _: getExecutableName() };
        for (var x in ENV) {
          if (ENV[x] === void 0)
            delete env[x];
          else
            env[x] = ENV[x];
        }
        var strings = [];
        for (var x in env) {
          strings.push(`${x}=${env[x]}`);
        }
        getEnvStrings.strings = strings;
      }
      return getEnvStrings.strings;
    };
    var _environ_get = (__environ, environ_buf) => {
      var bufSize = 0;
      var envp = 0;
      for (var string of getEnvStrings()) {
        var ptr = environ_buf + bufSize;
        HEAPU32[__environ + envp >> 2] = ptr;
        bufSize += stringToUTF8(string, ptr, Infinity) + 1;
        envp += 4;
      }
      return 0;
    };
    var _environ_sizes_get = (penviron_count, penviron_buf_size) => {
      var strings = getEnvStrings();
      HEAPU32[penviron_count >> 2] = strings.length;
      var bufSize = 0;
      for (var string of strings) {
        bufSize += lengthBytesUTF8(string) + 1;
      }
      HEAPU32[penviron_buf_size >> 2] = bufSize;
      return 0;
    };
    function _fd_close(fd) {
      try {
        var stream = SYSCALLS.getStreamFromFD(fd);
        FS.close(stream);
        return 0;
      } catch (e) {
        if (typeof FS == "undefined" || !(e.name === "ErrnoError"))
          throw e;
        return e.errno;
      }
    }
    function _fd_fdstat_get(fd, pbuf) {
      try {
        var rightsBase = 0;
        var rightsInheriting = 0;
        var flags = 0;
        {
          var stream = SYSCALLS.getStreamFromFD(fd);
          var type = stream.tty ? 2 : FS.isDir(stream.mode) ? 3 : FS.isLink(stream.mode) ? 7 : 4;
        }
        HEAP8[pbuf] = type;
        HEAP16[pbuf + 2 >> 1] = flags;
        HEAP64[pbuf + 8 >> 3] = BigInt(rightsBase);
        HEAP64[pbuf + 16 >> 3] = BigInt(rightsInheriting);
        return 0;
      } catch (e) {
        if (typeof FS == "undefined" || !(e.name === "ErrnoError"))
          throw e;
        return e.errno;
      }
    }
    var doReadv = (stream, iov, iovcnt, offset) => {
      var ret = 0;
      for (var i = 0; i < iovcnt; i++) {
        var ptr = HEAPU32[iov >> 2];
        var len = HEAPU32[iov + 4 >> 2];
        iov += 8;
        var curr = FS.read(stream, HEAP8, ptr, len, offset);
        if (curr < 0)
          return -1;
        ret += curr;
        if (curr < len)
          break;
      }
      return ret;
    };
    function _fd_read(fd, iov, iovcnt, pnum) {
      try {
        var stream = SYSCALLS.getStreamFromFD(fd);
        var num = doReadv(stream, iov, iovcnt);
        HEAPU32[pnum >> 2] = num;
        return 0;
      } catch (e) {
        if (typeof FS == "undefined" || !(e.name === "ErrnoError"))
          throw e;
        return e.errno;
      }
    }
    function _fd_seek(fd, offset, whence, newOffset) {
      offset = bigintToI53Checked(offset);
      try {
        if (isNaN(offset))
          return 61;
        var stream = SYSCALLS.getStreamFromFD(fd);
        FS.llseek(stream, offset, whence);
        HEAP64[newOffset >> 3] = BigInt(stream.position);
        if (stream.getdents && offset === 0 && whence === 0)
          stream.getdents = null;
        return 0;
      } catch (e) {
        if (typeof FS == "undefined" || !(e.name === "ErrnoError"))
          throw e;
        return e.errno;
      }
    }
    var doWritev = (stream, iov, iovcnt, offset) => {
      var ret = 0;
      for (var i = 0; i < iovcnt; i++) {
        var ptr = HEAPU32[iov >> 2];
        var len = HEAPU32[iov + 4 >> 2];
        iov += 8;
        var curr = FS.write(stream, HEAP8, ptr, len, offset);
        if (curr < 0)
          return -1;
        ret += curr;
        if (curr < len) {
          break;
        }
      }
      return ret;
    };
    function _fd_write(fd, iov, iovcnt, pnum) {
      try {
        var stream = SYSCALLS.getStreamFromFD(fd);
        var num = doWritev(stream, iov, iovcnt);
        HEAPU32[pnum >> 2] = num;
        return 0;
      } catch (e) {
        if (typeof FS == "undefined" || !(e.name === "ErrnoError"))
          throw e;
        return e.errno;
      }
    }
    FS.createPreloadedFile = FS_createPreloadedFile;
    FS.staticInit();
    MEMFS.doesNotExistError = new FS.ErrnoError(44);
    MEMFS.doesNotExistError.stack = "<generic error, no stack>";
    embind_init_charCodes();
    {
      if (Module["noExitRuntime"])
        noExitRuntime = Module["noExitRuntime"];
      if (Module["preloadPlugins"])
        preloadPlugins = Module["preloadPlugins"];
      if (Module["print"])
        out = Module["print"];
      if (Module["printErr"])
        err = Module["printErr"];
      if (Module["wasmBinary"])
        wasmBinary = Module["wasmBinary"];
      if (Module["arguments"])
        arguments_ = Module["arguments"];
      if (Module["thisProgram"])
        thisProgram = Module["thisProgram"];
    }
    var ASM_CONSTS = { 1473352: () => {
      console.error("progress callback is not set.");
    } };
    var wasmImports = { a: ___cxa_throw, j: ___syscall_fcntl64, G: ___syscall_ioctl, H: ___syscall_openat, r: ___syscall_renameat, s: ___syscall_rmdir, w: ___syscall_stat64, t: ___syscall_unlinkat, u: __abort_js, L: __embind_finalize_value_object, m: __embind_register_bigint, R: __embind_register_bool, P: __embind_register_emval, l: __embind_register_float, f: __embind_register_function, c: __embind_register_integer, b: __embind_register_memory_view, Q: __embind_register_std_string, g: __embind_register_std_wstring, O: __embind_register_value_object, M: __embind_register_value_object_field, S: __embind_register_void, N: __emval_as, d: __emval_call, q: __emval_decref, F: __emval_get_global, e: __emval_get_method_caller, o: __emval_incref, T: __emval_is_string, n: __emval_run_destructors, p: __emval_take_value, x: __gmtime_js, y: __localtime_js, z: __tzset_js, J: _clock_time_get, K: _emscripten_asm_const_int, I: _emscripten_date_now, v: _emscripten_resize_heap, k: _emscripten_sleep, C: _environ_get, D: _environ_sizes_get, h: _fd_close, B: _fd_fdstat_get, E: _fd_read, A: _fd_seek, i: _fd_write };
    var wasmExports = await createWasm();
    wasmExports["V"];
    var ___getTypeName = wasmExports["W"];
    var _malloc = wasmExports["X"];
    var _free = wasmExports["Y"];
    Module["_main"] = wasmExports["Z"];
    wasmExports["_"];
    Module["dynCall_ii"] = wasmExports["$"];
    Module["dynCall_vi"] = wasmExports["aa"];
    Module["dynCall_vii"] = wasmExports["ba"];
    Module["dynCall_iii"] = wasmExports["ca"];
    Module["dynCall_viii"] = wasmExports["da"];
    Module["dynCall_v"] = wasmExports["ea"];
    Module["dynCall_diiii"] = wasmExports["fa"];
    Module["dynCall_viif"] = wasmExports["ga"];
    Module["dynCall_iiiii"] = wasmExports["ha"];
    Module["dynCall_iiii"] = wasmExports["ia"];
    Module["dynCall_ddd"] = wasmExports["ja"];
    Module["dynCall_i"] = wasmExports["ka"];
    Module["dynCall_fii"] = wasmExports["la"];
    Module["dynCall_viiii"] = wasmExports["ma"];
    Module["dynCall_iiiiii"] = wasmExports["na"];
    Module["dynCall_idd"] = wasmExports["oa"];
    Module["dynCall_jiji"] = wasmExports["pa"];
    Module["dynCall_iidiiii"] = wasmExports["qa"];
    Module["dynCall_viijii"] = wasmExports["ra"];
    Module["dynCall_iiiiiiiii"] = wasmExports["sa"];
    Module["dynCall_iiiiiii"] = wasmExports["ta"];
    Module["dynCall_iiiiij"] = wasmExports["ua"];
    Module["dynCall_iiiiid"] = wasmExports["va"];
    Module["dynCall_iiiiijj"] = wasmExports["wa"];
    Module["dynCall_iiiiiiii"] = wasmExports["xa"];
    Module["dynCall_iiiiiijj"] = wasmExports["ya"];
    Module["dynCall_viiiiii"] = wasmExports["za"];
    Module["dynCall_viiiii"] = wasmExports["Aa"];
    var _asyncify_start_unwind = wasmExports["Ba"];
    var _asyncify_stop_unwind = wasmExports["Ca"];
    var _asyncify_start_rewind = wasmExports["Da"];
    var _asyncify_stop_rewind = wasmExports["Ea"];
    function run(args = arguments_) {
      if (runDependencies > 0) {
        dependenciesFulfilled = run;
        return;
      }
      preRun();
      if (runDependencies > 0) {
        dependenciesFulfilled = run;
        return;
      }
      function doRun() {
        Module["calledRun"] = true;
        if (ABORT)
          return;
        initRuntime();
        readyPromiseResolve(Module);
        Module["onRuntimeInitialized"]?.();
        Module["noInitialRun"] || true;
        postRun();
      }
      if (Module["setStatus"]) {
        Module["setStatus"]("Running...");
        setTimeout(() => {
          setTimeout(() => Module["setStatus"](""), 1);
          doRun();
        }, 1);
      } else {
        doRun();
      }
    }
    function preInit() {
      if (Module["preInit"]) {
        if (typeof Module["preInit"] == "function")
          Module["preInit"] = [Module["preInit"]];
        while (Module["preInit"].length > 0) {
          Module["preInit"].shift()();
        }
      }
    }
    preInit();
    run();
    moduleRtn = readyPromise;
    return moduleRtn;
  };
})();

var __defProp$1 = Object.defineProperty;
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$1 = (obj, key, value) => {
  __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
  return value;
};
const cdnUrl = `https://cdn.jsdelivr.net/npm/@creooxag/cxconverter@${packageJSON.version}/dist/`;
const defaultUrlPrefix = "./";
const _IfcWasmConverterLoader = class _IfcWasmConverterLoader {
  constructor() {
    __publicField$1(this, "module");
  }
  static async getInstance(userUrlPrefix = "remote") {
    let urlPrefix = defaultUrlPrefix;
    if (userUrlPrefix) {
      urlPrefix = userUrlPrefix == "remote" ? cdnUrl : userUrlPrefix;
    }
    if (!_IfcWasmConverterLoader.instance) {
      _IfcWasmConverterLoader.instance = new _IfcWasmConverterLoader();
      const Module = {
        locateFile: function(path) {
          const fullPath = urlPrefix + path;
          if (path.endsWith(".wasm")) {
            console.log(`Loading wasm file from ${fullPath}`);
            return fullPath;
          }
          return path;
        }
      };
      try {
        _IfcWasmConverterLoader.instance.module = await IfcDbWasm(Module);
      } catch (error) {
        console.error("Error on wasm import:", error);
      }
    }
    return _IfcWasmConverterLoader.instance;
  }
  static async getModule(userUrlPrefix) {
    return (await _IfcWasmConverterLoader.getInstance(userUrlPrefix)).module;
  }
};
__publicField$1(_IfcWasmConverterLoader, "instance");

math.vec2();
math.vec3();
math.vec3();
math.vec3();

/**
 * Default server client which loads content for a {@link BIMViewer} via HTTP from the file system.
 *
 * A BIMViewer is instantiated with an instance of this class.
 *
 * To load content from an alternative source, instantiate BIMViewer with your own custom implementation of this class.
 */
class Server {

    /**
     * Constructs a Server.
     *
     * @param {*} [cfg] Server configuration.
     * @param {String} [cfg.dataDir] Base directory for content.
     */
    constructor(cfg = {}) {
        this._dataDir = cfg.dataDir || "";
    }

    /**
     * Gets information on all available projects.
     *
     * @param {Function} done Callback through which the JSON result is returned.
     * @param {Function} error Callback through which an error message is returned on error.
     */
    getProjects(done, error) {
        const url = this._dataDir + "/projects/index.json";
        utils.loadJSON(url, done, error);
    }

    /**
     * Gets information for a project.
     *
     * @param {String} projectId ID of the project.
     * @param {Function} done Callback through which the JSON result is returned.
     * @param {Function} error Callback through which an error message is returned on error.
     */
    getProject(projectId, done, error) {
        const url = this._dataDir + "/projects/" + projectId + "/index.json";
        utils.loadJSON(url, done, error);
    }

    /**
     * Gets metadata for a model within a project.
     *
     * @param {String} projectId ID of the project.
     * @param {String} modelId ID of the model.
     * @param {Function} done Callback through which the JSON result is returned.
     * @param {Function} error Callback through which an error message is returned on error.
     */
    getMetadata(projectId, modelId, done, error) {
        const url = this._dataDir + "/projects/" + projectId + "/models/" + modelId + "/metadata.json";
        utils.loadJSON(url, done, error);
    }

    /**
     * Gets geometry for a model within a project.
     *
     * @param {String} projectId ID of the project.
     * @param {String} modelId ID of the model.
     * @param {Function} done Callback through which the JSON result is returned.
     * @param {Function} error Callback through which an error message is returned on error.
     */
    getGeometry(projectId, modelId, done, error) {
        const url = this._dataDir + "/projects/" + projectId + "/models/" + modelId + "/geometry.xkt";
        utils.loadArraybuffer(url, done, error);
    }

    /**
     * Gets metadata for an object within a model within a project.
     *
     * @param {String} projectId ID of the project.
     * @param {String} modelId ID of the model.
     * @param {String} objectId ID of the object.
     * @param {Function} done Callback through which the JSON result is returned.
     * @param {Function} error Callback through which an error message is returned on error.
     */
    getObjectInfo(projectId, modelId, objectId, done, error) {
        const url = this._dataDir + "/projects/" + projectId + "/models/" + modelId + "/props/" + objectId + ".json";
        utils.loadJSON(url, done, error);
    }

    /**
     * Gets existing issues for a model within a project.
     *
     * @param {String} projectId ID of the project.
     * @param {String} modelId ID of the model.
     * @param {Function} done Callback through which the JSON result is returned.
     * @param {Function} error Callback through which an error message is returned on error.
     */
    getIssues(projectId, modelId, done, error) {
        const url = this._dataDir + "/projects/" + projectId + "/models/" + modelId + "/issues.json";
        utils.loadJSON(url, done, error);
    }


    /**
     * Gets a JSON manifest file for a model that's split into multiple XKT files (and maybe also JSON metadata files).
     *
     * The manifest can have an arbitrary name, and will list all the XKT (and maybe separate JSON metada files)
     * that comprise the model.
     *
     * @param {String} projectId ID of the project.
     * @param {String} modelId ID of the model.
     * @param {String} manifestName Filename of the manifest.
     * @param {Function} done Callback through which the JSON result is returned.
     * @param {Function} error Callback through which an error message is returned on error.
     */
    getSplitModelManifest(projectId, modelId, manifestName, done, error) {
        const url = this._dataDir + "/projects/" + projectId + "/models/" + modelId + "/" + manifestName;
        utils.loadJSON(url, done, error);
    }

    /**
     * Gets one of the metadata files within a split model within a project.
     *
     * @param {String} projectId ID of the project.
     * @param {String} modelId ID of the model.
     * @param {String} metadataFileName Filename of the metadata file.
     * @param {Function} done Callback through which the JSON result is returned.
     * @param {Function} error Callback through which an error message is returned on error.
     */
    getSplitModelMetadata(projectId, modelId, metadataFileName, done, error) {
        const url = this._dataDir + "/projects/" + projectId + "/models/" + modelId + "/" + metadataFileName;
        utils.loadJSON(url, done, error);
    }

    /**
     * Gets one of the XKT geometry files within a split model within a project.
     *
     * @param {String} projectId ID of the project.
     * @param {String} modelId ID of the model.
     *  @param {String} geometryFileName Filename of the XKT geometry file.
     * @param {Function} done Callback through which the JSON result is returned.
     * @param {Function} error Callback through which an error message is returned on error.
     */
    getSplitModelGeometry(projectId, modelId, geometryFileName, done, error) {
        const url = this._dataDir + "/projects/" + projectId + "/models/" + modelId + "/" + geometryFileName;
        utils.loadArraybuffer(url, done, error);
    }
}

/** @private */
let Map$1 = class Map {

    constructor(items, baseId) {
        this.items = items || [];
        this._lastUniqueId = (baseId || 0) + 1;
    }

    /**
     * Usage:
     *
     * id = myMap.addItem("foo") // ID internally generated
     * id = myMap.addItem("foo", "bar") // ID is "foo"
     */
    addItem() {
        let item;
        if (arguments.length === 2) {
            const id = arguments[0];
            item = arguments[1];
            if (this.items[id]) { // Won't happen if given ID is string
                throw "ID clash: '" + id + "'";
            }
            this.items[id] = item;
            return id;

        } else {
            item = arguments[0] || {};
            while (true) {
                const findId = this._lastUniqueId++;
                if (!this.items[findId]) {
                    this.items[findId] = item;
                    return findId;
                }
            }
        }
    }

    removeItem(id) {
        const item = this.items[id];
        delete this.items[id];
        return item;
    }
};

/** @private */
class Controller {

    /**
     * @protected
     */
    constructor(parent, cfg, server, viewer) {

        this.bimViewer = (parent ? (parent.bimViewer || parent) : this);
        this.server = parent ? parent.server : server;
        this.viewer = parent ? parent.viewer : viewer;

        this._children = [];

        if (parent) {
            parent._children.push(this);
        }

        this._subIdMap = null; // Subscription subId pool
        this._subIdEvents = null; // Subscription subIds mapped to event names
        this._eventSubs = null; // Event names mapped to subscribers
        this._events = null; // Maps names to events
        this._eventCallDepth = 0; // Helps us catch stack overflows from recursive events

        this._enabled = null; // Used by #setEnabled() and #getEnabled()
        this._active = null; // Used by #setActive() and #getActive()
    }

    /**
     * Fires an event on this Controller.
     *
     * @protected
     *
     * @param {String} event The event type name
     * @param {Object} value The event parameters
     * @param {Boolean} [forget=false] When true, does not retain for subsequent subscribers
     */
    fire(event, value, forget) {
        if (!this._events) {
            this._events = {};
        }
        if (!this._eventSubs) {
            this._eventSubs = {};
        }
        if (forget !== true) {
            this._events[event] = value || true; // Save notification
        }
        const subs = this._eventSubs[event];
        let sub;
        if (subs) { // Notify subscriptions
            for (const subId in subs) {
                if (subs.hasOwnProperty(subId)) {
                    sub = subs[subId];
                    this._eventCallDepth++;
                    if (this._eventCallDepth < 300) {
                        sub.callback.call(sub.scope, value);
                    } else {
                        this.error("fire: potential stack overflow from recursive event '" + event + "' - dropping this event");
                    }
                    this._eventCallDepth--;
                }
            }
        }
    }

    /**
     * Subscribes to an event on this Controller.
     *
     * The callback is be called with this component as scope.
     *
     * @param {String} event The event
     * @param {Function} callback Called fired on the event
     * @param {Object} [scope=this] Scope for the callback
     * @return {String} Handle to the subscription, which may be used to unsubscribe with {@link #off}.
     */
    on(event, callback, scope) {
        if (!this._events) {
            this._events = {};
        }
        if (!this._subIdMap) {
            this._subIdMap = new Map$1(); // Subscription subId pool
        }
        if (!this._subIdEvents) {
            this._subIdEvents = {};
        }
        if (!this._eventSubs) {
            this._eventSubs = {};
        }
        let subs = this._eventSubs[event];
        if (!subs) {
            subs = {};
            this._eventSubs[event] = subs;
        }
        const subId = this._subIdMap.addItem(); // Create unique subId
        subs[subId] = {
            callback: callback,
            scope: scope || this
        };
        this._subIdEvents[subId] = event;
        const value = this._events[event];
        if (value !== undefined) { // A publication exists, notify callback immediately
            callback.call(scope || this, value);
        }
        return subId;
    }

    /**
     * Cancels an event subscription that was previously made with {@link Controller#on} or {@link Controller#once}.
     *
     * @param {String} subId Subscription ID
     */
    off(subId) {
        if (subId === undefined || subId === null) {
            return;
        }
        if (!this._subIdEvents) {
            return;
        }
        const event = this._subIdEvents[subId];
        if (event) {
            delete this._subIdEvents[subId];
            const subs = this._eventSubs[event];
            if (subs) {
                delete subs[subId];
            }
            this._subIdMap.removeItem(subId); // Release subId
        }
    }

    /**
     * Subscribes to the next occurrence of the given event, then un-subscribes as soon as the event is handled.
     *
     * This is equivalent to calling {@link Controller#on}, and then calling {@link Controller#off} inside the callback function.
     *
     * @param {String} event Data event to listen to
     * @param {Function} callback Called when fresh data is available at the event
     * @param {Object} [scope=this] Scope for the callback
     */
    once(event, callback, scope) {
        const self = this;
        const subId = this.on(event,
            function (value) {
                self.off(subId);
                callback.call(scope || this, value);
            },
            scope);
    }

    /**
     * Logs a console debugging message for this Controller.
     *
     * The console message will have this format: *````[LOG] [<component type> <component id>: <message>````*
     *
     * @protected
     *
     * @param {String} message The message to log
     */
    log(message) {
        message = "[LOG] " + message;
        window.console.log(message);
    }

    /**
     * Logs a warning for this Controller to the JavaScript console.
     *
     * The console message will have this format: *````[WARN] [<component type> =<component id>: <message>````*
     *
     * @protected
     *
     * @param {String} message The message to log
     */
    warn(message) {
        message = "[WARN] " + message;
        window.console.warn(message);
    }

    /**
     * Logs an error for this Controller to the JavaScript console.
     *
     * The console message will have this format: *````[ERROR] [<component type> =<component id>: <message>````*
     *
     * @protected
     *
     * @param {String} message The message to log
     */
    error(message) {
        message = "[ERROR] " + message;
        window.console.error(message);
    }

    _mutexActivation(controllers) {
        const numControllers = controllers.length;
        for (let i = 0; i < numControllers; i++) {
            const controller = controllers[i];
            if (controller) {
                controller.on("active", (function () {
                    const _i = i;
                    return function (active) {
                        if (!active) {
                            return;
                        }
                        for (let j = 0; j < numControllers; j++) {
                            if (j === _i) {
                                continue;
                            }
                            controllers[j].setActive(false);
                        }
                    };
                })());
            }
        }
    }

    /**
     * Enables or disables this Controller.
     *
     * Fires an "enabled" event on update.
     *
     * @protected
     * @param {boolean} enabled Whether or not to enable.
     */
    setEnabled(enabled) {
        if (this._enabled === enabled) {
            return;
        }
        this._enabled = enabled;
        this.fire("enabled", this._enabled);
    }

    /**
     * Gets whether or not this Controller is enabled.
     *
     * @protected
     *
     * @returns {boolean}
     */
    getEnabled() {
        return this._enabled;
    }

    /**
     * Activates or deactivates this Controller.
     *
     * Fires an "active" event on update.
     *
     * @protected
     *
     * @param {boolean} active Whether or not to activate.
     */
    setActive(active) {
        if (this._active === active) {
            return;
        }
        this._active = active;
        this.fire("active", this._active);
    }

    /**
     * Gets whether or not this Controller is active.
     *
     * @protected
     *
     * @returns {boolean}
     */
    getActive() {
        return this._active;
    }

    /**
     * Destroys this Controller.
     *
     * @protected
     *
     */
    destroy() {
        if (this.destroyed) {
            return;
        }
        /**
         * Fired when this Controller is destroyed.
         * @event destroyed
         */
        this.fire("destroyed", this.destroyed = true);
        this._subIdMap = null;
        this._subIdEvents = null;
        this._eventSubs = null;
        this._events = null;
        this._eventCallDepth = 0;
        for (let i = 0, len = this._children.length; i < len; i++) {
            this._children[i].destroy();
        }
        this._children = [];
    }
}

/** @private */
class BusyModal extends Controller {

    constructor(parent, cfg = {}, rootDOMNode = document) {

        super(parent, cfg);

        const busyModalBackdropElement = cfg.busyModalBackdropElement || rootDOMNode;

        if (!busyModalBackdropElement) {
            throw "Missing config: busyModalBackdropElement";
        }

        this._modal = document.createElement("div");
        this._modal.classList.add("xeokit-busy-modal");
        this._modal.innerHTML = '<div class="xeokit-busy-modal-content"><div class="xeokit-busy-modal-body"><div class="xeokit-busy-modal-message">Default text</div></div></div>';

        busyModalBackdropElement.appendChild(this._modal);

        this._modalVisible = false;
        this._modal.style.display = 'hidden';
    }

    show(message) {
        this._modalVisible = true;
        this._modal.querySelector('.xeokit-busy-modal-message').innerText = message;
        this._modal.style.display = 'block';
    }

    hide() {
        this._modalVisible = false;
        this._modal.style.display = 'none';
    }

    destroy() {
        super.destroy();
        if (this._modal) {
            this._modal.parentNode.removeChild(this._modal);
            this._modal = null;
        }
    }
}

const tempVec3a$2 = math.vec3();

/** @private */
class ResetAction extends Controller {

    constructor(parent, cfg = {}) {

        super(parent, cfg);

        if (!cfg.buttonElement) {
            throw "Missing config: buttonElement";
        }

        const buttonElement = cfg.buttonElement;
        const camera = this.viewer.camera;

        this._modelMementos = {};

        // Initial camera position - looking down negative diagonal

        camera.eye = [0.577, 0.577, 0.577];
        camera.look = [0,0,0];
        camera.up = [-1, 1, -1];

        this.bimViewer._modelsExplorer.on("modelLoaded", (modelId) => {
            this._saveModelMemento(modelId);
        });

        this.bimViewer._modelsExplorer.on("modelUnloaded", (modelId) => {
            this._destroyModelMemento(modelId);
        });

        this.on("enabled", (enabled) => {
            if (!enabled) {
                buttonElement.classList.add("disabled");
            } else {
                buttonElement.classList.remove("disabled");
            }
        });

        this.on("active", (active) => {
            if (active) {
                buttonElement.classList.add("active");
            } else {
                buttonElement.classList.remove("active");
            }
        });

        buttonElement.addEventListener("click", (event) => {
            if (this.getEnabled()) {
                this.reset();
            }
            event.preventDefault();
        });
    }

    _saveModelMemento(modelId) {
        const metaModel = this.viewer.metaScene.metaModels[modelId];
        if (!metaModel) {
            return;
        }
        const modelMemento = new ModelMemento();
        modelMemento.saveObjects(this.viewer.scene, metaModel, {
            visible: true,
            edges: true,
            xrayed: true,
            highlighted: true,
            selected: true,
            clippable: true,
            pickable: true,
            colorize: false, // We don't colorize objects yet - also messes up point clouds
            opacity: false // FIXME: Restoring opacity broken by colorize fix - details at https://github.com/xeokit/xeokit-sdk/issues/239
        });
        this._modelMementos[modelId] = modelMemento;
    }

    _restoreModelMemento(modelId) {
        const metaModel = this.viewer.metaScene.metaModels[modelId];
        if (!metaModel) {
            return;
        }
        const modelMemento = this._modelMementos[modelId];
        modelMemento.restoreObjects(this.viewer.scene, metaModel);
    }

    _destroyModelMemento(modelId) {
        delete this._modelMementos[modelId];
    }

    reset() {
        const scene = this.viewer.scene;
        const modelIds = scene.modelIds;
        for (var i = 0, len = modelIds.length; i < len; i++) {
            const modelId = modelIds[i];
            this._restoreModelMemento(modelId);
        }
        this.bimViewer.unShowObjectInExplorers();
        this.fire("reset", true);
        this._resetCamera();
    }

    _resetCamera() {
        const viewer = this.viewer;
        const scene = viewer.scene;
        const aabb = scene.getAABB(scene.visibleObjectIds);
        const diag = math.getAABB3Diag(aabb);
        const center = math.getAABB3Center(aabb, tempVec3a$2);
        const camera = scene.camera;
        camera.perspective.fov;
        const dist = Math.abs(diag / Math.tan(45 * math.DEGTORAD));
        const dir = math.normalizeVec3((camera.yUp) ? [-0.5, -0.7071, -0.5] : [-1, 1, -1]);
        const up = math.normalizeVec3((camera.yUp) ? [-0.5, 0.7071, -0.5] : [-1, 1, 1]);
        viewer.cameraControl.pivotPos = center;
        viewer.cameraControl.planView = false;
        viewer.cameraFlight.flyTo({
            look: center,
            eye: [center[0] - (dist * dir[0]), center[1] - (dist * dir[1]), center[2] - (dist * dir[2])],
            up: up,
            orthoScale: diag * 1.3,
            projection: "perspective",
            duration: 1
        });
    }
}

const tempVec3$4 = math.vec3();

/** @private */
class FitAction extends Controller {

    constructor(parent, cfg={}) {

        super(parent, cfg);

        if (!cfg.buttonElement) {
            throw "Missing config: buttonElement";
        }

        const buttonElement = cfg.buttonElement;

        this.on("enabled", (enabled) => {
            if (!enabled) {
                buttonElement.classList.add("disabled");
            } else {
                buttonElement.classList.remove("disabled");
            }
        });

        this.on("active", (active) => {
            if (active) {
                buttonElement.classList.add("active");
            } else {
                buttonElement.classList.remove("active");
            }
        });

        buttonElement.addEventListener("click", (event) => {
            if (this.getEnabled()) {
                this.fit();
            }
            event.preventDefault();
        });
    }

    fit() {
        const scene = this.viewer.scene;
        const aabb = scene.getAABB(scene.visibleObjectIds);
        this.viewer.cameraFlight.flyTo({
            aabb: aabb
        });
        this.viewer.cameraControl.pivotPos = math.getAABB3Center(aabb, tempVec3$4);
    }

    set fov(fov) {
        this.viewer.scene.cameraFlight.fitFOV = fov;
    }

    get fov() {
        return this.viewer.scene.cameraFlight.fitFOV;
    }

    set duration(duration) {
        this.viewer.scene.cameraFlight.duration = duration;
    }

    get duration() {
        return this.viewer.scene.cameraFlight.duration;
    }
}

/** @private */
class FirstPersonMode extends Controller {

    constructor(parent, cfg) {

        super(parent, cfg);

        if (!cfg.buttonElement) {
            throw "Missing config: buttonElement";
        }

        const buttonElement = cfg.buttonElement;
        const cameraControl = this.viewer.cameraControl;
        const cameraControlNavModeMediator = cfg.cameraControlNavModeMediator;

        cameraControl.navMode = "orbit";
        cameraControl.followPointer = true;

        this.on("enabled", (enabled) => {
            if (!enabled) {
                buttonElement.classList.add("disabled");
            } else {
                buttonElement.classList.remove("disabled");
            }
        });

        this.on("active", (active) => {
            if (active) {
                buttonElement.classList.add("active");
            } else {
                buttonElement.classList.remove("active");
            }
        });

        this.on("active", (active) => {
            cameraControlNavModeMediator.setFirstPersonModeActive(active);
            if (active) {
                cameraControl.followPointer = true;
                cameraControl.pivoting = false;
            } else {
                cameraControl.pivoting = true;
            }
        });

        buttonElement.addEventListener("click", (event) => {
            if (this.getEnabled()) {
                const active = this.getActive();
                this.setActive(!active);
            }
            event.preventDefault();
        });

        this.bimViewer.on("reset", ()=>{
            this.setActive(false);
        });
    }
}

/** @private */
class HideTool extends Controller {

    constructor(parent, cfg) {

        super(parent, cfg);

        if (!cfg.buttonElement) {
            throw "Missing config: buttonElement";
        }

        const buttonElement = cfg.buttonElement;

        this.on("enabled", (enabled) => {
            if (!enabled) {
                buttonElement.classList.add("disabled");
            } else {
                buttonElement.classList.remove("disabled");
            }
        });

        this.on("active", (active) => {
            if (active) {
                buttonElement.classList.add("active");
                this.viewer.cameraControl.doublePickFlyTo = false;
                this._onPick = this.viewer.cameraControl.on("picked", (pickResult) => {
                    if (!pickResult.entity) {
                        return;
                    }
                    pickResult.entity.visible = false;
                });
            } else {
                buttonElement.classList.remove("active");
                this.viewer.cameraControl.doublePickFlyTo = false;
                if (this._onPick !== undefined) {
                    this.viewer.cameraControl.off(this._onPick);
                    this._onPick = undefined;
                }
            }
        });

        buttonElement.addEventListener("click", (event) => {
            if (this.getEnabled()) {
                this.bimViewer._sectionTool.hideControl();
                const active = this.getActive();
                this.setActive(!active);
            }
            event.preventDefault();
        });

        this.bimViewer.on("reset", () => {
            this.setActive(false);
        });
    }
}

/** @private */
class SelectionTool extends Controller {

    constructor(parent, cfg) {

        super(parent);

        if (!cfg.buttonElement) {
            throw "Missing config: buttonElement";
        }

        const buttonElement = cfg.buttonElement;

        this.on("enabled", (enabled) => {
            if (!enabled) {
                buttonElement.classList.add("disabled");
            } else {
                buttonElement.classList.remove("disabled");
            }
        });

        this.on("active", (active) => {
            if (active) {
                buttonElement.classList.add("active");
                this._onPick = this.viewer.cameraControl.on("picked", (pickResult) => {
                    if (!pickResult.entity) {
                        return;
                    }
                  pickResult.entity.selected = !pickResult.entity.selected;
                });
            } else {
                buttonElement.classList.remove("active");
                if (this._onPick !== undefined) {
                    this.viewer.cameraControl.off(this._onPick);
                    this._onPick = undefined;
                }
            }
        });

        buttonElement.addEventListener("click", (event) => {
            if (this.getEnabled()) {
                this.bimViewer._sectionTool.hideControl();
                const active = this.getActive();
                this.setActive(!active);
            }
            event.preventDefault();
        });

        this.bimViewer.on("reset", () => {
            this.setActive(false);
        });
    }
}

/** @private */
class ShowSpacesMode extends Controller {

    constructor(parent, cfg) {

        super(parent, cfg);

        if (!cfg.buttonElement) {
            throw "Missing config: buttonElement";
        }

        this._buttonElement = cfg.buttonElement;

        this.on("enabled", (enabled) => {
            if (!enabled) {
                this._buttonElement.classList.add("disabled");
            } else {
                this._buttonElement.classList.remove("disabled");
            }
        });

        this._buttonElement.addEventListener("click", (event) => {
            if (this.getEnabled()) {
                this.setActive(!this.getActive(), () => {
                });
            }
            event.preventDefault();
        });

        this.bimViewer.on("reset", () => {
            this.setActive(false); // IfcSpaces hidden by default
        });

        this.viewer.scene.on("modelLoaded", (modelId) => {
            if (!this._active) {
                const objectIds = this.viewer.metaScene.getObjectIDsByType("IfcSpace");
                this.viewer.scene.setObjectsCulled(objectIds, true);
            }
        });
        
        this._active = false;
        this._buttonElement.classList.remove("active");
    }

    setActive(active) {
        if (this._active === active) {
            return;
        }
        this._active = active;
        if (active) {
            this._buttonElement.classList.add("active");
            this._enterShowSpacesMode();
            this.fire("active", this._active);
        } else {
            this._buttonElement.classList.remove("active");
            this._exitShowSpacesMode();
            this.fire("active", this._active);
        }
    }

    _enterShowSpacesMode() {
        const viewer = this.viewer;
        const scene = viewer.scene;
        const metaScene = viewer.metaScene;
        const objectIds = metaScene.getObjectIDsByType("IfcSpace");
        scene.setObjectsCulled(objectIds, false);
    }

    _exitShowSpacesMode() {
        const viewer = this.viewer;
        const scene = viewer.scene;
        const metaScene = viewer.metaScene;
        const objectIds = metaScene.getObjectIDsByType("IfcSpace");
        scene.setObjectsCulled(objectIds, true);
    }
}

/** @private */
class QueryTool extends Controller {

    constructor(parent, cfg) {

        super(parent);

       //  this.on("active", (active) => {
       //      if (active) {
       //          this._onPick = this.viewer.cameraControl.on("picked", (pickResult) => {
       //              if (!pickResult.entity) {
       //                  return;
       //              }
       // //             this.queryEntity(pickResult.entity);
       //          });
       //          this._onPickedNothing = this.viewer.cameraControl.on("pickedNothing", () => {
       //              this.fire("queryNotPicked", false);
       //          });
       //      } else {
       //
       //          if (this._onPick !== undefined) {
       //              this.viewer.cameraControl.off(this._onPick);
       //              this.viewer.cameraControl.off(this._onPickedNothing);
       //              this._onPick = undefined;
       //              this._onPickedNothing = undefined;
       //          }
       //      }
       //  });
       //
       //  this.bimViewer.on("reset", () => {
       //      this.setActive(false);
       //  });
    }
}

const tempAABB = math.AABB3();
const tempVec3$3 = math.vec3();

/**
 * @private
 */
class SectionToolContextMenu extends ContextMenu {

    constructor(cfg = {}) {

        if (!cfg.sectionPlanesPlugin) {
            throw "Missing config: sectionPlanesPlugin";
        }

        super(utils.apply({}, cfg));

        this._sectionPlanesPlugin = cfg.sectionPlanesPlugin;
        this._viewer = this._sectionPlanesPlugin.viewer;

        this._onSceneSectionPlaneCreated = this._viewer.scene.on("sectionPlaneCreated", () => {
            this._buildMenu();
        });

        this._onSceneSectionPlaneDestroyed = this._viewer.scene.on("sectionPlaneDestroyed", () => {
            this._buildMenu();
        });

        this._buildMenu();
    }

    _buildMenu() {

        const sectionPlanesPlugin = this._sectionPlanesPlugin;
        const sectionPlanes = Object.values(sectionPlanesPlugin.sectionPlanes);

        const sectionPlanesMenuItems = [];

        for (let i = 0, len = sectionPlanes.length; i < len; i++) {

            const sectionPlane = sectionPlanes[i];

            sectionPlanesMenuItems.push({

                getTitle: (context) => {
                    return `${context.viewer.localeService.translate("sectionToolContextMenu.slice") || "Slice"} #` + (i + 1);
                },

                doHoverEnter(context) {
                    sectionPlanesPlugin.hideControl();
                    sectionPlanesPlugin.showControl(sectionPlane.id);
                },

                doHoverLeave(context) {
                    sectionPlanesPlugin.hideControl();
                },

                items: [ // Submenu
                    [ // Group
                        {
                            getTitle: (context) => {
                                return sectionPlane.active
                                    ? context.viewer.localeService.translate("sectionToolContextMenu.deactivate") || "Disable"
                                    : context.viewer.localeService.translate("sectionToolContextMenu.activate") || "Enable";
                            },
                            doAction: (context) => {
                                sectionPlane.active = !sectionPlane.active;
                            }
                        },
                        {
                            getTitle: (context) => {
                                return context.viewer.localeService.translate("sectionToolContextMenu.edit") || "Edit";
                            },
                            getEnabled() {
                              return sectionPlane.active;
                            },
                            doAction: (context) => {

                                sectionPlanesPlugin.hideControl();
                                sectionPlanesPlugin.showControl(sectionPlane.id);

                                const sectionPlanePos = sectionPlane.pos;
                                tempAABB.set(this._viewer.scene.aabb);
                                math.getAABB3Center(tempAABB, tempVec3$3);
                                tempAABB[0] += sectionPlanePos[0] - tempVec3$3[0];
                                tempAABB[1] += sectionPlanePos[1] - tempVec3$3[1];
                                tempAABB[2] += sectionPlanePos[2] - tempVec3$3[2];
                                tempAABB[3] += sectionPlanePos[0] - tempVec3$3[0];
                                tempAABB[4] += sectionPlanePos[1] - tempVec3$3[1];
                                tempAABB[5] += sectionPlanePos[2] - tempVec3$3[2];

                                this._viewer.cameraFlight.flyTo({
                                    aabb: tempAABB,
                                    fitFOV: 65
                                });
                            }
                        },
                        {
                            getTitle: (context) => {
                                return context.viewer.localeService.translate("sectionToolContextMenu.flip") || "Flip";
                            },
                            getEnabled() {
                                return sectionPlane.active;
                            },
                            doAction: (context) => {
                                sectionPlane.flipDir();
                            }
                        },

                        {
                            getTitle: (context) => {
                                return context.viewer.localeService.translate("sectionToolContextMenu.delete") || "Delete";
                            },
                            doAction: (context) => {
                                sectionPlane.destroy();
                            }
                        }
                    ]
                ]
            });
        }

        this.items = [
            [
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("sectionToolContextMenu.clearSlices") || "Clear Slices";
                    },
                    getEnabled: (context) => {
                        return (context.bimViewer.getNumSections() > 0);
                    },
                    doAction: (context) => {
                        context.bimViewer.clearSections();
                    }
                },
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("sectionToolContextMenu.flipSlices") || "Flip Slices";
                    },
                    getEnabled: (context) => {
                        return (context.bimViewer.getNumSections() > 0);
                    },
                    doAction: (context) => {
                        context.bimViewer.flipSections();
                    }
                }
            ],

            [
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("sectionToolContextMenu.disableAllSlices") || "Disable all Slices";
                    },
                    getEnabled: (context) => {
                        return (context.bimViewer.getNumSections() > 0);
                    },
                    doAction: (context) => {
                        context.bimViewer.disableSections();
                    }
                },
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("sectionToolContextMenu.enableAllSlices") || "Enable all Slices";
                    },
                    getEnabled: (context) => {
                        return (context.bimViewer.getNumSections() > 0);
                    },
                    doAction: (context) => {
                        context.bimViewer.enableSections();
                    }
                }
            ],
            sectionPlanesMenuItems
        ];
    }

    destroy() {
        super.destroy();
        const scene = this._viewer.scene;
        scene.off(this._onSceneSectionPlaneCreated);
        scene.off(this._onSceneSectionPlaneDestroyed);
    }
}

/** @private */
class SectionTool extends Controller { // XX

    constructor(parent, cfg) {

        super(parent, cfg);

        if (!cfg.buttonElement) {
            throw "Missing config: buttonElement";
        }

        if (!cfg.menuButtonElement) {
            throw "Missing config: menuButtonElement";
        }

        this._buttonElement = cfg.buttonElement;
        this._counterElement = cfg.counterElement;
        this._containerElement = cfg.containerElement;
        this._menuButtonElement = cfg.menuButtonElement;
        this._menuButtonArrowElement = cfg.menuButtonArrowElement;

        this._sectionPlanesPlugin = new SectionPlanesPlugin(this.viewer, {});

        this._sectionToolContextMenu = new SectionToolContextMenu({
            sectionPlanesPlugin: this._sectionPlanesPlugin,
            hideOnMouseDown: false,
            hideOnAction: false,
            parentNode: this._containerElement
        });

        this._sectionPlanesPlugin.setOverviewVisible(false);

        this.on("enabled", (enabled) => {
            if (!enabled) {
                this._buttonElement.classList.add("disabled");
                if (this._counterElement) {
                    this._counterElement.classList.add("disabled");
                }
                this._menuButtonElement.classList.add("disabled");
                this._menuButtonArrowElement.classList.add("disabled");
            } else {
                this._buttonElement.classList.remove("disabled");
                if (this._counterElement) {
                    this._counterElement.classList.remove("disabled");
                }
                this._menuButtonElement.classList.remove("disabled");
                this._menuButtonArrowElement.classList.remove("disabled");
            }
        });

        this.on("active", (active) => {
            if (active) {
                this._buttonElement.classList.add("active");
                if (this._counterElement) {
                    this._counterElement.classList.add("active");
                }
                this._menuButtonElement.classList.add("active");
                this._menuButtonArrowElement.classList.add("active");
            } else {
                this._buttonElement.classList.remove("active");
                if (this._counterElement) {
                    this._counterElement.classList.remove("active");
                }
                this._menuButtonElement.classList.remove("active");
                this._menuButtonArrowElement.classList.remove("active");
            }
        });

        this.on("active", (active) => {
            if (!active) {
                this._sectionPlanesPlugin.hideControl();
            }
        });

        this._buttonElement.addEventListener("click", (e) => {
            if (!this.getEnabled()) {
                return;
            }
            if (e.target === this._menuButtonElement || e.target.parentNode === this._menuButtonElement) {
                if (this._sectionToolContextMenu.shown) {
                    this._sectionToolContextMenu.hide();
                } else {
                    this._sectionToolContextMenu.context = {
                        bimViewer: this.bimViewer,
                        viewer: this.viewer,
                        sectionTool: this
                    };

                    const rect = this._menuButtonElement.getBoundingClientRect();

                    this._sectionToolContextMenu.show(rect.left + scrollX, rect.bottom + window.scrollY + 5);
                }
                return;
            }
            const active = this.getActive();
            this.setActive(!active);
            e.preventDefault();
        });

        this._sectionToolContextMenu.on("shown", () => {
            this._menuButtonArrowElement.classList.remove("xeokit-arrow-down");
            this._menuButtonArrowElement.classList.add("xeokit-arrow-up");
        });

        this._sectionToolContextMenu.on("hidden", () => {
            this._menuButtonArrowElement.classList.remove("xeokit-arrow-up");
            this._menuButtonArrowElement.classList.add("xeokit-arrow-down");
        });

        this.bimViewer.on("reset", () => {
            this.clear();
            this.setActive(false);
        });

        this.viewer.scene.on("sectionPlaneCreated", () => {
            this._updateSectionPlanesCount();
        });

        this.viewer.scene.on("sectionPlaneDestroyed", () => {
            this._updateSectionPlanesCount();
        });

        this._initSectionMode();
    }

    _initSectionMode() {

        this._containerElement.addEventListener('mouseup', (e) => {

            if (e.which === 1) {

                const coords = getMouseCanvasPos(e);
                if (!this.getActive() || !this.getEnabled()) {
                    return;
                }

                const pickResult = this.viewer.scene.pick({
                    canvasPos: coords,
                    pickSurface: true  // <<------ This causes picking to find the intersection point on the entity
                });

                if (pickResult && pickResult.entity && pickResult.entity.isObject) { // Only slice model objects, not 3D UI helpers

                    const sectionPlane = this._sectionPlanesPlugin.createSectionPlane({
                        pos: pickResult.worldPos,
                        dir: math.mulVec3Scalar(pickResult.worldNormal, -1)
                    });

                    this._sectionPlanesPlugin.showControl(sectionPlane.id);
                }
            }
        });

        this._updateSectionPlanesCount();
    }

    _updateSectionPlanesCount() {
        if (this._counterElement) {
            this._counterElement.innerText = ("" + this.getNumSections());
        }
    }

    getNumSections() {
        return Object.keys(this.viewer.scene.sectionPlanes).length;
    }

    clear() {
        this._sectionPlanesPlugin.clear();
        this._updateSectionPlanesCount();
    }

    flipSections() {
        this._sectionPlanesPlugin.flipSectionPlanes();
    }

    enableSections() {
        const sectionPlanes = this.viewer.scene.sectionPlanes;
        for (let id in sectionPlanes) {
            const sectionPlane = sectionPlanes[id];
            sectionPlane.active = true;
        }
    }

    disableSections() {
        const sectionPlanes = this.viewer.scene.sectionPlanes;
        for (let id in sectionPlanes) {
            const sectionPlane = sectionPlanes[id];
            sectionPlane.active = false;
        }
    }

    hideControl() {
        this._sectionPlanesPlugin.hideControl();
    }

    destroy() {
        this._sectionPlanesPlugin.destroy();
        this._sectionToolContextMenu.destroy();
        super.destroy();
    }
}

function getMouseCanvasPos(event) {
    if (!event) {
        event = window.event;
        this.mouseCanvasPos[0] = event.x;
        this.mouseCanvasPos[1] = event.y;
    } else {
        let element = event.target;
        let totalOffsetLeft = 0;
        let totalOffsetTop = 0;
        while (element.offsetParent) {
            totalOffsetLeft += element.offsetLeft;
            totalOffsetTop += element.offsetTop;
            element = element.offsetParent;
        }
        return [event.pageX - totalOffsetLeft, event.pageY - totalOffsetTop];
    }
}

/** @private */
class NavCubeMode extends Controller {

    constructor(parent, cfg) {

        super(parent, cfg);

        if (!cfg.navCubeCanvasElement) {
            throw "Missing config: navCubeCanvasElement";
        }

        const navCubeCanvasElement = cfg.navCubeCanvasElement;

        this._navCube = new NavCubePlugin(this.viewer, {
            canvasElement: navCubeCanvasElement,
            fitVisible: true,
            color: "#CFCFCF"
        });

        this._navCube.setVisible(this._active);

        this.on("active", (active) => {
            this._navCube.setVisible(active);
        });
    }

    destroy() {
        this._navCube.destroy();
        super.destroy();
    }
}

/**
 * @private
 * @param {*} cfg Configs
 * @param {Boolean} [cfg.enableEditModels=false] Set true to show Add/Edit/Delete options in the menu.
 */
class ModelsContextMenu extends ContextMenu {

    constructor(cfg = {}) {

        const enableEditModels = (!!cfg.enableEditModels);
        const enableMeasurements = (!!cfg.enableMeasurements);

        const items = [
            [
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("modelsContextMenu.loadModel") || "Load";
                    },
                    getEnabled: (context) => {
                        return (!context.bimViewer.isModelLoaded(context.modelId));
                    },
                    doAction: (context) => {
                        context.bimViewer.loadModel(context.modelId);
                    }
                },
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("modelsContextMenu.unloadModel") || "Unload";
                    },
                    getEnabled: (context) => {
                        return context.bimViewer.isModelLoaded(context.modelId);
                    },
                    doAction: (context) => {
                        context.bimViewer.unloadModel(context.modelId);
                    }
                }
            ]
        ];

        if (enableEditModels) {

            items.push([
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("modelsContextMenu.editModel") || "Edit";
                    },
                    getEnabled: (context) => {
                        return true;
                    },
                    doAction: (context) => {
                        context.bimViewer.editModel(context.modelId);
                    }
                },
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("modelsContextMenu.deleteModel") || "Delete";
                    },
                    getEnabled: (context) => {
                        return true;
                    },
                    doAction: (context) => {
                        context.bimViewer.deleteModel(context.modelId);
                    }
                }
            ]);
        }

        items.push([
            {
                getTitle: (context) => {
                    return context.viewer.localeService.translate("modelsContextMenu.loadAllModels") || "Load All";
                },
                getEnabled: (context) => {
                    const bimViewer = context.bimViewer;
                    const modelIds = bimViewer.getModelIds();
                    const loadedModelIds = bimViewer.getLoadedModelIds();
                    return (loadedModelIds.length < modelIds.length);
                },
                doAction: (context) => {
                    context.bimViewer.loadAllModels();
                }
            },
            {
                getTitle: (context) => {
                    return context.viewer.localeService.translate("modelsContextMenu.unloadAllModels") || "Unload All";
                },
                getEnabled: (context) => {
                    const loadedModelIds = context.bimViewer.getLoadedModelIds();
                    return (loadedModelIds.length > 0);
                },
                doAction: (context) => {
                    context.bimViewer.unloadAllModels();
                }
            }
        ]);

        items.push([
            {
                getTitle: (context) => {
                    return context.viewer.localeService.translate("modelsContextMenu.clearSlices") || "Clear Slices";
                },
                getEnabled: (context) => {
                    return (context.bimViewer.getNumSections() > 0);
                },
                doAction: (context) => {
                    context.bimViewer.clearSections();
                }
            }
        ]);

        if (enableMeasurements) {
            items.push([{
                getTitle: (context) => {
                    return context.viewer.localeService.translate("modelsContextMenu.measurements") || "Measurements";
                },
                doAction: function (context) {
                    // Does nothing
                },
                items: [ // Sub-menu
                    [{
                        getTitle: (context) => {
                            return context.viewer.localeService.translate("modelsContextMenu.clearMeasurements") || "Clear";
                        }, getEnabled: (context) => {
                            return (context.bimViewer.getNumMeasurements() > 0);
                        }, doAction: (context) => {
                            context.bimViewer.clearMeasurements();
                        }
                    }, {
                        getTitle: (context) => {
                            return context.bimViewer.getMeasurementsAxisVisible() ? context.viewer.localeService.translate("modelsContextMenu.hideMeasurementAxisWires") || "Hide Axis Wires" : context.viewer.localeService.translate("modelsContextMenu.showMeasurementAxisWires") || "Show Axis Wires"
                        }, getEnabled: (context) => {
                            return (context.bimViewer.getNumMeasurements() > 0);
                        }, doAction: (context) => {
                            context.bimViewer.setMeasurementsAxisVisible(!context.bimViewer.getMeasurementsAxisVisible());
                        }
                    }, {
                        getTitle: (context) => {
                            return context.bimViewer.getMeasurementsSnappingEnabled() ? context.viewer.localeService.translate("modelsContextMenu.disableMeasurementSnapping") || "Disable Snapping" : context.viewer.localeService.translate("modelsContextMenu.enableMeasurementSnapping") || "Enable Snapping"
                        }, getEnabled: (context) => {
                            return (context.bimViewer.getNumMeasurements() > 0);
                        }, doAction: (context) => {
                            context.bimViewer.setMeasurementsSnappingEnabled(!context.bimViewer.getMeasurementsSnappingEnabled());
                        }
                    }]]
            }]);
        }

        super({
            hideOnAction: cfg.hideOnAction,
            context: cfg.context,
            items: items,
            parentNode: cfg.parentNode
        });
    }
}

const tempVec3a$1 = math.vec3();


/**
 * Custom data access strategy for {@link XKTLoaderPlugin}.
 * @private
 */
class BIMViewerDataSource {

    constructor(server) {
        this._server = server;
    }

    setProjectId(projectId) {
        this._projectId = projectId;
    }

    setModelId(modelId) {
        this._modelId = modelId;
    }

    getManifest(src, ok, error) {
        this._server.getSplitModelManifest(this._projectId, this._modelId, src, ok, error);
    }

    getMetaModel(src, ok, error) {
        this._server.getSplitModelMetadata(this._projectId, this._modelId, src, ok, error);
    }

    getXKT(src, ok, error) {
        this._server.getSplitModelGeometry(this._projectId, this._modelId, src, ok, error);
    }
}


/** @private */
class ModelsExplorer extends Controller {

    constructor(parent, cfg) {

        super(parent, cfg);

        if (!cfg.modelsTabElement) {
            throw "Missing config: modelsTabElement";
        }

        if (!cfg.unloadModelsButtonElement) {
            throw "Missing config: unloadModelsButtonElement";
        }

        if (!cfg.modelsElement) {
            throw "Missing config: modelsElement";
        }

        this._containerElement = cfg.containerElement;
        this._enableAddModels = !!cfg.enableEditModels;
        this._modelsTabElement = cfg.modelsTabElement;
        this._loadModelsButtonElement = cfg.loadModelsButtonElement;
        this._unloadModelsButtonElement = cfg.unloadModelsButtonElement;
        this._addModelButtonElement = cfg.addModelButtonElement;
        this._modelsElement = cfg.modelsElement;
        this._modelsTabButtonElement = this._modelsTabElement.querySelector(".xeokit-tab-btn");

        if (!this._modelsTabButtonElement) {
            throw "Missing DOM element: ,xeokit-tab-btn";
        }

        this._dataSource = new BIMViewerDataSource(this.server);

        this._xktLoader = new XKTLoaderPlugin(this.viewer, {
            dataSource: this._dataSource
        });

        this._modelsContextMenu = new ModelsContextMenu({
            enableEditModels: cfg.enableEditModels,
            enableMeasurements: cfg.enableMeasurements,
            hideOnAction: true,
            parentNode: this._containerElement
        });

        this._modelsInfo = {};
        this._numModels = 0;
        this._numModelsLoaded = 0;
        this._projectId = null;
    }

    setObjectColors(objectColors) {
        this._xktLoader.objectDefaults = objectColors;
    }

    loadProject(projectId, done, error) {
        this.server.getProject(projectId, (projectInfo) => {
            this.unloadProject();
            this._projectId = projectId;
            this._modelsInfo = {};
            this._numModels = 0;
            this._parseProject(projectInfo, done);
            if (this._numModelsLoaded < this._numModels) {
                this._loadModelsButtonElement.classList.remove("disabled");
            }
            if (this._numModelsLoaded > 0) {
                this._unloadModelsButtonElement.classList.remove("disabled");
            }
            if (this._enableAddModels) {
                this._addModelButtonElement.classList.remove("disabled");
            }
        }, (errMsg) => {
            this.error(errMsg);
            if (error) {
                error(errMsg);
            }
        });
    }

    _parseProject(projectInfo, done) {
        this._buildModelsMenu(projectInfo);
        this._parseViewerConfigs(projectInfo);
        this._parseViewerContent(projectInfo, () => {
            this._parseViewerState(projectInfo, () => {
                done();
            });
        });
    }

    _buildModelsMenu(projectInfo) {
        var html = "";
        const modelsInfo = projectInfo.models || [];
        this._modelsInfo = {};
        this._numModels = modelsInfo.length;
        for (let i = 0, len = modelsInfo.length; i < len; i++) {
            const modelInfo = modelsInfo[i];
            this._modelsInfo[modelInfo.id] = modelInfo;
            html += "<div class='xeokit-form-check'>";
            html += "<input id='input-" + modelInfo.id + "' type='checkbox' value=''><span id='span-" + modelInfo.id + "' class='disabled'>" + modelInfo.name + "</span>";
            html += "</div>";
        }
        this._modelsElement.innerHTML = html;
        for (let i = 0, len = modelsInfo.length; i < len; i++) {
            const modelInfo = modelsInfo[i];
            const modelId = modelInfo.id;
            const checkBox = this._containerElement.querySelector("#input-" + modelId);
            const span = this._containerElement.querySelector("#span-" + modelId);
            checkBox.addEventListener("click", () => {
                if (checkBox.checked) {
                    this.loadModel(modelId);
                } else {
                    this.unloadModel(modelInfo.id);
                }
            });
            span.addEventListener("click", () => {
                const model = this.viewer.scene.models[modelId];
                const modelLoaded = (!!model);
                if (!modelLoaded) {
                    this.loadModel(modelId);
                } else {
                    this.unloadModel(modelInfo.id);
                }
            });
            span.oncontextmenu = (e) => {
                this._modelsContextMenu.context = {
                    bimViewer: this.bimViewer,
                    viewer: this.viewer,
                    modelId: modelId
                };
                this._modelsContextMenu.show(e.pageX, e.pageY);
                e.preventDefault();
            };
        }
    }

    _parseViewerConfigs(projectInfo) {
        const viewerConfigs = projectInfo.viewerConfigs;
        if (viewerConfigs) {
            this.bimViewer.setConfigs(viewerConfigs);
        }
    }

    _parseViewerContent(projectInfo, done) {
        const viewerContent = projectInfo.viewerContent;
        if (!viewerContent) {
            done();
            return;
        }
        this._parseModelsLoaded(viewerContent, () => {
            done();
        });
    }

    _parseModelsLoaded(viewerContent, done) {
        const modelsLoaded = viewerContent.modelsLoaded;
        if (!modelsLoaded || (modelsLoaded.length === 0)) {
            done();
            return;
        }
        this._loadNextModel(modelsLoaded.slice(0), done);
    }

    _loadNextModel(modelsLoaded, done) {
        if (modelsLoaded.length === 0) {
            done();
            return;
        }
        const modelId = modelsLoaded.pop();
        this.loadModel(modelId,
            () => { // Done
                this._loadNextModel(modelsLoaded, done);
            },
            () => { // Error - recover and attempt to load next model
                this._loadNextModel(modelsLoaded, done);
            });
    }

    _parseViewerState(projectInfo, done) {
        const viewerState = projectInfo.viewerState;
        if (!viewerState) {
            done();
            return;
        }
        this.bimViewer.setViewerState(viewerState, done);
    }

    unloadProject() {
        if (!this._projectId) {
            return;
        }
        const models = this.viewer.scene.models;
        for (var modelId in models) {
            if (models.hasOwnProperty(modelId)) {
                const model = models[modelId];
                model.destroy();
            }
        }
        this._modelsElement.innerHTML = "";
        this._numModelsLoaded = 0;

        this._loadModelsButtonElement.classList.add("disabled");
        this._unloadModelsButtonElement.classList.add("disabled");
        if (this._enableAddModels) {
            this._addModelButtonElement.classList.add("disabled");
        }
        const lastProjectId = this._projectId;
        this._projectId = null;
        this.fire("projectUnloaded", {
            projectId: lastProjectId
        });
    }

    getLoadedProjectId() {
        return this._projectId;
    }

    getModelIds() {
        return Object.keys(this._modelsInfo);
    }

    loadModel(modelId, done, error) {
        if (!this._projectId) {
            const errMsg = "No project currently loaded";
            this.error(errMsg);
            if (error) {
                error(errMsg);
            }
            return;
        }
        const modelInfo = this._modelsInfo[modelId];
        if (!modelInfo) {
            const errMsg = "Model not in currently loaded project";
            this.error(errMsg);
            if (error) {
                error(errMsg);
            }
            return;
        }

        this.bimViewer._busyModal.show(`${this.viewer.localeService.translate("busyModal.loading") || "Loading"} ${modelInfo.name}`);

        const externalMetadata = this.bimViewer.getConfig("externalMetadata");

        if (externalMetadata && !modelInfo.manifest) {
            this.server.getMetadata(this._projectId, modelId, (json) => {
                this._loadGeometry(modelId, modelInfo, json, done, error);
            },
                (errMsg) => {
                    this.bimViewer._busyModal.hide();
                    this.error(errMsg);
                    if (error) {
                        error(errMsg);
                    }
                });
        } else {
            this._loadGeometry(modelId, modelInfo, null, done, error);
        }
    }

    _loadGeometry(modelId, modelInfo, json, done, error) {

        const modelLoaded = () => {
            const checkbox = this._containerElement.querySelector("#input-" + modelId);
            checkbox.checked = true;
            this._numModelsLoaded++;
            this._unloadModelsButtonElement.classList.remove("disabled");
            if (this._numModelsLoaded < this._numModels) {
                this._loadModelsButtonElement.classList.remove("disabled");
            } else {
                this._loadModelsButtonElement.classList.add("disabled");
            }
            if (this._numModelsLoaded === 1) { // Jump camera to view-fit first model loaded
                this._jumpToInitialCamera();
                this.fire("modelLoaded", modelId);
                this.bimViewer._busyModal.hide();
                if (done) {
                    done();
                }
            } else {
                this.fire("modelLoaded", modelId);
                this.bimViewer._busyModal.hide();
                if (done) {
                    done();
                }
            }
        };

        const loadError = (errMsg) => {
            this.bimViewer._busyModal.hide();
            this.error(errMsg);
            if (error) {
                error(errMsg);
            }
        };

        if (modelInfo.manifest) {

            // Load multi-part split model;
            // Uses the BIMViewerDataSource, which then uses the BIMViewer's Server strategy

            this._dataSource.setProjectId(this._projectId);
            this._dataSource.setModelId(modelId);

            const model = this._xktLoader.load({
                id: modelId,
                manifestSrc: modelInfo.manifest,
                excludeUnclassifiedObjects: true,
                origin: modelInfo.origin || modelInfo.position,
                scale: modelInfo.scale,
                rotation: modelInfo.rotation,
                matrix: modelInfo.matrix,
                edges: (modelInfo.edges !== false),
                saoEnabled: modelInfo.saoEnabled,
                pbrEnabled: modelInfo.pbrEnabled,
                backfaces: modelInfo.backfaces,
                globalizeObjectIds: modelInfo.globalizeObjectIds,
                reuseGeometries: (modelInfo.reuseGeometries !== false)
            });

            model.on("loaded", modelLoaded);
            model.on("error", loadError);

        } else {

            // Load single XKT/Metamodel file model;
            // Uses the BIMViewer's Server strategy directly

            this.server.getGeometry(this._projectId, modelId, (arraybuffer) => {
                const model = this._xktLoader.load({
                    id: modelId,
                    metaModelData: json,
                    xkt: arraybuffer,
                    excludeUnclassifiedObjects: true,
                    origin: modelInfo.origin || modelInfo.position,
                    scale: modelInfo.scale,
                    rotation: modelInfo.rotation,
                    matrix: modelInfo.matrix,
                    edges: (modelInfo.edges !== false),
                    saoEnabled: modelInfo.saoEnabled,
                    pbrEnabled: modelInfo.pbrEnabled,
                    backfaces: modelInfo.backfaces,
                    globalizeObjectIds: modelInfo.globalizeObjectIds,
                    reuseGeometries: (modelInfo.reuseGeometries !== false)
                });
                model.on("loaded", modelLoaded);
                model.on("error", loadError);
            }, loadError);
        }
    }

    _jumpToInitialCamera() {
        const viewer = this.viewer;
        const scene = viewer.scene;
        const aabb = scene.getAABB(scene.visibleObjectIds);
        const diag = math.getAABB3Diag(aabb);
        const center = math.getAABB3Center(aabb, tempVec3a$1);
        const camera = scene.camera;
        camera.perspective.fov;
        const dist = Math.abs(diag / Math.tan(45 * math.DEGTORAD));
        const dir = math.normalizeVec3((camera.yUp) ? [-0.5, -0.7071, -0.5] : [-1, 1, -1]);
        const up = math.normalizeVec3((camera.yUp) ? [-0.5, 0.7071, -0.5] : [-1, 1, 1]);
        viewer.cameraControl.pivotPos = center;
        viewer.cameraControl.planView = false;
        viewer.cameraFlight.jumpTo({
            look: center,
            eye: [center[0] - (dist * dir[0]), center[1] - (dist * dir[1]), center[2] - (dist * dir[2])],
            up: up,
            orthoScale: diag * 1.1
        });
    }

    unloadModel(modelId) {
        const model = this.viewer.scene.models[modelId];
        if (!model) {
            this.error("Model not loaded: " + modelId);
            return;
        }
        model.destroy();
        const checkbox = this._containerElement.querySelector("#input-" + modelId);
        checkbox.checked = false;
        this._containerElement.querySelector("#span-" + modelId);
        this._numModelsLoaded--;
        if (this._numModelsLoaded > 0) {
            this._unloadModelsButtonElement.classList.remove("disabled");
        } else {
            this._unloadModelsButtonElement.classList.add("disabled");
        }
        if (this._numModelsLoaded < this._numModels) {
            this._loadModelsButtonElement.classList.remove("disabled");
        } else {
            this._loadModelsButtonElement.classList.add("disabled");
        }
        this.fire("modelUnloaded", modelId);
    }

    unloadAllModels() {
        const models = this.viewer.scene.models;
        const modelIds = Object.keys(models);
        for (var i = 0, len = modelIds.length; i < len; i++) {
            const modelId = modelIds[i];
            this.unloadModel(modelId);
        }
    }

    getNumModelsLoaded() {
        return this._numModelsLoaded;
    }

    _getLoadedModelIds() {
        return Object.keys(this.viewer.scene.models);
    }

    isModelLoaded(modelId) {
        return (!!this.viewer.scene.models[modelId]);
    }

    getModelsInfo() {
        return this._modelsInfo;
    }

    getModelInfo(modelId) {
        return this._modelsInfo[modelId];
    }

    setEnabled(enabled) {
        if (!enabled) {
            this._modelsTabButtonElement.classList.add("disabled");
            this._unloadModelsButtonElement.classList.add("disabled");
        } else {
            this._modelsTabButtonElement.classList.remove("disabled");
            this._unloadModelsButtonElement.classList.remove("disabled");
        }
    }

    /** @private */
    destroy() {
        super.destroy();
        this._xktLoader.destroy();
    }
}

const tempVec3$2 = math.vec3();

/**
 * @private
 */
class TreeViewContextMenu extends ContextMenu {

    constructor(bimViewer, cfg = {}) {
        super(cfg);
        this._bimViewer = bimViewer;
        this._buildMenu(cfg);
    }

    _buildMenu(cfg) {

        const showObjectItems = [];
        const focusObjectItems = [];
        const measurementItems = [];

        const enableMeasurements = (!!cfg.enableMeasurements);

        if (this._bimViewer._enablePropertiesInspector) {
            showObjectItems.push({
                getTitle: (context) => {
                    return context.viewer.localeService.translate("treeViewContextMenu.inspectProperties") || "Inspect Properties";
                }, getShown(context) {
                    return !!context.viewer.metaScene.metaObjects[context.treeViewNode.objectId];
                }, doAction: (context) => {
                    const objectId = context.treeViewNode.objectId;
                    context.bimViewer.showObjectProperties(objectId);
                }
            });
        }

        focusObjectItems.push(...[{
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.viewFit") || "View Fit";
            }, doAction: function (context) {
                const viewer = context.viewer;
                const scene = viewer.scene;
                const objectIds = [];
                context.treeViewPlugin.withNodeTree(context.treeViewNode, (treeViewNode) => {
                    if (treeViewNode.objectId) {
                        objectIds.push(treeViewNode.objectId);
                    }
                });
                scene.setObjectsVisible(objectIds, true);
                scene.setObjectsHighlighted(objectIds, true);
                const aabb = scene.getAABB(objectIds);
                viewer.cameraFlight.flyTo({
                    aabb: aabb, duration: 0.5
                }, () => {
                    setTimeout(function () {
                        scene.setObjectsHighlighted(scene.highlightedObjectIds, false);
                    }, 500);
                });
                viewer.cameraControl.pivotPos = math.getAABB3Center(aabb);
            }
        }, {
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.viewFitSelection") || "View Fit Selected";
            }, getEnabled: (context) => {
                return (context.viewer.scene.numSelectedObjects > 0);
            }, doAction: (context) => {
                const viewer = context.viewer;
                const scene = viewer.scene;
                const sceneAABB = scene.getAABB(scene.selectedObjectIds);
                viewer.cameraFlight.flyTo({
                    aabb: sceneAABB, duration: 0.5
                });
                viewer.cameraControl.pivotPos = math.getAABB3Center(sceneAABB);
            }
        }, {
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.viewFitAll") || "View Fit All";
            }, doAction: function (context) {
                const viewer = context.viewer;
                const scene = viewer.scene;
                const sceneAABB = scene.getAABB(scene.visibleObjectIds);
                viewer.cameraFlight.flyTo({
                    aabb: sceneAABB, duration: 0.5
                });
                viewer.cameraControl.pivotPos = math.getAABB3Center(sceneAABB);
            }
        }]);

        if (enableMeasurements) {
            measurementItems.push(...[{
                getTitle: (context) => {
                    return context.viewer.localeService.translate("treeViewContextMenu.measurements") || "Measurements";
                },
                doAction: function (context) {
                    // Does nothing
                },
                items: [ // Sub-menu
                    [{
                        getTitle: (context) => {
                            return context.viewer.localeService.translate("treeViewContextMenu.clearMeasurements") || "Clear";
                        }, getEnabled: (context) => {
                            return (context.bimViewer.getNumMeasurements() > 0);
                        }, doAction: (context) => {
                            context.bimViewer.clearMeasurements();
                        }
                    }, {
                        getTitle: (context) => {
                            return context.bimViewer.getMeasurementsAxisVisible() ? context.viewer.localeService.translate("treeViewContextMenu.hideMeasurementAxisWires") || "Hide Axis Wires" : context.viewer.localeService.translate("treeViewContextMenu.showMeasurementAxisWires") || "Show Axis Wires"
                        }, getEnabled: (context) => {
                            return (context.bimViewer.getNumMeasurements() > 0);
                        }, doAction: (context) => {
                            context.bimViewer.setMeasurementsAxisVisible(!context.bimViewer.getMeasurementsAxisVisible());
                        }
                    }, {
                        getTitle: (context) => {
                            return context.bimViewer.getMeasurementsSnappingEnabled() ? context.viewer.localeService.translate("treeViewContextMenu.disableMeasurementSnapping") || "Disable Snapping" : context.viewer.localeService.translate("treeViewContextMenu.enableMeasurementSnapping") || "Enable Snapping"
                        }, getEnabled: (context) => {
                            return (context.bimViewer.getNumMeasurements() > 0);
                        }, doAction: (context) => {
                            context.bimViewer.setMeasurementsSnappingEnabled(!context.bimViewer.getMeasurementsSnappingEnabled());
                        }
                    }]]
            }]);
        }

        this.items = [showObjectItems, focusObjectItems, [{
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.isolate") || "Isolate";
            }, doAction: function (context) {
                const viewer = context.viewer;
                const scene = viewer.scene;
                const objectIds = [];
                context.treeViewPlugin.withNodeTree(context.treeViewNode, (treeViewNode) => {
                    if (treeViewNode.objectId) {
                        objectIds.push(treeViewNode.objectId);
                    }
                });
                const aabb = scene.getAABB(objectIds);

                viewer.cameraControl.pivotPos = math.getAABB3Center(aabb, tempVec3$2);

                scene.setObjectsXRayed(scene.xrayedObjectIds, false);
                scene.setObjectsVisible(scene.visibleObjectIds, false);
                // scene.setObjectsPickable(scene.objectIds, false);
                scene.setObjectsSelected(scene.selectedObjectIds, false);

                scene.setObjectsVisible(objectIds, true);
                // scene.setObjectsPickable(objectIds, true);

                viewer.cameraFlight.flyTo({
                    aabb: aabb
                }, () => {
                });
            }
        }], [{
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.hide") || "Hide";
            }, doAction: function (context) {
                context.treeViewPlugin.withNodeTree(context.treeViewNode, (treeViewNode) => {
                    if (treeViewNode.objectId) {
                        const entity = context.viewer.scene.objects[treeViewNode.objectId];
                        if (entity) {
                            entity.visible = false;
                        }
                    }
                });
            }
        }, {
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.hideOthers") || "Hide Others";
            }, doAction: function (context) {
                const scene = context.viewer.scene;
                scene.setObjectsVisible(scene.visibleObjectIds, false);
                scene.setObjectsPickable(scene.xrayedObjectIds, true);
                scene.setObjectsXRayed(scene.xrayedObjectIds, false);
                context.treeViewPlugin.withNodeTree(context.treeViewNode, (treeViewNode) => {
                    if (treeViewNode.objectId) {
                        const entity = scene.objects[treeViewNode.objectId];
                        if (entity) {
                            entity.visible = true;
                        }
                    }
                });
            }
        }, {
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.hideAll") || "Hide All";
            }, getEnabled: function (context) {
                return (context.viewer.scene.visibleObjectIds.length > 0);
            }, doAction: function (context) {
                context.viewer.scene.setObjectsVisible(context.viewer.scene.visibleObjectIds, false);
            }
        }], [{
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.show") || "Show";
            }, doAction: function (context) {
                context.treeViewPlugin.withNodeTree(context.treeViewNode, (treeViewNode) => {
                    if (treeViewNode.objectId) {
                        const entity = context.viewer.scene.objects[treeViewNode.objectId];
                        if (entity) {
                            entity.visible = true;
                            if (entity.xrayed) {
                                entity.pickable = true;
                            }
                            entity.xrayed = false;
                            entity.selected = false;
                        }
                    }
                });
            }
        }, {
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.showOthers") || "Shows Others";
            }, doAction: function (context) {
                const scene = context.viewer.scene;
                scene.setObjectsVisible(scene.objectIds, true);
                scene.setObjectsPickable(scene.xrayedObjectIds, true);
                scene.setObjectsXRayed(scene.xrayedObjectIds, false);
                context.treeViewPlugin.withNodeTree(context.treeViewNode, (treeViewNode) => {
                    if (treeViewNode.objectId) {
                        const entity = scene.objects[treeViewNode.objectId];
                        if (entity) {
                            entity.visible = false;
                        }
                    }
                });
            }
        }, {
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.showAll") || "Show All";
            }, getEnabled: function (context) {
                const scene = context.viewer.scene;
                return ((scene.numVisibleObjects < scene.numObjects) || (context.viewer.scene.numXRayedObjects > 0));
            }, doAction: function (context) {
                const scene = context.viewer.scene;
                scene.setObjectsVisible(scene.objectIds, true);
                scene.setObjectsPickable(scene.xrayedObjectIds, true);
                scene.setObjectsXRayed(scene.xrayedObjectIds, false);
            }
        }], [{
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.xray") || "X-Ray";
            }, doAction: function (context) {
                context.treeViewPlugin.withNodeTree(context.treeViewNode, (treeViewNode) => {
                    if (treeViewNode.objectId) {
                        const entity = context.viewer.scene.objects[treeViewNode.objectId];
                        if (entity) {
                            entity.selected = false;
                            entity.xrayed = true;
                            entity.visible = true;
                            entity.pickable = context.bimViewer.getConfig("xrayPickable");
                        }
                    }
                });
            }
        }, {
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.undoXray") || "Undo X-Ray";
            }, doAction: function (context) {
                context.treeViewPlugin.withNodeTree(context.treeViewNode, (treeViewNode) => {
                    if (treeViewNode.objectId) {
                        const entity = context.viewer.scene.objects[treeViewNode.objectId];
                        if (entity) {
                            entity.xrayed = false;
                            entity.pickable = true;
                        }
                    }
                });
            }
        }, {
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.xrayOthers") || "X-Ray Others";
            }, doAction: function (context) {
                const scene = context.viewer.scene;
                scene.setObjectsVisible(scene.objectIds, true);
                if (!context.bimViewer.getConfig("xrayPickable")) {
                    scene.setObjectsPickable(scene.objectIds, false);
                }
                scene.setObjectsXRayed(scene.objectIds, true);
                scene.setObjectsSelected(scene.selectedObjectIds, false);
                context.treeViewPlugin.withNodeTree(context.treeViewNode, (treeViewNode) => {
                    if (treeViewNode.objectId) {
                        const entity = scene.objects[treeViewNode.objectId];
                        if (entity) {
                            entity.xrayed = false;
                            entity.pickable = true;
                        }
                    }
                });
            }
        }, {
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.xrayAll") || "X-Ray All";
            }, doAction: function (context) {
                const scene = context.viewer.scene;
                scene.setObjectsVisible(scene.objectIds, true);
                scene.setObjectsXRayed(scene.objectIds, true);
                scene.setObjectsSelected(scene.selectedObjectIds, false);
                scene.setObjectsPickable(scene.objectIds, false);
            }
        }, {
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.xrayNone") || "X-Ray None";
            }, getEnabled: function (context) {
                return (context.viewer.scene.numXRayedObjects > 0);
            }, doAction: function (context) {
                const scene = context.viewer.scene;
                const xrayedObjectIds = scene.xrayedObjectIds;
                scene.setObjectsPickable(xrayedObjectIds, true);
                scene.setObjectsXRayed(xrayedObjectIds, false);
            }
        }], [{
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.select") || "Select";
            }, doAction: function (context) {
                context.treeViewPlugin.withNodeTree(context.treeViewNode, (treeViewNode) => {
                    if (treeViewNode.objectId) {
                        const entity = context.viewer.scene.objects[treeViewNode.objectId];
                        if (entity) {
                            entity.selected = true;
                            entity.visible = true;
                        }
                    }
                });
            }
        }, {
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.undoSelect") || "Undo Select";
            }, doAction: function (context) {
                context.treeViewPlugin.withNodeTree(context.treeViewNode, (treeViewNode) => {
                    if (treeViewNode.objectId) {
                        const entity = context.viewer.scene.objects[treeViewNode.objectId];
                        if (entity) {
                            entity.selected = false;
                        }
                    }
                });
            }
        }, {
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.selectNone") || "Select None";
            }, getEnabled: function (context) {
                return (context.viewer.scene.numSelectedObjects > 0);
            }, doAction: function (context) {
                context.viewer.scene.setObjectsSelected(context.viewer.scene.selectedObjectIds, false);
            }
        }], [{
            getTitle: (context) => {
                return context.viewer.localeService.translate("treeViewContextMenu.clearSlices") || "Clear Slices";
            }, getEnabled: function (context) {
                return (context.bimViewer.getNumSections() > 0);
            }, doAction: function (context) {
                context.bimViewer.clearSections();
            }
        }],
            measurementItems];
    }
}

const DEFAULT_DOUBLE_CLICK_THRESHOLD = 300;
const tempVec3$1 = math.vec3();

function collectObjectIdsInSubtree(treeViewPlugin, treeViewNode) {
    const objectIds = [];
    if (!treeViewPlugin || !treeViewNode) {
        return objectIds;
    }
    treeViewPlugin.withNodeTree(treeViewNode, (node) => {
        if (node.objectId) {
            objectIds.push(node.objectId);
        }
    });
    return objectIds;
}

function focusTreeNodeInViewer(bimViewer, treeViewPlugin, treeViewNode) {
    if (!bimViewer || !treeViewPlugin || !treeViewNode) {
        return;
    }

    const viewer = bimViewer.viewer;
    const scene = viewer.scene;
    const objectIds = collectObjectIdsInSubtree(treeViewPlugin, treeViewNode);

    if (objectIds.length === 0) {
        console.log("[treeViewFocusHelper] empty objectIds for node", treeViewNode);
        return;
    }

    const aabb = scene.getAABB(objectIds);
    if (!aabb || !Array.isArray(aabb) || aabb.length !== 6) {
        console.log("[treeViewFocusHelper] no AABB for objectIds", objectIds);
        return;
    }
    console.log("[treeViewFocusHelper] flyTo", {
        nodeId: treeViewNode.nodeId || treeViewNode.id,
        objectIds: objectIds.slice(0, 10),
        count: objectIds.length,
        aabb: aabb
    });

    // ensure visible/pickable before flying
    scene.setObjectsVisible(objectIds, true);
    scene.setObjectsPickable(objectIds, true);

    const center = math.getAABB3Center(aabb, tempVec3$1);
    viewer.cameraControl.pivotPos = center;

    viewer.cameraFlight.flyTo({
        aabb,
        duration: bimViewer.getConfig("viewFitDuration"),
        fitFOV: bimViewer.getConfig("viewFitFOV")
    }, () => {
        const primaryObjectId = treeViewNode.objectId || objectIds[0];
        if (primaryObjectId) {
            bimViewer.showObjectInExplorers(primaryObjectId);
            bimViewer.showObjectProperties(primaryObjectId);
        }
    });
}

/** @private */
class ObjectsExplorer extends Controller {

    constructor(parent, cfg = {}) {

        super(parent);

        if (!cfg.objectsTabElement) {
            throw "Missing config: objectsTabElement";
        }

        if (!cfg.showAllObjectsButtonElement) {
            throw "Missing config: showAllObjectsButtonElement";
        }

        if (!cfg.hideAllObjectsButtonElement) {
            throw "Missing config: hideAllObjectsButtonElement";
        }

        if (!cfg.objectsElement) {
            throw "Missing config: objectsElement";
        }

        this._containerElement = cfg.containerElement;
        this._objectsTabElement = cfg.objectsTabElement;
        this._showAllObjectsButtonElement = cfg.showAllObjectsButtonElement;
        this._hideAllObjectsButtonElement = cfg.hideAllObjectsButtonElement;
        this._objectsTabButtonElement = this._objectsTabElement.querySelector(".xeokit-tab-btn");

        if (!this._objectsTabButtonElement) {
            throw "Missing DOM element: ,xeokit-tab-btn";
        }

        const objectsElement = cfg.objectsElement;

        this._treeView = new TreeViewPlugin(this.viewer, {
            containerElement: objectsElement,
            hierarchy: "containment",
            autoAddModels: false,
            pruneEmptyNodes: true
        });

        this._treeViewContextMenu = new TreeViewContextMenu(this.bimViewer, {
            hideOnAction: true,
            enableMeasurements: cfg.enableMeasurements,
            parentNode: cfg.containerElement
        });

        this._lastNodeClickTime = 0;
        this._lastNodeClickId = null;
        this._doubleClickThreshold = this.viewer.doubleClickTimeFrame || DEFAULT_DOUBLE_CLICK_THRESHOLD;

        this._treeView.on("contextmenu", (e) => {
            this._treeViewContextMenu.context = {
                bimViewer: this.bimViewer,
                viewer: e.viewer,
                treeViewPlugin: e.treeViewPlugin,
                treeViewNode: e.treeViewNode
            };
            this._treeViewContextMenu.show(e.event.pageX, e.event.pageY);
        });

        this._treeView.on("nodeTitleClicked", (e) => {
            const now = (typeof performance !== "undefined" && performance.now) ? performance.now() : Date.now();
            const nodeId = e.treeViewNode.nodeId || e.treeViewNode.id;
            const clickCount = e.event && e.event.detail ? e.event.detail : 1;
            const isSameNode = (this._lastNodeClickId === nodeId);
            const withinThreshold = isSameNode && ((now - this._lastNodeClickTime) <= this._doubleClickThreshold);
            const isDoubleClick = (clickCount >= 2) || withinThreshold;

            this._lastNodeClickTime = now;
            this._lastNodeClickId = nodeId;

            if (isDoubleClick) {
                console.log("[ObjectsExplorer] double click", {
                    nodeId: nodeId,
                    title: e.treeViewNode.title || e.treeViewNode.name
                });
                this._onTreeNodeTitleDoubleClicked(e);
                return;
            }

            this._onTreeNodeTitleSingleClicked(e);
        });

        this._onModelLoaded = this.viewer.scene.on("modelLoaded", (modelId) => {
            if (this.viewer.metaScene.metaModels[modelId]) {
                const modelInfo = this.bimViewer._modelsExplorer.getModelInfo(modelId);
                if (!modelInfo) {
                    return;
                }
                this._treeView.addModel(modelId, {
                    rootName: modelInfo.name
                });
            }
        });

        this._onModelUnloaded = this.viewer.scene.on("modelUnloaded", (modelId) => {
            if (this.viewer.metaScene.metaModels[modelId]) {
                this._treeView.removeModel(modelId);
            }
        });

        this.bimViewer.on("reset", () => {
            this._treeView.collapse();
        });
    }

    setEnabled(enabled) {
        if (!enabled) {
            this._objectsTabButtonElement.classList.add("disabled");
            this._showAllObjectsButtonElement.classList.add("disabled");
            this._hideAllObjectsButtonElement.classList.add("disabled");
        } else {
            this._objectsTabButtonElement.classList.remove("disabled");
            this._showAllObjectsButtonElement.classList.remove("disabled");
            this._hideAllObjectsButtonElement.classList.remove("disabled");
        }
    }

    expandTreeViewToDepth(depth) {
        this._treeView.expandToDepth(depth);
    }

    showNodeInTreeView(objectId) {
        this._treeView.collapse();
        this._treeView.showNode(objectId);
    }

    unShowNodeInTreeView() {
        this._treeView.unShowNode();
    }

    _onTreeNodeTitleSingleClicked(e) {
        const scene = this.viewer.scene;
        const objectIds = [];
        e.treeViewPlugin.withNodeTree(e.treeViewNode, (treeViewNode) => {
            if (treeViewNode.objectId) {
                objectIds.push(treeViewNode.objectId);
            }
        });
        const checked = e.treeViewNode.checked;
        if (checked) {
            scene.setObjectsXRayed(objectIds, false);
            scene.setObjectsVisible(objectIds, false);
            scene.setObjectsPickable(objectIds, true);
        } else {
            scene.setObjectsXRayed(objectIds, false);
            scene.setObjectsVisible(objectIds, true);
            scene.setObjectsPickable(objectIds, true);
        }
    }

    _onTreeNodeTitleDoubleClicked(e) {
        this._focusTreeNodeInViewer(e);
    }

    _focusTreeNodeInViewer(e) {
        focusTreeNodeInViewer(this.bimViewer, e.treeViewPlugin, e.treeViewNode);
    }

    destroy() {
        super.destroy();
        this._treeView.destroy();
        this._treeViewContextMenu.destroy();
        this.viewer.scene.off(this._onModelLoaded);
        this.viewer.scene.off(this._onModelUnloaded);
    }
}

/** @private */
class ClassesExplorer extends Controller {

    constructor(parent, cfg = {}) {

        super(parent);

        if (!cfg.classesTabElement) {
            throw "Missing config: classesTabElement";
        }

        if (!cfg.showAllClassesButtonElement) {
            throw "Missing config: showAllClassesButtonElement";
        }

        if (!cfg.hideAllClassesButtonElement) {
            throw "Missing config: hideAllClassesButtonElement";
        }

        if (!cfg.classesElement) {
            throw "Missing config: classesElement";
        }

        this._containerElement = cfg.containerElement;
        this._classesTabElement = cfg.classesTabElement;
        this._showAllClassesButtonElement = cfg.showAllClassesButtonElement;
        this._hideAllClassesButtonElement = cfg.hideAllClassesButtonElement;
        this._classesTabButtonElement = this._classesTabElement.querySelector(".xeokit-tab-btn");

        if (!this._classesTabButtonElement) {
            throw "Missing DOM element: xeokit-tab-btn";
        }

        const classesElement = cfg.classesElement;

        this._treeView = new TreeViewPlugin(this.viewer, {
            containerElement: classesElement,
            hierarchy: "types",
            autoAddModels: false,
            pruneEmptyNodes: true
        });

        this._treeViewContextMenu = new TreeViewContextMenu(this.bimViewer, {
            hideOnAction: true,
            enableMeasurements: cfg.enableMeasurements,
            parentNode: cfg.containerElement
        });

        this._lastNodeClickTime = 0;
        this._lastNodeClickId = null;
        this._doubleClickThreshold = this.viewer.doubleClickTimeFrame || DEFAULT_DOUBLE_CLICK_THRESHOLD;

        this._treeView.on("contextmenu", (e) => {
            this._treeViewContextMenu.context = {
                bimViewer: this.bimViewer,
                viewer: e.viewer,
                treeViewPlugin: e.treeViewPlugin,
                treeViewNode: e.treeViewNode
            };
            this._treeViewContextMenu.show(e.event.pageX, e.event.pageY);
        });

        this._treeView.on("nodeTitleClicked", (e) => {
            const now = (typeof performance !== "undefined" && performance.now) ? performance.now() : Date.now();
            const nodeId = e.treeViewNode.nodeId || e.treeViewNode.id;
            const clickCount = e.event && e.event.detail ? e.event.detail : 1;
            const isSameNode = (this._lastNodeClickId === nodeId);
            const withinThreshold = isSameNode && ((now - this._lastNodeClickTime) <= this._doubleClickThreshold);
            const isDoubleClick = (clickCount >= 2) || withinThreshold;

            this._lastNodeClickTime = now;
            this._lastNodeClickId = nodeId;

            if (isDoubleClick) {
                console.log("[ClassesExplorer] double click", {
                    nodeId: nodeId,
                    title: e.treeViewNode.title || e.treeViewNode.name
                });
                this._onTreeNodeTitleDoubleClicked(e);
                return;
            }

            this._onTreeNodeTitleSingleClicked(e);
        });

        this._onModelLoaded = this.viewer.scene.on("modelLoaded", (modelId) => {
            if (this.viewer.metaScene.metaModels[modelId]) {
                const modelInfo = this.bimViewer._modelsExplorer.getModelInfo(modelId);
                if (!modelInfo) {
                    return;
                }
                this._treeView.addModel(modelId, {
                    rootName: modelInfo.name
                });
            }
        });

        this._onModelUnloaded = this.viewer.scene.on("modelUnloaded", (modelId) => {
            if (this.viewer.metaScene.metaModels[modelId]) {
                this._treeView.removeModel(modelId);
            }
        });

        this.bimViewer.on("reset", () => {
            this._treeView.collapse();
        });
    }

    setEnabled(enabled) {
        if (!enabled) {
            this._classesTabButtonElement.classList.add("disabled");
            this._showAllClassesButtonElement.classList.add("disabled");
            this._hideAllClassesButtonElement.classList.add("disabled");
        } else {
            this._classesTabButtonElement.classList.remove("disabled");
            this._showAllClassesButtonElement.classList.remove("disabled");
            this._hideAllClassesButtonElement.classList.remove("disabled");
        }
    }

    expandTreeViewToDepth(depth) {
        this._treeView.expandToDepth(depth);
    }

    showNodeInTreeView(objectId) {
        this._treeView.collapse();
        this._treeView.showNode(objectId);
    }

    unShowNodeInTreeView() {
        this._treeView.unShowNode();
    }

    _onTreeNodeTitleSingleClicked(e) {
        const scene = this.viewer.scene;
        const objectIds = [];
        e.treeViewPlugin.withNodeTree(e.treeViewNode, (treeViewNode) => {
            if (treeViewNode.objectId) {
                objectIds.push(treeViewNode.objectId);
            }
        });
        const checked = e.treeViewNode.checked;
        if (checked) {
            scene.setObjectsXRayed(objectIds, false);
            scene.setObjectsVisible(objectIds, false);
            scene.setObjectsPickable(objectIds, true);
        } else {
            scene.setObjectsXRayed(objectIds, false);
            scene.setObjectsVisible(objectIds, true);
            scene.setObjectsPickable(objectIds, true);
        }
    }

    _onTreeNodeTitleDoubleClicked(e) {
        this._focusTreeNodeInViewer(e);
    }

    _focusTreeNodeInViewer(e) {
        focusTreeNodeInViewer(this.bimViewer, e.treeViewPlugin, e.treeViewNode);
    }

    destroy() {
        super.destroy();
        this._treeView.destroy();
        this._treeViewContextMenu.destroy();
        this.viewer.scene.off(this._onModelLoaded);
        this.viewer.scene.off(this._onModelUnloaded);
    }
}

const tempVec3 = math.vec3();

/** @private */
class StoreysExplorer extends Controller {

    constructor(parent, cfg = {}) {

        super(parent);

        if (!cfg.storeysTabElement) {
            throw "Missing config: storeysTabElement";
        }

        if (!cfg.showAllStoreysButtonElement) {
            throw "Missing config: showAllStoreysButtonElement";
        }

        if (!cfg.hideAllStoreysButtonElement) {
            throw "Missing config: hideAllStoreysButtonElement";
        }

        if (!cfg.storeysElement) {
            throw "Missing config: storeysElement";
        }

        this._containerElement = cfg.containerElement;
        this._storeysTabElement = cfg.storeysTabElement;
        this._showAllStoreysButtonElement = cfg.showAllStoreysButtonElement;
        this._hideAllStoreysButtonElement = cfg.hideAllStoreysButtonElement;
        this._storeysTabButtonElement = this._storeysTabElement.querySelector(".xeokit-tab-btn");

        if (!this._storeysTabButtonElement) {
            throw "Missing DOM element: .xeokit-tab-btn";
        }

        const storeysElement = cfg.storeysElement;

        this._treeView = new TreeViewPlugin(this.viewer, {
            containerElement: storeysElement,
            autoAddModels: false,
            hierarchy: "storeys",
            autoExpandDepth: 1
        });

        this._treeViewContextMenu = new TreeViewContextMenu(this.bimViewer, {
            hideOnAction: true,
            enableMeasurements: cfg.enableMeasurements,
            parentNode: cfg.containerElement
        });

        this._lastNodeClickTime = 0;
        this._lastNodeClickId = null;
        this._doubleClickThreshold = this.viewer.doubleClickTimeFrame || DEFAULT_DOUBLE_CLICK_THRESHOLD;

        this._treeView.on("contextmenu", (e) => {
            this._treeViewContextMenu.context = {
                bimViewer: this.bimViewer,
                viewer: e.viewer,
                treeViewPlugin: e.treeViewPlugin,
                treeViewNode: e.treeViewNode,
                pruneEmptyNodes: true
            };
            this._treeViewContextMenu.show(e.event.pageX, e.event.pageY);
        });

        this._treeView.on("nodeTitleClicked", (e) => {
            const now = (typeof performance !== "undefined" && performance.now) ? performance.now() : Date.now();
            const nodeId = e.treeViewNode.nodeId || e.treeViewNode.id;
            const clickCount = e.event && e.event.detail ? e.event.detail : 1;
            const isSameNode = (this._lastNodeClickId === nodeId);
            const withinThreshold = isSameNode && ((now - this._lastNodeClickTime) <= this._doubleClickThreshold);
            const isDoubleClick = (clickCount >= 2) || withinThreshold;

            this._lastNodeClickTime = now;
            this._lastNodeClickId = nodeId;

            if (isDoubleClick) {
                console.log("[StoreysExplorer] double click", {
                    nodeId: nodeId,
                    title: e.treeViewNode.title || e.treeViewNode.name
                });
                this._onTreeNodeTitleDoubleClicked(e);
                return;
            }

            this._onTreeNodeTitleSingleClicked(e);
        });

        this._onModelLoaded = this.viewer.scene.on("modelLoaded", (modelId) =>{
            const modelInfo = this.bimViewer._modelsExplorer.getModelInfo(modelId);
            if (!modelInfo) {
                return;
            }
            this._treeView.addModel(modelId, {
                rootName: modelInfo.name
            });
        });

        this._onModelUnloaded = this.viewer.scene.on("modelUnloaded", (modelId) => {
            if (this.viewer.metaScene.metaModels[modelId]) {
                this._treeView.removeModel(modelId);
            }
        });

        this.bimViewer.on("reset", () => {
            this._treeView.collapse();
            this._treeView.expandToDepth(1);
        });
    }

    setEnabled(enabled) {
        if (!enabled) {
            this._storeysTabButtonElement.classList.add("disabled");
            this._showAllStoreysButtonElement.classList.add("disabled");
            this._hideAllStoreysButtonElement.classList.add("disabled");
        } else {
            this._storeysTabButtonElement.classList.remove("disabled");
            this._showAllStoreysButtonElement.classList.remove("disabled");
            this._hideAllStoreysButtonElement.classList.remove("disabled");
        }
    }

    expandTreeViewToDepth(depth) {
        this._treeView.expandToDepth(depth);
    }

    showNodeInTreeView(objectId) {
        this._treeView.collapse();
        this._treeView.showNode(objectId);
    }

    unShowNodeInTreeView() {
        this._treeView.unShowNode();
    }

    _onTreeNodeTitleSingleClicked(e) {
        const scene = this.viewer.scene;
        const objectIds = [];
        e.treeViewPlugin.withNodeTree(e.treeViewNode, (treeViewNode) => {
            if (treeViewNode.objectId) {
                objectIds.push(treeViewNode.objectId);
            }
        });
        const checked = e.treeViewNode.checked;
        if (checked) {
            scene.setObjectsXRayed(objectIds, false);
            scene.setObjectsVisible(objectIds, false);
            scene.setObjectsPickable(objectIds, true);
        } else {
            scene.setObjectsXRayed(objectIds, false);
            scene.setObjectsVisible(objectIds, true);
            scene.setObjectsPickable(objectIds, true);
        }
    }

    _onTreeNodeTitleDoubleClicked(e) {
        this._focusTreeNodeInViewer(e);
    }

    _focusTreeNodeInViewer(e) {
        focusTreeNodeInViewer(this.bimViewer, e.treeViewPlugin, e.treeViewNode);
    }

    selectStorey(storeyObjectId, done) {
        const metaScene = this.viewer.metaScene;
        const storeyMetaObject = metaScene.metaObjects[storeyObjectId];
        if (!storeyMetaObject) {
            this.error("selectStorey() - object is not found: '" + storeyObjectId + "'");
            return;
        }
        if (storeyMetaObject.type !== "IfcBuildingStorey") {
            this.error("selectStorey() - object is not found: '" + storeyObjectId + "'");
            return;
        }
        const objectIds = storeyMetaObject.getObjectIDsInSubtree();
        this._selectObjects(objectIds, done);
    }

    _selectObjects(objectIds, done) {
        const scene = this.viewer.scene;
        const aabb = scene.getAABB(objectIds);

        this.viewer.cameraControl.pivotPos = math.getAABB3Center(aabb, tempVec3);

        if (done) {

            scene.setObjectsXRayed(scene.objectIds, true);
            scene.setObjectsVisible(scene.objectIds, true);
            scene.setObjectsPickable(scene.objectIds, false);
            scene.setObjectsSelected(scene.selectedObjectIds, false);

            scene.setObjectsXRayed(objectIds, false);
            scene.setObjectsVisible(objectIds, true);
            scene.setObjectsPickable(objectIds, true);

            this.viewer.cameraFlight.flyTo({
                aabb: aabb
            }, () => {
                setTimeout(function () {
                    scene.setObjectsVisible(scene.xrayedObjectIds, false);
                    scene.setObjectsXRayed(scene.xrayedObjectIds, false);
                }, 500);
                done();
            });
        } else {

            scene.setObjectsVisible(scene.objectIds, false);
            scene.setObjectsPickable(scene.xrayedObjectIds, true);
            scene.setObjectsXRayed(scene.xrayedObjectIds, false);
            scene.setObjectsSelected(scene.selectedObjectIds, false);

            scene.setObjectsVisible(objectIds, true);

            this.viewer.cameraFlight.jumpTo({
                aabb: aabb
            });
        }
    }

    destroy() {
        super.destroy();
        this._treeView.destroy();
        this._treeViewContextMenu.destroy();
        this.viewer.scene.off(this._onModelLoaded);
        this.viewer.scene.off(this._onModelUnloaded);
    }
}

const tempVec3a = math.vec3();

/** @private */
class ThreeDMode extends Controller {

    constructor(parent, cfg) {

        super(parent, cfg);

        if (!cfg.buttonElement) {
            throw "Missing config: buttonElement";
        }

        this._saveOrthoActive = null;
        this._buttonElement = cfg.buttonElement;

        this._cameraControlNavModeMediator = cfg.cameraControlNavModeMediator;

        this._active = false;

        this.on("enabled", (enabled) => {
            if (!enabled) {
                this._buttonElement.classList.add("disabled");
            } else {
                this._buttonElement.classList.remove("disabled");
            }
        });

        this._buttonElement.addEventListener("click", (event) => {
            if (this.getEnabled()) {
                this.bimViewer._sectionTool.hideControl();
                this.setActive(!this.getActive(), () => { // Animated
                });
            }
            event.preventDefault();
        });

        this.bimViewer.on("reset", () => {
            this.setActive(true, () => { // Animated
            });
        });
    }

    setEnabled(enabled) {
        super.setEnabled(enabled);
        this._saveOrthoActive = this.bimViewer._orthoMode.getActive();
    }

    setActive(active, done) {
        if (this._active === active) {
            if (done) {
                done();
            }
            return;
        }
        this._active = active;
        if (active) {
            this._buttonElement.classList.add("active");
            if (done) {
                this._enterThreeDMode(() => {
                    this.fire("active", this._active);
                    done();
                });
            } else {
                this._enterThreeDMode();
                this.fire("active", this._active);
            }
        } else {
            this._buttonElement.classList.remove("active");
            if (done) {
                this._exitThreeDMode(() => {
                    this.fire("active", this._active);
                    done();
                });
            } else {
                this._exitThreeDMode();
                this.fire("active", this._active);
            }
        }
    }

    _enterThreeDMode(done) {

        const viewer = this.viewer;
        const scene = viewer.scene;
        const aabb = scene.getAABB(scene.visibleObjectIds);
        const diag = math.getAABB3Diag(aabb);
        const center = math.getAABB3Center(aabb, tempVec3a);
        const dist = Math.abs(diag / Math.tan(65.0 / 2));     // TODO: fovy match with CameraFlight
        const camera = scene.camera;
        const dir = (camera.yUp) ? [-1, -1, -1] : [1, 1, 1];
        const up = (camera.yUp) ? [-1, 1, -1] : [-1, 1, 1];

        viewer.cameraControl.pivotPos = center;

        this.bimViewer._navCubeMode.setActive(true);
        this.bimViewer._firstPersonMode.setEnabled(true);
        this._cameraControlNavModeMediator.setThreeDModeActive(true);
        this.bimViewer._sectionTool.setEnabled(true);
        this.bimViewer._orthoMode.setEnabled(true);

        if (done) {
            viewer.cameraFlight.flyTo({
                look: center,
                eye: [center[0] - (dist * dir[0]), center[1] - (dist * dir[1]), center[2] - (dist * dir[2])],
                up: up,
                orthoScale: diag * 1.3,
                duration: 1,
                projection: this._saveOrthoActive ? "ortho" : "perspective"
            }, () => {
                done();
            });
        } else {
            viewer.cameraFlight.jumpTo({
                look: center,
                eye: [center[0] - (dist * dir[0]), center[1] - (dist * dir[1]), center[2] - (dist * dir[2])],
                up: up,
                orthoScale: diag * 1.3,
                projection: this._saveOrthoActive ? "ortho" : "perspective"
            });
        }
    }

    _exitThreeDMode(done) {

        const viewer = this.viewer;
        const scene = viewer.scene;
        const camera = scene.camera;
        const aabb = scene.getAABB(scene.visibleObjectIds);
        const look2 = math.getAABB3Center(aabb);
        const diag = math.getAABB3Diag(aabb);
        const fitFOV = 45; // fitFOV;
        const sca = Math.abs(diag / Math.tan(fitFOV * math.DEGTORAD));
        const orthoScale2 = diag * 1.3;
        const eye2 = tempVec3a;

        eye2[0] = look2[0] + (camera.worldUp[0] * sca);
        eye2[1] = look2[1] + (camera.worldUp[1] * sca);
        eye2[2] = look2[2] + (camera.worldUp[2] * sca);

        const up2 = math.mulVec3Scalar(camera.worldForward, -1, []);

        this.bimViewer._sectionTool.setActive(false);
        this.bimViewer._firstPersonMode.setEnabled(false);

        this._saveOrthoActive = this.bimViewer._orthoMode.getActive();
         this.bimViewer._orthoMode.setEnabled(false);

        this._cameraControlNavModeMediator.setThreeDModeActive(false);

        if (done) {
            viewer.cameraFlight.flyTo({
                eye: eye2,
                look: look2,
                up: up2,
                orthoScale: orthoScale2,
                projection: "ortho"
            }, () => {
                this.bimViewer._navCubeMode.setActive(false);
            });
        } else {
            viewer.cameraFlight.jumpTo({
                eye: eye2,
                look: look2,
                up: up2,
                orthoScale: orthoScale2,
                projection: "ortho"
            });
            this.bimViewer._navCubeMode.setActive(false);
        }
    }
}

/**
 * @private
 */
class ObjectContextMenu extends ContextMenu {

    constructor(bimViewer, cfg = {}) {
        super(cfg);
        this._bimViewer = bimViewer;
        this._buildMenu(cfg);
    }

    _buildMenu(cfg) {

        const showObjectItems = [];
        const focusObjectItems = [];
        const measurementItems = [];

        const enableMeasurements = (!!cfg.enableMeasurements);

        if (this._bimViewer._enablePropertiesInspector) {
            showObjectItems.push(...[{
                getTitle: (context) => {
                    return context.viewer.localeService.translate("objectContextMenu.inspectProperties") || "Inspect Properties";
                },
                doAction: (context) => {
                    const objectId = context.entity.id;
                    context.bimViewer.showObjectProperties(objectId);
                }
            }]);
        }

        showObjectItems.push(...[
            {
                getTitle: (context) => {
                    return context.viewer.localeService.translate("objectContextMenu.showInTree") || "Show in Explorer";
                },
                doAction: (context) => {
                    const objectId = context.entity.id;
                    context.showObjectInExplorers(objectId);
                }
            }
        ]);

        focusObjectItems.push(...[
            {
                getTitle: (context) => {
                    return context.viewer.localeService.translate("objectContextMenu.viewFit") || "View Fit";
                },
                doAction: (context) => {
                    const viewer = context.viewer;
                    const scene = viewer.scene;
                    const entity = context.entity;
                    viewer.cameraFlight.flyTo({
                        aabb: entity.aabb,
                        duration: 0.5
                    }, () => {
                        setTimeout(function () {
                            scene.setObjectsHighlighted(scene.highlightedObjectIds, false);
                        }, 500);
                    });
                    viewer.cameraControl.pivotPos = math.getAABB3Center(entity.aabb);
                }
            },
            {
                getTitle: (context) => {
                    return context.viewer.localeService.translate("objectContextMenu.viewFitSelection") || "View Fit Selected";
                },
                getEnabled: (context) => {
                    return (context.viewer.scene.numSelectedObjects > 0);
                },
                doAction: (context) => {
                    const viewer = context.viewer;
                    const scene = viewer.scene;
                    const sceneAABB = scene.getAABB(scene.selectedObjectIds);
                    viewer.cameraFlight.flyTo({
                        aabb: sceneAABB,
                        duration: 0.5
                    });
                    viewer.cameraControl.pivotPos = math.getAABB3Center(sceneAABB);
                }
            },
            {
                getTitle: (context) => {
                    return context.viewer.localeService.translate("objectContextMenu.viewFitAll") || "View Fit All";
                },
                doAction: (context) => {
                    const viewer = context.viewer;
                    const scene = viewer.scene;
                    const sceneAABB = scene.getAABB(scene.visibleObjectIds);
                    viewer.cameraFlight.flyTo({
                        aabb: sceneAABB,
                        duration: 0.5
                    });
                    viewer.cameraControl.pivotPos = math.getAABB3Center(sceneAABB);
                }
            }
        ]);

        if (enableMeasurements) {
            measurementItems.push(...[{
                getTitle: (context) => {
                    return context.viewer.localeService.translate("objectContextMenu.measurements") || "Measurements";
                },
                doAction: function (context) {
                    // Does nothing
                },
                items: [ // Sub-menu
                    [{
                        getTitle: (context) => {
                            return context.viewer.localeService.translate("objectContextMenu.clearMeasurements") || "Clear";
                        }, getEnabled: (context) => {
                            return (context.bimViewer.getNumMeasurements() > 0);
                        }, doAction: (context) => {
                            context.bimViewer.clearMeasurements();
                        }
                    }, {
                        getTitle: (context) => {
                            return context.bimViewer.getMeasurementsAxisVisible() ? context.viewer.localeService.translate("objectContextMenu.hideMeasurementAxisWires") || "Hide Axis Wires" : context.viewer.localeService.translate("objectContextMenu.showMeasurementAxisWires") || "Show Axis Wires"
                        }, getEnabled: (context) => {
                            return (context.bimViewer.getNumMeasurements() > 0);
                        }, doAction: (context) => {
                            context.bimViewer.setMeasurementsAxisVisible(!context.bimViewer.getMeasurementsAxisVisible());
                        }
                    }, {
                        getTitle: (context) => {
                            return context.bimViewer.getMeasurementsSnappingEnabled() ? context.viewer.localeService.translate("objectContextMenu.disableMeasurementSnapping") || "Disable Snapping" : context.viewer.localeService.translate("objectContextMenu.enableMeasurementSnapping") || "Enable Snapping"
                        }, getEnabled: (context) => {
                            return (context.bimViewer.getNumMeasurements() > 0);
                        }, doAction: (context) => {
                            context.bimViewer.setMeasurementsSnappingEnabled(!context.bimViewer.getMeasurementsSnappingEnabled());
                        }
                    }]]
            }]);
        }

        this.items = [
            showObjectItems,
            focusObjectItems,
            [
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("objectContextMenu.hide") || "Hide";
                    },
                    getEnabled: (context) => {
                        return context.entity.visible;
                    },
                    doAction: (context) => {
                        context.entity.visible = false;
                    }
                },
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("objectContextMenu.hideOthers") || "Hide Others";
                    },
                    doAction: (context) => {
                        const viewer = context.viewer;
                        const scene = viewer.scene;
                        const entity = context.entity;
                        const metaObject = viewer.metaScene.metaObjects[entity.id];
                        if (!metaObject) {
                            return;
                        }
                        scene.setObjectsVisible(scene.visibleObjectIds, false);
                        metaObject.withMetaObjectsInSubtree((metaObject) => {
                            const entity = scene.objects[metaObject.id];
                            if (entity) {
                                entity.visible = true;
                            }
                        });
                    }
                },
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("objectContextMenu.hideAll") || "Hide All";
                    },
                    getEnabled: (context) => {
                        return (context.viewer.scene.numVisibleObjects > 0);
                    },
                    doAction: (context) => {
                        context.viewer.scene.setObjectsVisible(context.viewer.scene.visibleObjectIds, false);
                    }
                },
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("objectContextMenu.showAll") || "Show All";
                    },
                    getEnabled: (context) => {
                        const scene = context.viewer.scene;
                        return ((scene.numVisibleObjects < scene.numObjects) || (context.viewer.scene.numXRayedObjects > 0));
                    },
                    doAction: (context) => {
                        const scene = context.viewer.scene;
                        scene.setObjectsVisible(scene.objectIds, true);
                        scene.setObjectsPickable(scene.xrayedObjectIds, true);
                        scene.setObjectsXRayed(scene.xrayedObjectIds, false);
                    }
                }
            ],
            [
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("objectContextMenu.xray") || "X-Ray";
                    },
                    getEnabled: (context) => {
                        return (!context.entity.xrayed);
                    },
                    doAction: (context) => {
                        const entity = context.entity;
                        entity.xrayed = true;
                        entity.pickable = context.bimViewer.getConfig("xrayPickable");
                    }
                },
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("objectContextMenu.xrayOthers") || "X-Ray Others";
                    },
                    doAction: (context) => {
                        const viewer = context.viewer;
                        const scene = viewer.scene;
                        const entity = context.entity;
                        const metaObject = viewer.metaScene.metaObjects[entity.id];
                        if (!metaObject) {
                            return;
                        }
                        scene.setObjectsVisible(scene.objectIds, true);
                        scene.setObjectsXRayed(scene.objectIds, true);
                        if (!context.bimViewer.getConfig("xrayPickable")) {
                            scene.setObjectsPickable(scene.objectIds, false);
                        }
                        metaObject.withMetaObjectsInSubtree((metaObject) => {
                            const entity = scene.objects[metaObject.id];
                            if (entity) {
                                entity.xrayed = false;
                                entity.pickable = true;
                            }
                        });
                    }
                },
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("objectContextMenu.xrayAll") || "X-Ray All";
                    },
                    getEnabled: (context) => {
                        const scene = context.viewer.scene;
                        return (scene.numXRayedObjects < scene.numObjects);
                    },
                    doAction: (context) => {
                        const scene = context.viewer.scene;
                        scene.setObjectsVisible(scene.objectIds, true);
                        if (!context.bimViewer.getConfig("xrayPickable")) {
                            scene.setObjectsPickable(scene.objectIds, false);
                        }
                        scene.setObjectsXRayed(scene.objectIds, true);
                    }
                },
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("objectContextMenu.xrayNone") || "X-Ray None";
                    },
                    getEnabled: (context) => {
                        return (context.viewer.scene.numXRayedObjects > 0);
                    },
                    doAction: (context) => {
                        const scene = context.viewer.scene;
                        const xrayedObjectIds = scene.xrayedObjectIds;
                        scene.setObjectsPickable(xrayedObjectIds, true);
                        scene.setObjectsXRayed(xrayedObjectIds, false);
                    }
                }
            ],
            [
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("objectContextMenu.select") || "Select";
                    },
                    getEnabled: (context) => {
                        return (!context.entity.selected);
                    },
                    doAction: (context) => {
                        context.entity.selected = true;

                    }
                },
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("objectContextMenu.undoSelect") || "Undo Select";
                    },
                    getEnabled: (context) => {
                        return context.entity.selected;
                    },
                    doAction: (context) => {
                        context.entity.selected = false;
                    }
                },
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("objectContextMenu.selectNone") || "Select None";
                    },
                    getEnabled: (context) => {
                        return (context.viewer.scene.numSelectedObjects > 0);
                    },
                    doAction: (context) => {
                        context.viewer.scene.setObjectsSelected(context.viewer.scene.selectedObjectIds, false);
                    }
                }
            ],
            [
                {
                    getTitle: (context) => {
                        return context.viewer.localeService.translate("objectContextMenu.clearSlices") || "Clear Slices";
                    },
                    getEnabled: (context) => {
                        return (context.bimViewer.getNumSections() > 0);
                    },
                    doAction: (context) => {
                        context.bimViewer.clearSections();
                    }
                }
            ],
            measurementItems
        ];
    }
}

/**
 * @private
 */
class CanvasContextMenu extends ContextMenu {
    constructor(bimViewer, cfg = {}) {
        super({
            hideOnAction: cfg.hideOnAction,
            context: cfg.context,
            items: [
                [
                    {
                        getTitle: (context) => {
                            return context.viewer.localeService.translate("canvasContextMenu.viewFitAll") || "View Fit All";
                        },
                        doAction: (context) => {
                            const viewer = context.viewer;
                            const scene = viewer.scene;
                            const sceneAABB = scene.getAABB(scene.visibleObjectIds);
                            viewer.cameraFlight.flyTo({
                                aabb: sceneAABB,
                                duration: 0.5
                            });
                            viewer.cameraControl.pivotPos = math.getAABB3Center(sceneAABB);
                        }
                    },
                    {
                        getTitle: (context) => {
                            return context.viewer.localeService.translate("canvasContextMenu.viewFitSelection") || "View Fit Selected";
                        },
                        getEnabled: (context) => {
                            return (context.viewer.scene.numSelectedObjects > 0);
                        },
                        doAction: (context) => {
                            const viewer = context.viewer;
                            const scene = viewer.scene;
                            const sceneAABB = scene.getAABB(scene.selectedObjectIds);
                            viewer.cameraFlight.flyTo({
                                aabb: sceneAABB,
                                duration: 0.5
                            });
                            viewer.cameraControl.pivotPos = math.getAABB3Center(sceneAABB);
                        }
                    }
                ],
                [
                    {
                        getTitle: (context) => {
                            return context.viewer.localeService.translate("canvasContextMenu.hideAll") || "Hide All";
                        },
                        getEnabled: (context) => {
                            return (context.viewer.scene.numVisibleObjects > 0);
                        },
                        doAction: (context) => {
                            context.viewer.scene.setObjectsVisible(context.viewer.scene.visibleObjectIds, false);
                        }
                    },
                    {
                        getTitle: (context) => {
                            return context.viewer.localeService.translate("canvasContextMenu.showAll") || "Show All";
                        },
                        getEnabled: (context) => {
                            const scene = context.viewer.scene;
                            return ((scene.numVisibleObjects < scene.numObjects) || (context.viewer.scene.numXRayedObjects > 0));
                        },
                        doAction: (context) => {
                            const scene = context.viewer.scene;
                            scene.setObjectsVisible(scene.objectIds, true);
                            scene.setObjectsXRayed(scene.xrayedObjectIds, false);
                        }
                    }
                ],
                [
                    {
                        getTitle: (context) => {
                            return context.viewer.localeService.translate("canvasContextMenu.xRayAll") || "X-Ray All";
                        },
                        getEnabled: (context) => {
                            const scene = context.viewer.scene;
                            return (scene.numXRayedObjects < scene.numObjects);
                        },
                        doAction: (context) => {
                            const scene = context.viewer.scene;
                            scene.setObjectsVisible(scene.objectIds, true);
                            scene.setObjectsXRayed(scene.objectIds, true);
                            if (!context.bimViewer.getConfig("xrayPickable")) {
                                scene.setObjectsPickable(scene.objectIds, false);
                            }
                        }
                    },
                    {
                        getTitle: (context) => {
                            return context.viewer.localeService.translate("canvasContextMenu.xRayNone") || "X-Ray None";
                        },
                        getEnabled: (context) => {
                            return (context.viewer.scene.numXRayedObjects > 0);
                        },
                        doAction: (context) => {
                            const xrayedObjectIds = context.viewer.scene.xrayedObjectIds;
                            context.viewer.scene.setObjectsPickable(xrayedObjectIds, true);
                            context.viewer.scene.setObjectsXRayed(xrayedObjectIds, false);
                        }
                    }
                ],
                [
                    {
                        getTitle: (context) => {
                            return context.viewer.localeService.translate("canvasContextMenu.selectNone") || "Select None";
                        },
                        getEnabled: (context) => {
                            return (context.viewer.scene.numSelectedObjects > 0);
                        },
                        doAction: (context) => {
                            context.viewer.scene.setObjectsSelected(context.viewer.scene.selectedObjectIds, false);
                        }
                    }
                ],
                [
                    {
                        getTitle: (context) => {
                            return context.viewer.localeService.translate("canvasContextMenu.resetView") || "Reset View";
                        },
                        doAction: (context) => {
                            context.bimViewer.resetView();
                        }
                    }
                ],
                [
                    {
                        getTitle: (context) => {
                            return context.viewer.localeService.translate("canvasContextMenu.clearSlices") || "Clear Slices";
                        },
                        getEnabled: (context) => {
                            return (context.bimViewer.getNumSections() > 0);
                        },
                        doAction: (context) => {
                            context.bimViewer.clearSections();
                        }
                    }
                ],
                cfg.enableMeasurements ? [
                    { // Item

                        getTitle: (context) => {
                            return context.viewer.localeService.translate("canvasContextMenu.measurements") || "Measurements";
                        },

                        doAction: function (context) {
                            // Does nothing
                        },

                        items: [ // Sub-menu
                            [
                                {
                                    getTitle: (context) => {
                                        return context.viewer.localeService.translate("canvasContextMenu.clearMeasurements") || "Clear";
                                    },
                                    getEnabled: (context) => {
                                        return (context.bimViewer.getNumMeasurements() > 0);
                                    },
                                    doAction: (context) => {
                                        context.bimViewer.clearMeasurements();
                                    }
                                },
                                {
                                    getTitle: (context) => {
                                        return context.bimViewer.getMeasurementsAxisVisible()
                                            ? context.viewer.localeService.translate("canvasContextMenu.hideMeasurementAxisWires") || "Hide Axis Wires"
                                            : context.viewer.localeService.translate("canvasContextMenu.showMeasurementAxisWires") || "Show Axis Wires"
                                    },
                                    getEnabled: (context) => {
                                        return (context.bimViewer.getNumMeasurements() > 0);
                                    },
                                    doAction: (context) => {
                                        context.bimViewer.setMeasurementsAxisVisible(!context.bimViewer.getMeasurementsAxisVisible());
                                    }
                                },
                                {
                                    getTitle: (context) => {
                                        return context.bimViewer.getMeasurementsSnappingEnabled() ? context.viewer.localeService.translate("canvasContextMenu.disableMeasurementSnapping") || "Disable Snapping" : context.viewer.localeService.translate("canvasContextMenu.enableMeasurementSnapping") || "Enable Snapping"
                                    }, getEnabled: (context) => {
                                        return (context.bimViewer.getNumMeasurements() > 0);
                                    }, doAction: (context) => {
                                        context.bimViewer.setMeasurementsSnappingEnabled(!context.bimViewer.getMeasurementsSnappingEnabled());
                                    }
                                }
                            ]
                        ]
                    }
                ] : []
            ],
            parentNode: cfg.parentNode
        });
    }
}

/** @private */
class OrthoMode extends Controller {

    constructor(parent, cfg) {

        super(parent, cfg);

        if (!cfg.buttonElement) {
            throw "Missing config: buttonElement";
        }

        this._buttonElement = cfg.buttonElement;

        this.on("enabled", (enabled) => {
            if (!enabled) {
                this._buttonElement.classList.add("disabled");
            } else {
                this._buttonElement.classList.remove("disabled");
            }
        });

        this._buttonElement.addEventListener("click", (event) => {
            if (this.getEnabled()) {
                this.setActive(!this.getActive(), () => {
                });
            }
            event.preventDefault();
        });

        this.bimViewer.on("reset", () => {
            this.setActive(false);
        });

        this.viewer.camera.on("projection", () => {
            const isOrtho = (this.viewer.camera.projection === "ortho");
            this._active = isOrtho;
            if (this._active) {
                this._buttonElement.classList.add("active");
            } else {
                this._buttonElement.classList.remove("active");
            }
        });

        this._active = false;
        this._buttonElement.classList.remove("active");
    }

    setActive(active, done) {
        if (this._active === active) {
            if (done) {
                done();
            }
            return;
        }
        this._active = active;
        if (active) {
            this._buttonElement.classList.add("active");
            if (done) {
                this._enterOrthoMode(() => {
                    this.fire("active", this._active);
                    done();
                });
            } else {
                this._enterOrthoMode();
                this.fire("active", this._active);
            }
        } else {
            this._buttonElement.classList.remove("active");
            if (done) {
                this._exitOrthoMode(() => {
                    this.fire("active", this._active);
                    done();
                });
            } else {
                this._exitOrthoMode();
                this.fire("active", this._active);
            }
        }
    }

    _enterOrthoMode(done) {
        if (done) {
            this.viewer.cameraFlight.flyTo({projection: "ortho", duration: 0.5}, done);
        } else {
            this.viewer.cameraFlight.jumpTo({projection: "ortho"});
        }
    }

    _exitOrthoMode(done) {
        if (done) {
            this.viewer.cameraFlight.flyTo({projection: "perspective", duration: 0.5}, done);
        } else {
            this.viewer.cameraFlight.jumpTo({projection: "perspective"});
        }
    }
}

/** @private */
class PropertiesInspector extends Controller {

    constructor(parent, cfg = {}) {

        super(parent);

        if (!cfg.propertiesTabElement) {
            throw "Missing config: propertiesTabElement";
        }

        if (!cfg.propertiesElement) {
            throw "Missing config: propertiesElement";
        }

        this._metaObject = null;

        this._propertiesTabElement = cfg.propertiesTabElement;
        this._propertiesElement = cfg.propertiesElement;
        this._propertiesTabButtonElement = this._propertiesTabElement.querySelector(".xeokit-tab-btn");

        if (!this._propertiesTabButtonElement) {
            throw "Missing DOM element: ,xeokit-tab-btn";
        }

        this._onModelUnloaded = this.viewer.scene.on("modelUnloaded", (modelId) => {
            if (this._metaObject) {
                const metaModels = this._metaObject.metaModels;
                for (let i = 0, len = metaModels.length; i < len; i++) {
                    if (metaModels[i].id === modelId) {
                        this.clear();
                        return;
                    }
                }
            }
        });

        this.bimViewer.on("reset", () => {
            this.clear();
        });

        document.addEventListener('click', this._clickListener = (e) => {
            if (!e.target.matches('.xeokit-accordion .xeokit-accordion-button')) {
                return;
            } else {
                const container = e.target.parentElement;
                const panel = container.querySelector('.xeokit-accordion-panel');

                if (!container.classList.contains('active')) {
                    // Opening the panel
                    container.classList.add('active');

                    // Calculate the full height of the content
                    const scrollHeight = panel.scrollHeight;

                    // Set the height to the full content height
                    panel.style.height = scrollHeight + 'px';
                } else {
                    // Closing the panel
                    // First set explicit height based on current height
                    panel.style.height = panel.scrollHeight + 'px';

                    // Force a reflow
                    panel.offsetHeight;

                    // Now set height to 0 for smooth animation
                    panel.style.height = '0px';

                    // Remove active class after transition completes
                    panel.addEventListener('transitionend', function removeActive() {
                        container.classList.remove('active');
                        panel.removeEventListener('transitionend', removeActive);
                    }, { once: true });
                }
            }
        });

        // Add a resize listener to handle content changes
        window.addEventListener('resize', () => {
            const activeContainers = document.querySelectorAll('.xeokit-accordion-container.active');

            activeContainers.forEach(container => {
                const panel = container.querySelector('.xeokit-accordion-panel');

                // Temporarily set height to auto to measure true height
                panel.style.height;
                panel.style.height = 'auto';
                const scrollHeight = panel.scrollHeight;

                // Set back to calculated height
                panel.style.height = scrollHeight + 'px';
            });
        });

        this.clear();
    }

    showObjectPropertySets(objectId) {
        const metaObject = this.viewer.metaScene.metaObjects[objectId];
        if (!metaObject) {
            return;
        }
        const propertySets = metaObject.propertySets;
        if (propertySets && propertySets.length > 0) {
            this._setPropertySets(metaObject, propertySets);
        } else {
            this._setPropertySets(metaObject);
        }
        this._metaObject = metaObject;
    }

    clear() {
        const html = [],
            localizedText = this.viewer.localeService.translate('propertiesInspector.noObjectSelectedWarning') || 'No object inspected. Right-click or long-tab an object and select \'Inspect Properties\' to view its properties here.';
        html.push(`<div class="element-attributes">`);
        html.push(`<p class="xeokit-i18n subsubtitle no-object-selected-warning" data-xeokit-i18n="propertiesInspector.noObjectSelectedWarning">${localizedText}</p>`);
        html.push(`</div>`);
        const htmlStr = html.join("");
       this._propertiesElement.innerHTML = htmlStr;
    }

    _setPropertySets(metaObject, propertySets) {
        const html = [];
        html.push(`<div class="element-attributes">`);
        if (!metaObject) {
            html.push(`<p class="subsubtitle">No object selected</p>`);
        } else {
            html.push('<table class="xeokit-table">');
            html.push(`<tr><td class="td1">Name:</td><td class="td2">${metaObject.name}</td></tr>`);
            if (metaObject.type) {
                html.push(`<tr><td class="td1">Class:</td><td class="td2">${metaObject.type}</td></tr>`);
            }
            html.push(`<tr><td class="td1">UUID:</td><td class="td2">${metaObject.originalSystemId}</td></tr>`);
            html.push(`<tr><td class="td1">Viewer ID:</td><td class="td2">${metaObject.id}</td></tr>`);
            const attributes = metaObject.attributes;
            if (attributes) {
                for (let key in attributes) {
                    html.push(`<tr><td class="td1">${capitalizeFirstChar(key)}:</td><td class="td2">${attributes[key]}</td></tr>`);
                }
            }
            html.push('</table>');
            if (!propertySets || propertySets.length === 0) {
                const localizedText = this.viewer.localeService.translate('propertiesInspector.noPropSetWarning') || 'No properties sets found for this object';
                html.push(`<p class="xeokit-i18n subtitle xeokit-no-prop-set-warning" data-xeokit-i18n="propertiesInspector.noPropSetWarning">${localizedText}</p>`);
                html.push(`</div>`);
            } else {
                html.push(`</div>`);
                html.push(`<div class="xeokit-accordion">`);
                for (let i = 0, len = propertySets.length; i < len; i++) {
                    const propertySet = propertySets[i];
                    const properties = propertySet.properties || [];
                    if (properties.length > 0) {
                        html.push(`<div class="xeokit-accordion-container">
                                        <p class="xeokit-accordion-button"><span></span>${propertySet.name}</p>                                       
                                        <div class="xeokit-accordion-panel">                                           
                                            <table class="xeokit-table"><tbody>`);
                        for (let i = 0, len = properties.length; i < len; i++) {
                            const property = properties[i];
                            html.push(`<tr><td class="td1">${property.name || property.label}:</td><td class="td2">${property.value}</td></tr>`);
                        }
                        html.push(`</tbody></table>
                        </div>
                        </div>`);
                    }
                }
                html.push(`</div>`);
            }
        }
        this._propertiesElement.innerHTML = html.join("");
    }

    setEnabled(enabled) {
        if (!enabled) {
            this._propertiesTabButtonElement.classList.add("disabled");
        } else {
            this._propertiesTabButtonElement.classList.remove("disabled");
        }
    }

    destroy() {
        super.destroy();
        this.viewer.scene.off(this._onModelLoaded);
        this.viewer.scene.off(this._onModelUnloaded);
        document.removeEventListener('click', this._clickListener);
    }
}

function capitalizeFirstChar(str) {
    if (!str) {
        return str;
    }
   return str.charAt(0).toUpperCase() + str.slice(1);
}

const MAX_KD_TREE_DEPTH = 15; // Increase if greater precision needed
const kdTreeDimLength = new Float32Array(3);

/**
 * A k-d tree for World-space 3D Entity collision detection with AABBs and frustums.
 */
class ObjectsKdTree3 {

    constructor(cfg) {

        if (!cfg) {
            throw "Parameter expected: cfg";
        }

        if (!cfg.viewer) {
            throw "Parameter expected: cfg.viewer";
        }

        this.viewer = cfg.viewer;

        this._maxTreeDepth = cfg.maxTreeDepth || MAX_KD_TREE_DEPTH;
        this._root = null;
        this._needsRebuild = true;

        this._onModelLoaded = this.viewer.scene.on("modelLoaded", (modelId) => {
            this._needsRebuild = true;
        });

        this._onModelUnloaded = this.viewer.scene.on("modelUnloaded", (modelId) => {
            this._needsRebuild = true;
        });
    }

    /**
     * Gets the root ObjectsKdTree3 node.
     */
    get root() {
        if (this._needsRebuild) {
            this._rebuild();
        }
        return this._root;
    }

    _rebuild() {
        const viewer = this.viewer;
        const scene = viewer.scene;
        const depth = 0;
        this._root = {
            aabb: scene.getAABB()
        };
        for (let objectId in scene.objects) {
            const entity = scene.objects[objectId];
            this._insertEntity(this._root, entity, depth + 1);
        }
        this._needsRebuild = false;
    }

    _insertEntity(node, entity, depth) {

        const entityAABB = entity.aabb;

        if (depth >= this._maxTreeDepth) {
            node.entities = node.entities || [];
            node.entities.push(entity);
            return;
        }
        if (node.left) {
            if (math.containsAABB3(node.left.aabb, entityAABB)) {
                this._insertEntity(node.left, entity, depth + 1);
                return;
            }
        }
        if (node.right) {
            if (math.containsAABB3(node.right.aabb, entityAABB)) {
                this._insertEntity(node.right, entity, depth + 1);
                return;
            }
        }
        const nodeAABB = node.aabb;
        kdTreeDimLength[0] = nodeAABB[3] - nodeAABB[0];
        kdTreeDimLength[1] = nodeAABB[4] - nodeAABB[1];
        kdTreeDimLength[2] = nodeAABB[5] - nodeAABB[2];
        let dim = 0;
        if (kdTreeDimLength[1] > kdTreeDimLength[dim]) {
            dim = 1;
        }
        if (kdTreeDimLength[2] > kdTreeDimLength[dim]) {
            dim = 2;
        }
        if (!node.left) {
            const aabbLeft = nodeAABB.slice();
            aabbLeft[dim + 3] = ((nodeAABB[dim] + nodeAABB[dim + 3]) / 2.0);
            node.left = {
                aabb: aabbLeft
            };
            if (math.containsAABB3(aabbLeft, entityAABB)) {
                this._insertEntity(node.left, entity, depth + 1);
                return;
            }
        }
        if (!node.right) {
            const aabbRight = nodeAABB.slice();
            aabbRight[dim] = ((nodeAABB[dim] + nodeAABB[dim + 3]) / 2.0);
            node.right = {
                aabb: aabbRight
            };
            if (math.containsAABB3(aabbRight, entityAABB)) {
                this._insertEntity(node.right, entity, depth + 1);
                return;
            }
        }
        node.entities = node.entities || [];
        node.entities.push(entity);
    }

    /**
     * Destroys this ObjectsKdTree3.
     */
    destroy() {
        const scene = this.viewer.scene;
        scene.off(this._onModelLoaded);
        scene.off(this._onModelUnloaded);
        this._root = null;
        this._needsRebuild = true;
    }
}

const LEFT_TO_RIGHT = 0;
const RIGHT_TO_LEFT = 1;

/** @private */
class MarqueeSelectionTool extends Controller {

    constructor(parent, cfg) {

        super(parent);

        if (!cfg.buttonElement) {
            throw "Missing config: buttonElement";
        }

        this._objectsKdTree3 = cfg.objectsKdTree3;
        this._marquee = math.AABB2();
        this._marqueeFrustum = new Frustum$1();
        this._marqueeFrustumProjMat = math.mat4();
        this._marqueeDir = false;

        const buttonElement = cfg.buttonElement;

        this.on("enabled", (enabled) => {
            if (!enabled) {
                buttonElement.classList.add("disabled");
            } else {
                buttonElement.classList.remove("disabled");
            }
        });

        this.on("active", (active) => {
            if (active) {
                buttonElement.classList.add("active");
                this._objectsKdTree3.root; // Lazy-build the kd-tree
            } else {
                buttonElement.classList.remove("active");
            }
        });

        buttonElement.addEventListener("click", (event) => {
            if (this.getEnabled()) {
                const active = this.getActive();
                this.setActive(!active);
            }
            event.preventDefault();
        });

        this.bimViewer.on("reset", () => {
            this.setActive(false);
        });

        const scene = this.viewer.scene;
        const canvas = scene.canvas.canvas;

        this._marqueeElement = document.createElement('div');
        document.body.appendChild(this._marqueeElement);

        const marqueeElement = this._marqueeElement;
        const marqueeStyle = marqueeElement.style;
        marqueeStyle.position = "absolute";
        marqueeStyle["z-index"] = "40000005";
        marqueeStyle.width = 8 + "px";
        marqueeStyle.height = 8 + "px";
        marqueeStyle.visibility = "hidden";
        marqueeStyle.top = 0 + "px";
        marqueeStyle.left = 0 + "px";
        marqueeStyle["box-shadow"] = "0 2px 5px 0 #182A3D;";
        marqueeStyle["opacity"] = 1.0;
        marqueeStyle["pointer-events"] = "none";

        let canvasDragStartX;
        let canvasDragStartY;
        let canvasDragEndX;
        let canvasDragEndY;

        let canvasMarqueeStartX;
        let canvasMarqueeStartY;
        let canvasMarqueeEndX;
        let canvasMarqueeEndY;

        let isMouseDragging = false;
        let mouseWasUpOffCanvas = false;

        canvas.addEventListener("mousedown", (e) => {
            if (!this.getActive() || !this.getEnabled()) {
                return;
            }
            if (e.button !== 0) { // Left button only
                return;
            }
            const input = this.bimViewer.viewer.scene.input;
            if (!input.keyDown[input.KEY_CTRL]) { // Clear selection unless CTRL down
                scene.setObjectsSelected(scene.selectedObjectIds, false);
            }
            canvasDragStartX = e.pageX;
            canvasDragStartY = e.pageY;
            marqueeStyle.visibility = "visible";
            marqueeStyle.left = `${canvasDragStartX}px`;
            marqueeStyle.top = `${canvasDragStartY}px`;
            marqueeStyle.width = "0px";
            marqueeStyle.height = "0px";
            marqueeStyle.display = "block";
            canvasMarqueeStartX = e.offsetX;
            canvasMarqueeStartY = e.offsetY;
            isMouseDragging = true;
            this.viewer.cameraControl.pointerEnabled = false; // Disable camera rotation
        });

        canvas.addEventListener("mouseup", (e) => {
            if (!this.getActive() || !this.getEnabled()) {
                return;
            }
            if (!isMouseDragging && !mouseWasUpOffCanvas) {
                return
            }
            if (e.button !== 0) {
                return;
            }
            canvasDragEndX = e.pageX;
            canvasDragEndY = e.pageY;
            const width = Math.abs(canvasDragEndX - canvasDragStartX);
            const height = Math.abs(canvasDragEndY - canvasDragStartY);
            marqueeStyle.width = `${width}px`;
            marqueeStyle.height = `${height}px`;
            marqueeStyle.visibility = "hidden";
            isMouseDragging = false;
            this.viewer.cameraControl.pointerEnabled = true; // Enable camera rotation
            if (mouseWasUpOffCanvas) {
                mouseWasUpOffCanvas = false;
            }
            if (width > 3 || height > 3) { // Marquee pick if rectangle big enough
                this._marqueePick();
            }
        }); // Bubbling

        document.addEventListener("mouseup", (e) => {
            if (!this.getActive() || !this.getEnabled()) {
                return;
            }
            if (e.button !== 0) { // check if left button was clicked
                return;
            }
            if (!isMouseDragging) {
                return
            }
            marqueeStyle.visibility = "hidden";
            isMouseDragging = false;
            mouseWasUpOffCanvas = true;
            this.viewer.cameraControl.pointerEnabled = true;
        }, true); // Capturing

        canvas.addEventListener("mousemove", (e) => {
            if (!this.getActive() || !this.getEnabled()) {
                return;
            }
            if (e.button !== 0) { // check if left button was clicked
                return;
            }
            if (!isMouseDragging) {
                return
            }
            const x = e.pageX;
            const y = e.pageY;
            const width = x - canvasDragStartX;
            const height = y - canvasDragStartY;
            marqueeStyle.width = `${Math.abs(width)}px`;
            marqueeStyle.height = `${Math.abs(height)}px`;
            marqueeStyle.left = `${Math.min(canvasDragStartX, x)}px`;
            marqueeStyle.top = `${Math.min(canvasDragStartY, y)}px`;
            canvasMarqueeEndX = e.offsetX;
            canvasMarqueeEndY = e.offsetY;
            const marqueeDir = (canvasMarqueeStartX < canvasMarqueeEndX) ? LEFT_TO_RIGHT : RIGHT_TO_LEFT;
            this._setMarqueeDir(marqueeDir);
            this._marquee[0] = Math.min(canvasMarqueeStartX, canvasMarqueeEndX);
            this._marquee[1] = Math.min(canvasMarqueeStartY, canvasMarqueeEndY);
            this._marquee[2] = Math.max(canvasMarqueeStartX, canvasMarqueeEndX);
            this._marquee[3] = Math.max(canvasMarqueeStartY, canvasMarqueeEndY);
        });
    }

    _setMarqueeDir(marqueeDir) {
        if (marqueeDir !== this._marqueeDir) {
            this._marqueeElement.style["background-image"] =
                marqueeDir === LEFT_TO_RIGHT
                    /* Solid */ ? "url(\"data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='6' ry='6' stroke='%23333' stroke-width='4'/%3e%3c/svg%3e\")"
                    /* Dashed */ : "url(\"data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='6' ry='6' stroke='%23333' stroke-width='4' stroke-dasharray='6%2c 14' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e\")";
            this._marqueeDir = marqueeDir;
        }
    }

    _marqueePick() {
        this._buildMarqueeFrustum();
        const entities1 = [];
        const visitNode = (node, intersects = Frustum$1.INTERSECT) => {
            if (intersects === Frustum$1.INTERSECT) {
                intersects = frustumIntersectsAABB3(this._marqueeFrustum, node.aabb);
            }
            if (intersects === Frustum$1.OUTSIDE) {
                return;
            }
            if (node.entities) {
                const entities = node.entities;
                for (let i = 0, len = entities.length; i < len; i++) {
                    const entity = entities[i];
                    if (!entity.visible) {
                        continue;
                    }
                    const entityAABB = entity.aabb;
                    if (this._marqueeDir === LEFT_TO_RIGHT) {
                        // Select entities that are completely inside marquee
                        const intersection = frustumIntersectsAABB3(this._marqueeFrustum, entityAABB);
                        if (intersection === Frustum$1.INSIDE) {
                            entities1.push(entity);
                        }
                    } else {
                        // Select entities that are partially inside marquee
                        const intersection = frustumIntersectsAABB3(this._marqueeFrustum, entityAABB);
                        if (intersection !== Frustum$1.OUTSIDE) {
                            entities1.push(entity);
                        }
                    }
                }
            }
            //    }
            if (node.left) {
                visitNode(node.left, intersects);
            }
            if (node.right) {
                visitNode(node.right, intersects);
            }
        };
        visitNode(this._objectsKdTree3.root);
        for (let i = 0, len = entities1.length; i < len; i++) {
            entities1[i].selected = true;
        }
        return entities1;
    }

    _buildMarqueeFrustum() { // https://github.com/xeokit/xeokit-sdk/issues/869#issuecomment-1165375770
        const canvas = this.viewer.scene.canvas.canvas;
        const canvasWidth = canvas.clientWidth;
        const canvasHeight = canvas.clientHeight;
        const xCanvasToClip = 2.0 / canvasWidth;
        const yCanvasToClip = 2.0 / canvasHeight;
        const NEAR_SCALING = 17;
        const ratio = canvas.clientHeight / canvas.clientWidth;
        const FAR_PLANE = 10000;
        const left = this._marquee[0] * xCanvasToClip + -1;
        const right = this._marquee[2] * xCanvasToClip + -1;
        const bottom = -this._marquee[3] * yCanvasToClip + 1;
        const top = -this._marquee[1] * yCanvasToClip + 1;
        const near = this.viewer.scene.camera.frustum.near * (NEAR_SCALING * ratio);
        const far = FAR_PLANE;
        math.frustumMat4(
            left,
            right,
            bottom * ratio,
            top * ratio,
            near,
            far,
            this._marqueeFrustumProjMat,
        );
        setFrustum(this._marqueeFrustum, this.viewer.scene.camera.viewMatrix, this._marqueeFrustumProjMat);
    }
}

/** @private */
class MeasureDistanceTool extends Controller {

    constructor(parent, cfg) {

        super(parent);

        if (!cfg.buttonElement) {
            throw "Missing config: buttonElement";
        }

        const buttonElement = cfg.buttonElement;

        this._contextMenu = new ContextMenu({
            items: [
                [
                    {
                        getTitle: (context) => {
                            return context.measurement.axisVisible ? (this.viewer.localeService.translate("measureContextMenu.hideMeasurementAxisWires") || "Hide Measurement Axis") : (this.viewer.localeService.translate("measureContextMenu.showMeasurementAxisWires") || "Show Measurement Axis");
                        },
                        doAction: function (context) {
                            context.measurement.axisVisible = !context.measurement.axisVisible;
                        }
                    },
                    {
                        getTitle: (context) => {
                            return context.measurement.labelsVisible ? (this.viewer.localeService.translate("measureContextMenu.hideMeasurementLabels") || "Hide Measurement Labels") : (this.viewer.localeService.translate("measureContextMenu.showMeasurementLabels") || "Show Measurement Labels");
                        },
                        doAction: function (context) {
                            context.measurement.labelsVisible = !context.measurement.labelsVisible;
                        }
                    }
                ], [
                    {
                        getTitle: (context) => {
                            return this.viewer.localeService.translate("measureContextMenu.clearMeasurements") || "Clear Measurements";
                        },
                        doAction: function (context) {
                            context.measurement.destroy();
                        }
                    }
                ]
            ]
        });

        this._contextMenu.on("hidden", () => {
            if (this._contextMenu.context.measurement) {
                this._contextMenu.context.measurement.setHighlighted(false);
            }
        });

        this._distanceMeasurementsPlugin = new DistanceMeasurementsPlugin(this.viewer, {
            defaultAxisVisible: false,
            defaultLabelsOnWires : false
        });

        this._distanceMeasurementsPlugin.on("mouseOver", (e) => {
            e.measurement.setHighlighted(true);
        });

        this._distanceMeasurementsPlugin.on("mouseLeave", (e) => {
            if (this._contextMenu.shown && this._contextMenu.context.measurement.id === e.measurement.id) {
                return;
            }
            e.measurement.setHighlighted(false);
        });

        this._distanceMeasurementsPlugin.on("contextMenu", (e) => {
            this._contextMenu.context = { // Must set context before showing menu
                distanceMeasurementsPlugin: this._distanceMeasurementsPlugin,
                measurement: e.measurement
            };
            this._contextMenu.show(e.event.clientX, e.event.clientY);
            e.event.preventDefault();
        });

        this._distanceMeasurementsMouseControl = new DistanceMeasurementsMouseControl(this._distanceMeasurementsPlugin, {
            //   pointerLens : new PointerLens(viewer)
        });

        this._distanceMeasurementsMouseControl.snapping = true;

        this.on("enabled", (enabled) => {
            if (!enabled) {
                buttonElement.classList.add("disabled");
            } else {
                buttonElement.classList.remove("disabled");
            }
        });

        this.on("active", (active) => {
            if (active) {
                buttonElement.classList.add("active");
                this._distanceMeasurementsMouseControl.activate();
            } else {
                buttonElement.classList.remove("active");
                this._distanceMeasurementsMouseControl.deactivate();
            }
        });

        buttonElement.addEventListener("click", (event) => {
            if (this.getEnabled()) {
                const active = this.getActive();
                this.setActive(!active);
            }
            event.preventDefault();
        });

        this.bimViewer.on("reset", () => {
            this.setActive(false);
            this.clear();
        });
    }

    getNumMeasurements() {
        return Object.keys(this._distanceMeasurementsPlugin.measurements).length;
    }

    setMeasurementsAxisVisible(axisVisible) {
        this._distanceMeasurementsPlugin.setAxisVisible(axisVisible);
    }

    getMeasurementsAxisVisible() {
        return this._distanceMeasurementsPlugin.getAxisVisible();
    }

    setSnappingEnabled(snappingEnabled) {
        return this._distanceMeasurementsMouseControl.snapping = snappingEnabled;
    }

    getSnappingEnabled() {
        return this._distanceMeasurementsMouseControl.snapping;
    }

    clear() {
        this._distanceMeasurementsPlugin.clear();
    }

    destroy() {
        this._distanceMeasurementsPlugin.destroy();
        this._distanceMeasurementsMouseControl.destroy();
        this._contextMenu.destroy();
        super.destroy();
    }
}

/** @private */
class MeasureAngleTool extends Controller {

    constructor(parent, cfg) {

        super(parent);

        if (!cfg.buttonElement) {
            throw "Missing config: buttonElement";
        }

        const buttonElement = cfg.buttonElement;

        this._contextMenu = new ContextMenu({
            items: [
                [
                    {
                        getTitle: (context) => {
                            return context.measurement.labelsVisible ? (this.viewer.localeService.translate("measureContextMenu.hideMeasurementLabels") || "Hide Measurement Labels") : (this.viewer.localeService.translate("measureContextMenu.showMeasurementLabels") || "Show Measurement Labels");
                        },
                        doAction: function (context) {
                            context.measurement.labelsVisible = !context.measurement.labelsVisible;
                        }
                    }
                ], [
                    {
                        getTitle: (context) => {
                            return this.viewer.localeService.translate("measureContextMenu.clearMeasurements") || "Clear Measurements";
                        },
                        doAction: function (context) {
                            context.measurement.destroy();
                        }
                    }
                ]
            ]
        });

        this._contextMenu.on("hidden", () => {
            if (this._contextMenu.context.measurement) {
                this._contextMenu.context.measurement.setHighlighted(false);
            }
        });

        this._angleMeasurementsPlugin = new AngleMeasurementsPlugin(this.viewer, {});

        this._angleMeasurementsPlugin.on("mouseOver", (e) => {
            e.measurement.setHighlighted(true);
        });

        this._angleMeasurementsPlugin.on("mouseLeave", (e) => {
            if (this._contextMenu.shown && this._contextMenu.context.measurement.id === e.measurement.id) {
                return;
            }
            e.measurement.setHighlighted(false);
        });

        this._angleMeasurementsPlugin.on("contextMenu", (e) => {
            this._contextMenu.context = { // Must set context before showing menu
                angleMeasurementsPlugin: this._angleMeasurementsPlugin,
                measurement: e.measurement
            };
            this._contextMenu.show(e.event.clientX, e.event.clientY);
            e.event.preventDefault();
        });

        this._angleMeasurementsMouseControl  = new AngleMeasurementsMouseControl(this._angleMeasurementsPlugin, {
         //   pointerLens : new PointerLens(viewer)
        });

        this._angleMeasurementsMouseControl.snapping = true;
        
        this.on("enabled", (enabled) => {
            if (!enabled) {
                buttonElement.classList.add("disabled");
            } else {
                buttonElement.classList.remove("disabled");
            }
        });

        this.on("active", (active) => {
            if (active) {
                buttonElement.classList.add("active");
                this._angleMeasurementsMouseControl.activate();
            } else {
                buttonElement.classList.remove("active");
                this._angleMeasurementsMouseControl.deactivate();
            }
        });

        buttonElement.addEventListener("click", (event) => {
            if (this.getEnabled()) {
                const active = this.getActive();
                this.setActive(!active);
            }
            event.preventDefault();
        });

        this.bimViewer.on("reset", () => {
            this.setActive(false);
            this.clear();
        });
    }

    getNumMeasurements() {
        return Object.keys(this._angleMeasurementsPlugin.measurements).length;
    }

    setSnappingEnabled(snappingEnabled) {
        return this._angleMeasurementsMouseControl.snapping = snappingEnabled;
    }

    getSnappingEnabled() {
        return this._angleMeasurementsMouseControl.snapping;
    }

    clear() {
        this._angleMeasurementsPlugin.clear();
    }

    destroy() {
        this._angleMeasurementsPlugin.destroy();
        this._angleMeasurementsMouseControl.destroy();
        this._contextMenu.destroy();
        super.destroy();
    }
}

function createExplorerTemplate(cfg) {
    const explorerTemplate = `<div class="xeokit-tabs"> 
    <div class="xeokit-tab xeokit-modelsTab">
        <a class="xeokit-i18n xeokit-tab-btn" href="#" data-xeokit-i18n="modelsExplorer.title">Models</a>
        <div class="xeokit-tab-content">
            <div class="xeokit-btn-group">
                <button type="button" class="xeokit-i18n xeokit-loadAllModels xeokit-btn disabled" data-xeokit-i18n="modelsExplorer.loadAll" data-xeokit-i18ntip="modelsExplorer.loadAllTip" data-tippy-content="Load all models">Load all</button>
                <button type="button" class="xeokit-i18n xeokit-unloadAllModels xeokit-btn disabled" data-xeokit-i18n="modelsExplorer.unloadAll"  data-xeokit-i18ntip="modelsExplorer.unloadAllTip" data-tippy-content="Unload all models">Unload all</button>` +
        (cfg.enableEditModels ? `<button type="button" class="xeokit-i18n xeokit-addModel xeokit-btn disabled" data-xeokit-i18n="modelsExplorer.add"  data-xeokit-i18ntip="modelsExplorer.addTip" data-tippy-content="Add model">Add</button>` : ``) + `</div>
            <div class="xeokit-models" ></div>
        </div>
    </div>
    <div class="xeokit-tab xeokit-objectsTab">
        <a class="xeokit-i18n xeokit-tab-btn disabled" href="#" data-xeokit-i18n="objectsExplorer.title">Objects</a>
        <div class="xeokit-tab-content">
         <div class="xeokit-btn-group">
            <button type="button" class="xeokit-i18n xeokit-showAllObjects xeokit-btn disabled" data-xeokit-i18n="objectsExplorer.showAll" data-xeokit-i18ntip="objectsExplorer.showAllTip" data-tippy-content="Show all objects">Show all</button>
            <button type="button" class="xeokit-i18n xeokit-hideAllObjects xeokit-btn disabled" data-xeokit-i18n="objectsExplorer.hideAll" data-xeokit-i18ntip="objectsExplorer.hideAllTip" data-tippy-content="Hide all objects">Hide all</button>
        </div>
        <div class="xeokit-objects xeokit-tree-panel" ></div>
        </div>
    </div>
    <div class="xeokit-i18n xeokit-tab xeokit-classesTab">
        <a class="xeokit-i18n xeokit-tab-btn disabled" href="#" data-xeokit-i18n="classesExplorer.title">Classes</a>
        <div class="xeokit-tab-content">
            <div class="xeokit-btn-group">
                <button type="button" class="xeokit-i18n xeokit-showAllClasses xeokit-btn disabled" data-xeokit-i18n="classesExplorer.showAll"  data-xeokit-i18ntip="classesExplorer.hideAllTip" data-tippy-content="Show all classes">Show all</button>
                <button type="button" class="xeokit-i18n xeokit-hideAllClasses xeokit-btn disabled" data-xeokit-i18n="classesExplorer.hideAll" data-xeokit-i18ntip="classesExplorer.hideAllTip" data-tippy-content="Hide all classes">Hide all</button>
            </div>
            <div class="xeokit-classes xeokit-tree-panel" ></div>
        </div>
    </div>
     <div class="xeokit-tab xeokit-storeysTab">
        <a class="xeokit-i18n xeokit-tab-btn disabled" href="#" data-xeokit-i18n="storeysExplorer.title">Storeys</a>
        <div class="xeokit-tab-content">
         <div class="xeokit-btn-group">
                <button type="button" class="xeokit-i18n xeokit-showAllStoreys xeokit-btn disabled" data-xeokit-i18n="storeysExplorer.showAll" data-xeokit-i18ntip="storeysExplorer.showAllTip" data-tippy-content="Show all storeys">Show all</button>
                <button type="button" class="xeokit-i18n xeokit-hideAllStoreys xeokit-btn disabled" data-xeokit-i18n="storeysExplorer.hideAll" data-xeokit-i18ntip="storeysExplorer.hideAllTip" data-tippy-content="Hide all storeys">Hide all</button>
            </div>
             <div class="xeokit-storeys xeokit-tree-panel"></div>
        </div>
    </div>
</div>`;
    return explorerTemplate;
}

function createToolbarTemplate(cfg = {}) {
    const toolbarTemplate = `<div class="xeokit-toolbar">
    <!-- Reset button -->
    <div class="xeokit-btn-group">
        <button type="button" class="xeokit-i18n xeokit-reset xeokit-btn fa fa-home fa-2x disabled" data-xeokit-i18ntip="toolbar.resetViewTip" data-tippy-content="Reset view"></button>
    </div>
    <div class="xeokit-btn-group" role="group">
        <!-- 3D Mode button -->
        <button type="button" class="xeokit-i18n xeokit-threeD xeokit-btn fa fa-cube fa-2x disabled" data-xeokit-i18ntip="toolbar.toggle2d3dTip" data-tippy-content="Toggle 2D/3D"></button>
        <!-- Perspective/Ortho Mode button -->
        <button type="button" class="xeokit-i18n xeokit-ortho xeokit-btn fa fa-th fa-2x  disabled" data-xeokit-i18ntip="toolbar.togglePerspectiveTip" data-tippy-content="Toggle Perspective/Ortho"></button>
        <!-- Fit button -->
        <button type="button" class="xeokit-i18n xeokit-fit xeokit-btn fa fa-crop fa-2x disabled" data-xeokit-i18ntip="toolbar.viewFitTip" data-tippy-content="View fit"></button>
        <!-- First Person mode button -->
        <button type="button" class="xeokit-i18n xeokit-firstPerson xeokit-btn fa fa-male fa-2x disabled" data-xeokit-i18ntip="toolbar.firstPersonTip" data-tippy-content="Toggle first-person mode"></button>
          <!-- Show/hide IFCSpaces -->
        <button type="button" class="xeokit-i18n xeokit-showSpaces xeokit-btn fab fa-codepen fa-2x disabled" data-xeokit-i18ntip="toolbar.showSpacesTip" data-tippy-content="Show IFCSpaces"></button>   
    </div>
    <!-- Tools button group -->
    <div class="xeokit-btn-group" role="group">
        <!-- Hide tool button -->
        <button type="button" class="xeokit-i18n xeokit-hide xeokit-btn fa fa-eraser fa-2x disabled" data-xeokit-i18ntip="toolbar.hideObjectsTip" data-tippy-content="Hide objects"></button>
        <!-- Select tool button -->
        <button type="button" class="xeokit-i18n xeokit-select xeokit-btn fa fa-mouse-pointer fa-2x disabled" data-xeokit-i18ntip="toolbar.selectObjectsTip" data-tippy-content="Select objects"></button>    
          <!-- Marquee select tool button -->
        <button type="button" class="xeokit-i18n xeokit-marquee xeokit-btn fas fa-object-group fa-2x disabled" data-xeokit-i18ntip="toolbar.marqueeSelectTip" data-tippy-content="Marquee select objects"></button>`
        + (cfg.enableMeasurements ? `<!-- Measure distance tool button -->
        <button type="button" class="xeokit-i18n xeokit-measure-distance xeokit-btn fa fa-ruler fa-2x disabled" data-xeokit-i18ntip="toolbar.measureDistanceTip" data-tippy-content="Measure distance"></button>  
          <!-- Measure angle tool button -->
        <button type="button" class="xeokit-i18n xeokit-measure-angle xeokit-btn fa fa-chevron-left fa-2x disabled" data-xeokit-i18ntip="toolbar.measureAngleTip" data-tippy-content="Measure angle"></button>`
            : ` `)
        + `<!-- section tool button -->
        <button type="button" class="xeokit-i18n xeokit-section xeokit-btn fa fa-cut fa-2x disabled" data-xeokit-i18ntip="toolbar.sliceObjectsTip" data-tippy-content="Slice objects">
            <div class="xeokit-i18n xeokit-section-menu-button disabled" data-xeokit-i18ntip="toolbar.slicesMenuTip"  data-tippy-content="Slices menu">
                <span class="xeokit-arrow-down xeokit-section-menu-button-arrow"></span>
            </div>
            <div class="xeokit-i18n xeokit-section-counter" data-xeokit-i18ntip="toolbar.numSlicesTip" data-tippy-content="Number of existing slices"></div>
        </button>
    </div>
</div>`;
    return toolbarTemplate;
}

function createInspectorTemplate() {
    const inspectorTemplate = `<div class="xeokit-tabs">  
    <div class="xeokit-tab xeokit-propertiesTab">
        <a class="xeokit-i18n xeokit-tab-btn disabled" href="#" data-xeokit-i18n="propertiesInspector.title">Properties</a>
        <div class="xeokit-tab-content">        
        <div class="xeokit-properties"></div>
        </div>
    </div>
</div>`;
    return inspectorTemplate;
}

function initTabs(containerElement) {

    const tabsClass = 'xeokit-tabs';
    const tabClass = 'xeokit-tab';
    const tabButtonClass = 'xeokit-tab-btn';
    const activeClass = 'active';

    // Activates the chosen tab and deactivates the rest
    function activateTab(chosenTabElement) {
        let tabList = chosenTabElement.parentNode.querySelectorAll('.' + tabClass);
        for (let i = 0; i < tabList.length; i++) {
            let tabElement = tabList[i];
            if (tabElement.isEqualNode(chosenTabElement)) {
                tabElement.classList.add(activeClass);
            } else {
                tabElement.classList.remove(activeClass);
            }
        }
    }

    // Initialize each tabbed container
    let tabbedContainers = containerElement.querySelectorAll('.' + tabsClass);
    for (let i = 0; i < tabbedContainers.length; i++) {
        let tabbedContainer = tabbedContainers[i];
        let tabList = tabbedContainer.querySelectorAll('.' + tabClass);
        activateTab(tabList[0]);
        for (let i = 0; i < tabList.length; i++) {
            let tabElement = tabList[i];
            let tabButton = tabElement.querySelector('.' + tabButtonClass);
            tabButton.addEventListener('click', function (event) {
                event.preventDefault();
                if (this.classList.contains("disabled")) {
                    return;
                }
                activateTab(event.target.parentNode);
            });
        }
    }
}


/**
 * @desc A BIM viewer based on the [xeokit SDK](http://xeokit.io).
 *
 *
 */
class BIMViewer extends Controller {

    /**
     * Constructs a BIMViewer.
     * @param {Server} server Data access strategy.
     * @param {*} cfg Configuration.
     * @param {Boolean} [cfg.enableEditModels=false] Set ````true```` to show "Add", "Edit" and "Delete" options in the Models tab's context menu.
     * @param {Boolean} [cfg.enableMeasurements=true] Set ````true```` to enable distance and angle measurements with the BIMViewer.
     * @param {Boolean} [cfg.keyboardEventsElement] Optional reference to HTML element on which key events should be handled. Defaults to the HTML Document.
     * @param {Node | undefined} [cfg.containerElement] Optional reference of an existing DOM Node (e.g. ShadowRoot), which encapsulates all HTML elements related to viewer plugins, defaults to ````document.body````. 
     */
    constructor(server, cfg = {}) {

        if (!cfg.canvasElement) {
            throw "Config expected: canvasElement";
        }

        if (!cfg.explorerElement) {
            throw "Config expected: explorerElement";
        }

        if (!cfg.toolbarElement) {
            throw "Config expected: toolbarElement";
        }

        if (!cfg.navCubeCanvasElement) {
            throw "Config expected: navCubeCanvasElement";
        }

        const canvasElement = cfg.canvasElement;
        const explorerElement = cfg.explorerElement;
        const inspectorElement = cfg.inspectorElement;
        const toolbarElement = cfg.toolbarElement;
        const navCubeCanvasElement = cfg.navCubeCanvasElement;
        const busyModelBackdropElement = cfg.busyModelBackdropElement;

        explorerElement.oncontextmenu = (e) => {
            e.preventDefault();
        };

        toolbarElement.oncontextmenu = (e) => {
            e.preventDefault();
        };

        navCubeCanvasElement.oncontextmenu = (e) => {
            e.preventDefault();
        };

        const viewer = new Viewer({
            localeService: cfg.localeService,
            canvasElement: canvasElement,
            keyboardEventsElement: cfg.keyboardEventsElement,
            transparent: false,
            backgroundColor: [1, 1, 1],
            backgroundColorFromAmbientLight: false,
            saoEnabled: true,
            pbrEnabled: false,
            colorTextureEnabled: true,


            // Enhances the efficiency of SectionPlane creation by proactively allocating Viewer resources
            // for a specified quantity of SectionPlanes. Introducing this parameter streamlines the initial
            // creation speed of SectionPlanes, particularly up to the designated quantity. This parameter
            // internally configures renderer logic for the specified number of SectionPlanes, eliminating
            // the need for setting up logic with each SectionPlane creation and thereby enhancing responsiveness.
            // It is important to consider that each SectionPlane imposes rendering performance, so it is
            // recommended to set this value to a quantity that aligns with your expected usage.

            numCachedSectionPlanes: 4
        });

        super(null, cfg, server, viewer);

        this._containerElement = cfg.containerElement || document.body;
        this._configs = {};

        this._enableAddModels = !!cfg.enableEditModels;
        this._enableMeasurements = (cfg.enableMeasurements !== false);
        this._enablePropertiesInspector = !!cfg.inspectorElement;

        /**
         * The xeokit [Viewer](https://xeokit.github.io/xeokit-sdk/docs/class/src/viewer/Viewer.js~Viewer.html) at the core of this BIMViewer.
         *
         * @type {Viewer}
         */
        this.viewer = viewer;

        this._objectsKdTree3 = new ObjectsKdTree3(({
            viewer
        }));

        this._customizeViewer();
        this._initCanvasContextMenus();

        explorerElement.innerHTML = createExplorerTemplate(cfg);
        toolbarElement.innerHTML = createToolbarTemplate({ enableMeasurements: this._enableMeasurements });
        if (this._enablePropertiesInspector) {
            inspectorElement.innerHTML = createInspectorTemplate();
        }

        this._explorerElement = explorerElement;
        this._inspectorElement = inspectorElement;

        initTabs(explorerElement);
        if (this._enablePropertiesInspector) {
            initTabs(inspectorElement);
        }

        this._modelsExplorer = new ModelsExplorer(this, {
            enableMeasurements: this._enableMeasurements,
            modelsTabElement: explorerElement.querySelector(".xeokit-modelsTab"),
            loadModelsButtonElement: explorerElement.querySelector(".xeokit-loadAllModels"), // Can be undefined
            unloadModelsButtonElement: explorerElement.querySelector(".xeokit-unloadAllModels"),
            addModelButtonElement: explorerElement.querySelector(".xeokit-addModel"), // Can be undefined
            modelsElement: explorerElement.querySelector(".xeokit-models"),
            enableEditModels: this._enableAddModels,
            containerElement: this._containerElement
        });

        this._objectsExplorer = new ObjectsExplorer(this, {
            enableMeasurements: this._enableMeasurements,
            objectsTabElement: explorerElement.querySelector(".xeokit-objectsTab"),
            showAllObjectsButtonElement: explorerElement.querySelector(".xeokit-showAllObjects"),
            hideAllObjectsButtonElement: explorerElement.querySelector(".xeokit-hideAllObjects"),
            objectsElement: explorerElement.querySelector(".xeokit-objects"),
            containerElement: this._containerElement
        });

        this._classesExplorer = new ClassesExplorer(this, {
            enableMeasurements: this._enableMeasurements,
            classesTabElement: explorerElement.querySelector(".xeokit-classesTab"),
            showAllClassesButtonElement: explorerElement.querySelector(".xeokit-showAllClasses"),
            hideAllClassesButtonElement: explorerElement.querySelector(".xeokit-hideAllClasses"),
            classesElement: explorerElement.querySelector(".xeokit-classes"),
            containerElement: this._containerElement
        });

        this._storeysExplorer = new StoreysExplorer(this, {
            enableMeasurements: this._enableMeasurements,
            storeysTabElement: explorerElement.querySelector(".xeokit-storeysTab"),
            showAllStoreysButtonElement: explorerElement.querySelector(".xeokit-showAllStoreys"),
            hideAllStoreysButtonElement: explorerElement.querySelector(".xeokit-hideAllStoreys"),
            storeysElement: explorerElement.querySelector(".xeokit-storeys"),
            containerElement: this._containerElement
        });

        if (this._enablePropertiesInspector) {
            this._propertiesInspector = new PropertiesInspector(this, {
                propertiesTabElement: inspectorElement.querySelector(".xeokit-propertiesTab"),
                propertiesElement: inspectorElement.querySelector(".xeokit-properties")
            });
        }

        this._resetAction = new ResetAction(this, {
            buttonElement: toolbarElement.querySelector(".xeokit-reset"),
            active: false
        });

        this._fitAction = new FitAction(this, {
            buttonElement: toolbarElement.querySelector(".xeokit-fit"),
            active: false
        });

        // Allows Three-D and First Person toggle buttons to cooperatively switch
        // CameraControl#navMode between "orbit", "firstPerson" and "planView" modes

        const cameraControlNavModeMediator = new (function (bimViewer) {

            let threeDActive = false;

            this.setThreeDModeActive = (active) => {
                if (active) {
                    bimViewer._firstPersonMode.setActive(false);
                    bimViewer._marqueeSelectionTool.setEnabled(true);
                    bimViewer.viewer.cameraControl.navMode = "orbit";
                } else {
                    bimViewer._marqueeSelectionTool.setEnabled(false);
                    bimViewer._marqueeSelectionTool.setActive(false);
                    bimViewer._firstPersonMode.setActive(false);
                    bimViewer.viewer.cameraControl.navMode = "planView";
                }
                threeDActive = active;
            };

            this.setFirstPersonModeActive = (active) => {
                bimViewer.viewer.cameraControl.navMode = active ? "firstPerson" : (threeDActive ? "orbit" : "planView");
            };

        })(this);

        this._threeDMode = new ThreeDMode(this, {
            buttonElement: toolbarElement.querySelector(".xeokit-threeD"),
            cameraControlNavModeMediator,
            active: false
        });

        this._orthoMode = new OrthoMode(this, {
            buttonElement: toolbarElement.querySelector(".xeokit-ortho"),
            active: false
        });

        this._firstPersonMode = new FirstPersonMode(this, {
            buttonElement: toolbarElement.querySelector(".xeokit-firstPerson"),
            cameraControlNavModeMediator,
            active: false
        });

        this._hideTool = new HideTool(this, {
            buttonElement: toolbarElement.querySelector(".xeokit-hide"),
            active: false
        });

        this._selectionTool = new SelectionTool(this, {
            buttonElement: toolbarElement.querySelector(".xeokit-select"),
            active: false
        });

        this._marqueeSelectionTool = new MarqueeSelectionTool(this, {
            buttonElement: toolbarElement.querySelector(".xeokit-marquee"),
            active: false,
            objectsKdTree3: this._objectsKdTree3
        });

        this._showSpacesMode = new ShowSpacesMode(this, {
            buttonElement: toolbarElement.querySelector(".xeokit-showSpaces"),
            active: false
        });

        this._queryTool = new QueryTool(this, {
            active: false
        });

        this._sectionTool = new SectionTool(this, {
            buttonElement: toolbarElement.querySelector(".xeokit-section"),
            counterElement: toolbarElement.querySelector(".xeokit-section-counter"),
            menuButtonElement: toolbarElement.querySelector(".xeokit-section-menu-button"),
            menuButtonArrowElement: toolbarElement.querySelector(".xeokit-section-menu-button-arrow"),
            active: false,
            containerElement: this._containerElement
        });

        if (this._enableMeasurements) {

            this._measureDistanceTool = new MeasureDistanceTool(this, {
                buttonElement: toolbarElement.querySelector(".xeokit-measure-distance"),
                active: false
            });

            this._measureAngleTool = new MeasureAngleTool(this, {
                buttonElement: toolbarElement.querySelector(".xeokit-measure-angle"),
                active: false
            });
        }

        this._navCubeMode = new NavCubeMode(this, {
            navCubeCanvasElement: navCubeCanvasElement,
            active: true
        });

        this._busyModal = new BusyModal(this, {
            busyModalBackdropElement: busyModelBackdropElement
        });

        this._threeDMode.setActive(true);
        this._firstPersonMode.setActive(false);
        this._navCubeMode.setActive(true);

        this._modelsExplorer.on("modelLoaded", (modelId) => {
            if (this._modelsExplorer.getNumModelsLoaded() > 0) {
                this.setControlsEnabled(true);
            }
            this.fire("modelLoaded", modelId);
        });

        this._modelsExplorer.on("modelUnloaded", (modelId) => {
            if (this._modelsExplorer.getNumModelsLoaded() === 0) {
                this.setControlsEnabled(false);
                this.openTab("models");
            }
            this.fire("modelUnloaded", modelId);
        });

        this._resetAction.on("reset", () => {
            this.fire("reset", true);
        });

        this._mutexActivation([
            this._hideTool,
            this._selectionTool,
            this._marqueeSelectionTool,
            this._sectionTool,
            this._enableMeasurements ? this._measureDistanceTool : null,
            this._enableMeasurements ? this._measureAngleTool : null
        ]);

        explorerElement.querySelector(".xeokit-showAllObjects").addEventListener("click", (event) => {
            this.setAllObjectsVisible(true);
            this.setAllObjectsXRayed(false);
            event.preventDefault();
        });

        explorerElement.querySelector(".xeokit-hideAllObjects").addEventListener("click", (event) => {
            this.setAllObjectsVisible(false);
            event.preventDefault();
        });

        explorerElement.querySelector(".xeokit-showAllClasses").addEventListener("click", (event) => {
            this.setAllObjectsVisible(true);
            this.setAllObjectsXRayed(false);
            event.preventDefault();
        });

        explorerElement.querySelector(".xeokit-hideAllClasses").addEventListener("click", (event) => {
            this.setAllObjectsVisible(false);
            event.preventDefault();
        });

        explorerElement.querySelector(".xeokit-showAllStoreys").addEventListener("click", (event) => {
            this.setAllObjectsVisible(true);
            this.setAllObjectsXRayed(false);
            event.preventDefault();
        });

        explorerElement.querySelector(".xeokit-hideAllStoreys").addEventListener("click", (event) => {
            this.setAllObjectsVisible(false);
            event.preventDefault();
        });

        explorerElement.querySelector(".xeokit-loadAllModels").addEventListener("click", (event) => {
            this.setControlsEnabled(false); // For quick UI feedback
            this.loadAllModels();
            event.preventDefault();
        });

        explorerElement.querySelector(".xeokit-unloadAllModels").addEventListener("click", (event) => {
            this.setControlsEnabled(false); // For quick UI feedback
            this._modelsExplorer.unloadAllModels();
            event.preventDefault();
        });

        if (this._enableAddModels) {
            explorerElement.querySelector(".xeokit-addModel").addEventListener("click", (event) => {
                this.fire("addModel", {});
                event.preventDefault();
            });
        }

        this._bcfViewpointsPlugin = new BCFViewpointsPlugin(this.viewer, {
            xrayAsZeroAlpha: true
        });

        this._fastNavPlugin = new FastNavPlugin(viewer, {
            hideEdges: true,
            hideSAO: true,
            hidePBR: false,
            hideColorTexture: false,
            hideTransparentObjects: false,
            scaleCanvasResolution: false,
            scaleCanvasResolutionFactor: 0.6
        });

        // this.viewer.scene.on("rendered", () => {
        //     const fastNavPlugin = this._fastNavPlugin;
        //     fastNavPlugin.hideEdges = (hideEdgesMinDrawCount < (stats.frame.drawElements + stats.frame.drawArrays));
        //     fastNavPlugin.scaleCanvasResolution = (scaleCanvasResolutionMinDrawCount < (stats.frame.drawElements + stats.frame.drawArrays));
        // });

        this._initConfigs();
        this.setControlsEnabled(false);
    }

    /**
     * Returns the LocaleService that was configured on this Viewer.
     *
     * @return {LocaleService} The LocaleService.
     */
    get localeService() {
        return this.viewer.localeService;
    }

    _customizeViewer() {

        const scene = this.viewer.scene;

        // Emphasis effects

        scene.xrayMaterial.fill = false;
        scene.xrayMaterial.fillAlpha = 0.3;
        scene.xrayMaterial.fillColor = [0, 0, 0];
        scene.xrayMaterial.edges = true;
        scene.xrayMaterial.edgeAlpha = 0.1;
        scene.xrayMaterial.edgeColor = [0, 0, 0];

        scene.highlightMaterial.edges = true;
        scene.highlightMaterial.edgeColor = [1, 1, 1];
        scene.highlightMaterial.edgeAlpha = 1.0;
        scene.highlightMaterial.fill = true;
        scene.highlightMaterial.fillAlpha = 0.1;
        scene.highlightMaterial.fillColor = [1, 0, 0];

        scene.selectedMaterial.edges = true;
        scene.selectedMaterial.edgeColor = [1, 1, 1];
        scene.selectedMaterial.edgeAlpha = 1.0;
        scene.selectedMaterial.fill = true;
        scene.selectedMaterial.fillAlpha = 0.1;
        scene.selectedMaterial.fillColor = [0, 1, 0];

        //------------------------------------------------------------------------------------------------------------------
        // Configure points material
        //------------------------------------------------------------------------------------------------------------------

        scene.pointsMaterial.pointSize = 1;
        scene.pointsMaterial.roundPoints = true;
        scene.pointsMaterial.perspectivePoints = true;
        scene.pointsMaterial.minPerspectivePointSize = 2;
        scene.pointsMaterial.maxPerspectivePointSize = 4;

        // Camera control

        this.viewer.cameraControl.panRightClick = false;
        this.viewer.cameraControl.followPointer = true;
        this.viewer.cameraControl.doublePickFlyTo = false;
        this.viewer.cameraControl.smartPivot = true;

        // Dolly tweaks for best precision when aligning camera for BCF snapshots

        this.viewer.cameraControl.keyboardDollyRate = 100.0;
        this.viewer.cameraControl.mouseWheelDollyRate = 100.0;
        this.viewer.cameraControl.dollyInertia = 0;
        this.viewer.cameraControl.dollyMinSpeed = 0.04;
        this.viewer.cameraControl.dollyProximityThreshold = 30.0;

        const cameraPivotElement = document.createRange().createContextualFragment("<div class='xeokit-camera-pivot-marker'></div>").firstChild;
        this._containerElement.appendChild(cameraPivotElement);
        this.viewer.cameraControl.pivotElement = cameraPivotElement;

        scene.camera.perspective.near = 0.01;
        scene.camera.perspective.far = 3000.0;
        scene.camera.ortho.near = 0.01;
        scene.camera.ortho.far = 2000.0; //

        // Scalable Ambient Obscurance (SAO) defaults
        // Since SAO is non-interactive, set to higher-quality

        const sao = scene.sao;
        sao.enabled = true;
        sao.numSamples = 50;
        sao.kernelRadius = 200;
    }

    _initCanvasContextMenus() {

        this._canvasContextMenu = new CanvasContextMenu(this, {
            hideOnAction: true,
            enableMeasurements: this._enableMeasurements,
            parentNode: this._containerElement
        });
        this._objectContextMenu = new ObjectContextMenu(this, {
            hideOnAction: true,
            enableMeasurements: this._enableMeasurements,
            parentNode: this._containerElement
        });

        const getCanvasPosFromEvent = function (event) {
            const canvasPos = [];
            if (!event) {
                event = window.event;
                canvasPos[0] = event.x;
                canvasPos[1] = event.y;
            } else {
                let element = event.target;
                let totalOffsetLeft = 0;
                let totalOffsetTop = 0;
                let totalScrollX = 0;
                let totalScrollY = 0;
                while (element.offsetParent) {
                    totalOffsetLeft += element.offsetLeft;
                    totalOffsetTop += element.offsetTop;
                    totalScrollX += element.scrollLeft;
                    totalScrollY += element.scrollTop;
                    element = element.offsetParent;
                }
                canvasPos[0] = event.pageX + totalScrollX - totalOffsetLeft;
                canvasPos[1] = event.pageY + totalScrollY - totalOffsetTop;
            }
            return canvasPos;
        };

        this.viewer.scene.canvas.canvas.addEventListener('contextmenu', (event) => {

            const canvasPos = getCanvasPosFromEvent(event);

            const hit = this.viewer.scene.pick({
                canvasPos
            });

            if (hit && hit.entity.isObject) {
                this._canvasContextMenu.hide();
                this._objectContextMenu.context = {
                    viewer: this.viewer,
                    bimViewer: this,
                    showObjectInExplorers: (objectId) => {
                        const openTabId = this.getOpenTab();
                        if (openTabId !== "objects" && openTabId !== "classes" && openTabId !== "storeys") {
                            // Scroll won't work if tab not open
                            this.openTab("objects");
                        }
                        this.showObjectInExplorers(objectId);
                    },
                    entity: hit.entity
                };
                this._objectContextMenu.show(event.pageX, event.pageY);
            } else {
                this._objectContextMenu.hide();
                this._canvasContextMenu.context = {
                    viewer: this.viewer,
                    bimViewer: this
                };
                this._canvasContextMenu.show(event.pageX, event.pageY);
            }
        });
    }

    _initConfigs() {
        this.setConfigs({
            "cameraNear": "0.05",
            "cameraFar": "3000.0",
            "smartPivot": true,
            "saoEnabled": true,
            "pbrEnabled": false,
            "scaleCanvasResolution": false,
            "saoBias": 0.5,
            "saoIntensity": 0.15,
            "saoNumSamples": 40,
            "saoKernelRadius": 100,
            "edgesEnabled": true,
            "xrayContext": true,
            "xrayPickable": false,
            "selectedGlowThrough": true,
            "highlightGlowThrough": true,
            "backgroundColor": [1.0, 1.0, 1.0],
            "externalMetadata": false,
            "dtxEnabled": false
        });
    }

    /**
     * Sets a batch of viewer configurations.
     *
     * Note that this method is not to be confused with {@link BIMViewer#setViewerState}, which batch-updates various states of the viewer's UI and 3D view.
     *
     * See [Viewer Configurations](https://xeokit.github.io/xeokit-bim-viewer/docs/#viewer-configurations) for the list of available configurations.
     *
     * @param {*} viewerConfigs Map of key-value configuration pairs.
     */
    setConfigs(viewerConfigs) {
        for (let name in viewerConfigs) {
            if (viewerConfigs.hasOwnProperty(name)) {
                const value = viewerConfigs[name];
                this.setConfig(name, value);
            }
        }
    }

    /**
     * Sets a viewer configuration.
     *
     * See [Viewer Configurations](https://xeokit.github.io/xeokit-bim-viewer/docs/#viewer-configurations) for the list of available configurations.
     *
     * @param {String} name Configuration name.
     * @param {*} value Configuration value.
     */
    setConfig(name, value) {

        function parseBool(value) {
            return ((value === true) || (value === "true"));
        }

        try {
            switch (name) {

                case "backgroundColor":
                    const rgbColor = value;
                    this.setBackgroundColor(rgbColor);
                    this._configs[name] = rgbColor;
                    break;

                case "cameraNear":
                    const near = parseFloat(value);
                    this.viewer.scene.camera.perspective.near = near;
                    this.viewer.scene.camera.ortho.near = near;
                    this._configs[name] = near;
                    break;

                case "cameraFar":
                    const far = parseFloat(value);
                    this.viewer.scene.camera.perspective.far = far;
                    // this.viewer.scene.camera.ortho.far = far;
                    this._configs[name] = far;
                    break;

                case "smartPivot":
                    this.viewer.cameraControl.smartPivot = this._configs[name] = parseBool(value);
                    break;

                case "saoEnabled":
                    this._fastNavPlugin.saoEnabled = this._configs[name] = parseBool(value);
                    break;

                case "saoBias":
                    this.viewer.scene.sao.bias = parseFloat(value);
                    break;

                case "saoIntensity":
                    this.viewer.scene.sao.intensity = parseFloat(value);
                    break;

                case "saoKernelRadius":
                    this.viewer.scene.sao.kernelRadius = this._configs[name] = parseFloat(value);
                    break;

                case "saoNumSamples":
                    this.viewer.scene.sao.numSamples = this._configs[name] = parseFloat(value);
                    break;

                case "saoBlur":
                    this.viewer.scene.sao.blur = this._configs[name] = parseBool(value);
                    break;

                case "edgesEnabled":
                    this._fastNavPlugin.edgesEnabled = this._configs[name] = parseBool(value);
                    break;

                case "pbrEnabled":
                    this._fastNavPlugin.pbrEnabled = this._configs[name] = parseBool(value);
                    break;

                case "scaleCanvasResolution":
                    this._fastNavPlugin.scaleCanvasResolution = this._configs[name] = parseBool(value);
                    break;

                case "viewFitFOV":
                    this.viewer.cameraFlight.fitFOV = this._configs[name] = parseFloat(value);
                    break;

                case "viewFitDuration":
                    this.viewer.cameraFlight.duration = this._configs[name] = parseFloat(value);
                    break;

                case "perspectiveFOV":
                    this.viewer.camera.perspective.fov = this._configs[name] = parseFloat(value);
                    break;

                case "excludeUnclassifiedObjects":
                    this._configs[name] = parseBool(value);
                    break;

                case "xrayContext":
                    this._configs[name] = value;
                    break;

                case "xrayPickable":
                    this._configs[name] = parseBool(value);
                    break;

                case "selectedGlowThrough":
                    const selectedGlowThrough = this._configs[name] = parseBool(value);
                    const selectedMaterial = this.viewer.scene.selectedMaterial;
                    selectedMaterial.glowThrough = selectedGlowThrough;
                    selectedMaterial.fillAlpha = selectedGlowThrough ? 0.5 : 1.0;
                    selectedMaterial.edgeAlpha = selectedGlowThrough ? 0.5 : 1.0;
                    break;

                case "highlightGlowThrough":
                    const highlightGlowThrough = this._configs[name] = parseBool(value);
                    const highlightMaterial = this.viewer.scene.highlightMaterial;
                    highlightMaterial.glowThrough = highlightGlowThrough;
                    highlightMaterial.fillAlpha = highlightGlowThrough ? 0.5 : 1.0;
                    highlightMaterial.edgeAlpha = highlightGlowThrough ? 0.5 : 1.0;
                    break;

                case "externalMetadata":
                    this._configs[name] = parseBool(value);
                    break;

                case "showSpaces":
                    this._configs[name] = parseBool(value);
                    this._showSpacesMode.setActive(value);
                    break;

                case "dtxEnabled":
                    this._configs[name] = parseBool(value);
                    this.viewer.scene.dtxEnabled = value;
                    break;

                case "objectColors":
                    this._configs[name] = value;
                    this._modelsExplorer.setObjectColors(value);
                    break;

                default:
                    this.warn("setConfig() - unsupported configuration: '" + name + "'");
            }

        } catch (e) {
            this.error("setConfig() - failed to configure '" + name + "': " + e);
        }
    }

    /**
     * Gets the value of a viewer configuration.
     *
     * These are set with {@link BIMViewer#setConfig} and {@link BIMViewer#setConfigs}.
     *
     * @param {String} name Configuration name.
     * @ereturns {*} Configuration value.
     */
    getConfig(name) {
        return this._configs[name];
    }

    //------------------------------------------------------------------------------------------------------------------
    // Content querying methods
    //------------------------------------------------------------------------------------------------------------------

    /**
     * Gets information on all available projects.
     * Gets information on all available projects.
     *
     * See [Getting Info on Available Projects](https://xeokit.github.io/xeokit-bim-viewer/docs/#getting-info-on-available-projects) for usage.
     *
     * @param {Function} done Callback invoked on success, into which the projects information JSON is passed.
     * @param {Function} error Callback invoked on failure, into which the error message string is passed.
     */
    getProjectsInfo(done, error) {
        if (!done) {
            this.error("getProjectsInfo() - Argument expected: 'done'");
            return;
        }
        this.server.getProjects(done, (errorMsg) => {
            this.error("getProjectsInfo() - " + errorMsg);
            if (error) {
                error(errorMsg);
            }
        });
    }

    /**
     * Gets information on the given project.
     *
     * See [Getting Info on a Project](https://xeokit.github.io/xeokit-bim-viewer/docs/#getting-info-on-a-project) for usage.
     *
     * @param {String} projectId ID of the project to get information on. Must be the ID of one of the projects in the information obtained by {@link BIMViewer#getProjects}.
     * @param {Function} done Callback invoked on success, into which the project information JSON is passed.
     * @param {Function} error Callback invoked on failure, into which the error message string is passed.
     */
    getProjectInfo(projectId, done, error) {
        if (!projectId) {
            this.error("getProjectInfo() - Argument expected: projectId");
            return;
        }
        if (!done) {
            this.error("getProjectInfo() - Argument expected: 'done'");
            return;
        }
        this.server.getProject(projectId,
            done, (errorMsg) => {
                this.error("getProjectInfo() - " + errorMsg);
                if (error) {
                    error(errorMsg);
                }
            });
    }

    /**
     * Gets information on the given object, belonging to the given model, within the given project.
     *
     * See [Getting Info on an Object](https://xeokit.github.io/xeokit-bim-viewer/docs/#getting-info-on-an-object) for usage.
     *
     * @param {String} projectId ID of the project to get information on. Must be the ID of one of the projects in the information obtained by {@link BIMViewer#getProjects}.
     * @param {String} modelId ID of a model within the project. Must be the ID of one of the models in the information obtained by {@link BIMViewer#getProjectInfo}.
     * @param {String} objectId ID of an object in the model.
     * @param {Function} done Callback invoked on success, into which the object information JSON is passed.
     * @param {Function} error Callback invoked on failure, into which the error message string is passed.
     */
    getObjectInfo(projectId, modelId, objectId, done, error) {
        if (!projectId) {
            this.error("getObjectInfo() - Argument expected: projectId");
            return;
        }
        if (!modelId) {
            this.error("getObjectInfo() - Argument expected: modelId");
            return;
        }
        if (!objectId) {
            this.error("getObjectInfo() - Argument expected: objectId");
            return;
        }
        if (!done) {
            this.error("getProjectInfo() - Argument expected: 'done'");
            return;
        }
        this.server.getObjectInfo(projectId, modelId, objectId,
            done,
            (errorMsg) => {
                if (error) {
                    error(errorMsg);
                }
            });
    }

    //------------------------------------------------------------------------------------------------------------------
    // Content loading methods
    //------------------------------------------------------------------------------------------------------------------

    /**
     * Loads a project into the viewer.
     *
     * Unloads any currently loaded project and its models first. If the given project is already loaded, will unload that project first.
     *
     * @param {String} projectId ID of the project to load. Must be the ID of one of the projects in the information obtained by {@link BIMViewer#getProjects}.
     * @param {Function} done Callback invoked on success.
     * @param {Function} error Callback invoked on failure, into which the error message string is passed.
     */
    loadProject(projectId, done, error) {
        if (!projectId) {
            this.error("loadProject() - Argument expected: objectId");
            return;
        }
        this._modelsExplorer.loadProject(projectId,
            () => {
                if (done) {
                    done();
                }
            }, (errorMsg) => {
                this.error("loadProject() - " + errorMsg);
                if (error) {
                    error(errorMsg);
                }
            });
    }

    /**
     * Unloads whatever project is currently loaded.
     */
    unloadProject() {
        this._modelsExplorer.unloadProject();
        this.openTab("models");
        this.setControlsEnabled(false); // For quick UI feedback
    }

    /**
     * Returns the ID of the currently loaded project, if any.
     *
     * @returns {String} The ID of the currently loaded project, otherwise ````null```` if no project is currently loaded.
     */
    getLoadedProjectId() {
        return this._modelsExplorer.getLoadedProjectId();
    }

    /**
     * Returns the IDs of the models in the currently loaded project.
     *
     * @returns {String[]} The IDs of the models in the currently loaded project.
     */
    getModelIds() {
        return this._modelsExplorer.getModelIds();
    }

    /**
     * Loads a model into the viewer.
     *
     * Assumes that the project containing the model is currently loaded.
     *
     * @param {String} modelId ID of the model to load. Must be the ID of one of the models in the currently loaded project.
     * @param {Function} done Callback invoked on success.
     * @param {Function} error Callback invoked on failure, into which the error message string is passed.
     */
    loadModel(modelId, done, error) {
        if (!modelId) {
            this.error("loadModel() - Argument expected: modelId");
            return;
        }
        this._modelsExplorer.loadModel(modelId,
            () => {
                if (done) {
                    done();
                }
            }, (errorMsg) => {
                this.error("loadModel() - " + errorMsg);
                if (error) {
                    error(errorMsg);
                }
            });
    }

    /**
     * Load all models in the currently loaded project.
     *
     * Doesn't reload any models that are currently loaded.
     *
     * @param {Function} done Callback invoked on successful loading of the models.
     */
    loadAllModels(done = function () {
    }) {
        const modelIds = this._modelsExplorer.getModelIds();
        const loadNextModel = (i, done2) => {
            if (i >= modelIds.length) {
                done2();
            } else {
                const modelId = modelIds[i];
                if (!this._modelsExplorer.isModelLoaded(modelId)) {
                    this._modelsExplorer.loadModel(modelId, () => {
                        loadNextModel(i + 1, done2);
                    }, (errorMsg) => {
                        this.error("loadAllModels() - " + errorMsg);
                        loadNextModel(i + 1, done2);
                    });
                } else {
                    loadNextModel(i + 1, done2);
                }
            }
        };
        loadNextModel(0, done);
    }

    /**
     * Returns the IDs of the currently loaded models, if any.
     *
     * @returns {String[]} The IDs of the currently loaded models, otherwise an empty array if no models are currently loaded.
     */
    getLoadedModelIds() {
        return this._modelsExplorer._getLoadedModelIds();
    }

    /**
     * Gets if the given model is loaded.
     *
     * @param {String} modelId ID of the model to check. Must be the ID of one of the models in the currently loaded project.
     * @returns {Boolean} True if the given model is loaded.
     */
    isModelLoaded(modelId) {
        if (!modelId) {
            this.error("unloadModel() - Argument expected: modelId");
            return;
        }
        return this._modelsExplorer.isModelLoaded(modelId);
    }

    /**
     * Unloads a model from the viewer.
     *
     * Does nothing if the model is not currently loaded.
     *
     * @param {String} modelId ID of the model to unload.
     */
    unloadModel(modelId) {
        if (!modelId) {
            this.error("unloadModel() - Argument expected: modelId");
            return;
        }
        this._modelsExplorer.unloadModel(modelId);
    }

    /**
     * Unloads all currently loaded models.
     */
    unloadAllModels() {
        this._modelsExplorer.unloadAllModels();
    }

    /**
     * Edits a model.
     *
     * Assumes that the project containing the model is currently loaded.
     *
     * @param {String} modelId ID of the model to edit. Must be the ID of one of the models in the currently loaded project.
     */
    editModel(modelId) {
        this.fire("editModel", {
            modelId: modelId
        });
    }

    /**
     * Deletes a model.
     *
     * Assumes that the project containing the model is currently loaded.
     *
     * @param {String} modelId ID of the model to delete. Must be the ID of one of the models in the currently loaded project.
     */
    deleteModel(modelId) {
        this.fire("deleteModel", {
            modelId: modelId
        });
    }

    /**
     * Adds a model.
     *
     */
    addModel() {
        this.fire("addModel", {});
    }

    /**
     * Sets the viewer's background color.
     *
     * @param {Number[]} rgbColor Three-element array of RGB values, each in range ````[0..1]````.
     */
    setBackgroundColor(rgbColor) {
        this.viewer.scene.canvas.backgroundColor = rgbColor;
    }

    /**
     * Sets where the colors for model objects will be loaded from.
     *
     * Options are:
     *
     * * "model" - (default) load colors from models, and
     * * "viewer" - load colors from the viewer's inbuilt table of colors for IFC types.
     *
     * This is "model" by default.
     *
     * @deprecated
     * @param {String} source Where colors will be loaded from - "model" or "viewer".
     */
    setObjectColorSource(source) {
        console.log("BIMViewer.setObjectColorSource() is now deprecated and no longer functional. By default, " +
            "BIMViewer.getObjectColorSource() will now always return the (formerly) default value of `model`.");
    }

    /**
     * Gets where the colors for model objects will be loaded from.
     *
     * This is "model" by default.
     *
     * @deprecated
     * @return {String} Where colors will be loaded from - "model" to get colors from the model, or "viewer" to get them from the viewer's built-in table of colors for IFC types.
     */
    getObjectColorSource() {
        return "model";
    }

    /**
     * Updates viewer UI state according to the properties in the given object.
     *
     * Note that, since some updates could be animated (e.g. flying the camera to fit objects to view) this
     * method optionally takes a callback, which it invokes after updating the UI.
     *
     * Also, this method is not to be confused with {@link BIMViewer#setConfigs}, which is used to batch-update various configurations and user preferences on the viewer.
     *
     * See [Viewer States](https://xeokit.github.io/xeokit-bim-viewer/docs/#viewer_states) for the list of states that may be batch-updated with this method.
     *
     * @param {Object} viewerState Specifies the viewer UI state updates.
     * @param {Function} done Callback invoked on successful update of the viewer states.
     */
    setViewerState(viewerState, done = () => {
    }) {
        if (viewerState.tabOpen) {
            this.openTab(viewerState.tabOpen);
        }
        if (viewerState.expandObjectsTree) {
            this._objectsExplorer.expandTreeViewToDepth(viewerState.expandObjectsTree);
        }
        if (viewerState.expandClassesTree) {
            this._classesExplorer.expandTreeViewToDepth(viewerState.expandClassesTree);
        }
        if (viewerState.expandStoreysTree) {
            this._storeysExplorer.expandTreeViewToDepth(viewerState.expandStoreysTree);
        }
        if (viewerState.setCamera) {
            this.setCamera(viewerState.setCamera);
        }
        this._parseSelectedStorey(viewerState, () => {
            this._parseThreeDMode(viewerState, () => {
                done();
            });
        });
    }

    _parseSelectedStorey(viewerState, done) {
        if (viewerState.selectedStorey) {
            this.selectStorey(viewerState.selectedStorey);
            done();
        } else {
            done();
        }
    }

    _parseThreeDMode(viewerState, done) {
        const activateThreeDMode = (viewerState.threeDActive !== false);
        this.set3DEnabled(activateThreeDMode, done);
    }

    /**
     * Highlights the given object in the tree views within the Objects, Classes and Storeys tabs.
     *
     * Also scrolls the object's node into view within each tree, then highlights it.
     *
     * De-highlights whatever node is currently highlighted in each of those trees.
     *
     * @param {String} objectId ID of the object
     */
    showObjectInExplorers(objectId) {
        if (!objectId) {
            this.error("showObjectInExplorers() - Argument expected: objectId");
            return;
        }
        this._objectsExplorer.showNodeInTreeView(objectId);
        this._classesExplorer.showNodeInTreeView(objectId);
        this._storeysExplorer.showNodeInTreeView(objectId);
        this.fire("openExplorer", {});
    }

    /**
     * De-highlights the object previously highlighted with {@link BIMViewer#showObjectInExplorers}.
     *
     * This only de-highlights the node. If the node is currently scrolled into view, then the node will remain in view.
     *
     * For each tab, does nothing if a node is currently highlighted.
     */
    unShowObjectInExplorers() {
        this._objectsExplorer.unShowNodeInTreeView();
        this._classesExplorer.unShowNodeInTreeView();
        this._storeysExplorer.unShowNodeInTreeView();
    }

    /**
     * Shows the properties of the given object in the Properties tab.
     *
     * @param {String} objectId ID of the object
     */
    showObjectProperties(objectId) {
        if (!objectId) {
            this.error("showObjectInExplorers() - Argument expected: objectId");
            return;
        }
        if (this._enablePropertiesInspector) {
            this._propertiesInspector.showObjectPropertySets(objectId);
        }
        this.fire("openInspector", {});
    }

    /**
     * Sets whether or not the given objects are visible.
     *
     * @param {String[]} objectIds IDs of objects.
     * @param {Boolean} visible True to set objects visible, false to set them invisible.
     */
    setObjectsVisible(objectIds, visible) {
        this._withObjectsInSubtree(objectIds, (entity) => {
            entity.visible = visible;
        });
    }

    /**
     * Sets the visibility of all objects.
     *
     * @param {Boolean} visible True to set objects visible, false to set them invisible.
     */
    setAllObjectsVisible(visible) {
        if (visible) {
            this.viewer.scene.setObjectsVisible(this.viewer.scene.objectIds, true);
        } else {
            this.viewer.scene.setObjectsVisible(this.viewer.scene.visibleObjectIds, false);
        }
    }

    /**
     * Sets whether or not the given objects are X-rayed.
     *
     * @param {String[]} objectIds IDs of objects.
     * @param {Boolean} xrayed Whether or not to X-ray the objects.
     */
    setObjectsXRayed(objectIds, xrayed) {
        this._withObjectsInSubtree(objectIds, (entity) => {
            entity.xrayed = xrayed;
        });
    }

    /**
     * Sets whether or not all objects are X-rayed.
     *
     * @param {Boolean} xrayed Whether or not to set all objects X-rayed.
     */
    setAllObjectsXRayed(xrayed) {
        if (xrayed) {
            this.viewer.scene.setObjectsXRayed(this.viewer.scene.objectIds, true);
        } else {
            this.viewer.scene.setObjectsXRayed(this.viewer.scene.xrayedObjectIds, false);
        }
    }

    /**
     * Sets whether or not the given objects are selected.
     *
     * @param {String[]} objectIds IDs of objects.
     * @param {Boolean} selected Whether or not to set the objects selected.
     */
    setObjectsSelected(objectIds, selected) {
        this._withObjectsInSubtree(objectIds, (entity) => {
            entity.selected = selected;
        });
    }

    /**
     * Sets whether or not all objects are selected.
     *
     * @param {Boolean} selected Whether or not to set all objects selected.
     */
    setAllObjectsSelected(selected) {
        if (selected) {
            this.viewer.scene.setObjectsSelected(this.viewer.scene.objectIds, true);
        } else {
            this.viewer.scene.setObjectsSelected(this.viewer.scene.selectedObjectIds, false);
        }
    }

    _withObjectsInSubtree(objectIds, callback) {
        if (!objectIds) {
            this.error("Argument expected: objectIds");
            return;
        }
        for (let i = 0, len = objectIds.length; i < len; i++) {
            const objectId = objectIds[i];
            this.viewer.metaScene.withMetaObjectsInSubtree(objectId, (metaObject) => {
                const entity = this.viewer.scene.objects[metaObject.id];
                if (entity) {
                    callback(entity);
                }
            });
        }
    }

    /**
     * Flies the camera to fit the given object in view.
     *
     * @param {String} objectId ID of the object
     * @param {Function} done Callback invoked on completion
     */
    flyToObject(objectId, done) {
        if (!objectId) {
            this.error("flyToObject() - Argument expected: objectId");
            return;
        }
        const viewer = this.viewer;
        const scene = viewer.scene;
        const objectIds = [];
        this.viewer.metaScene.withMetaObjectsInSubtree(objectId, (metaObject) => {
            if (scene.objects[metaObject.id]) {
                objectIds.push(metaObject.id);
            }
        });
        if (objectIds.length === 0) {
            this.error("Object not found in viewer: '" + objectId + "'");
            if (done) {
                done();
            }
            return;
        }
        scene.setObjectsVisible(objectIds, true);
        scene.setObjectsHighlighted(objectIds, true);
        const aabb = scene.getAABB(objectIds);
        viewer.cameraFlight.flyTo({
            aabb: aabb
        }, () => {
            if (done) {
                done();
            }
            setTimeout(function () {
                scene.setObjectsHighlighted(scene.highlightedObjectIds, false);
            }, 500);
        });
        viewer.cameraControl.pivotPos = math.getAABB3Center(aabb);
    }

    /**
     * Flies the camera to fit the given objects in view.
     *
     * @param {String[]} objectIds IDs of the objects
     * @param {Function} done Callback invoked on completion
     */
    viewFitObjects(objectIds, done) {
        if (!objectIds) {
            this.error("flyToObject() - Argument expected: objectIds");
            return;
        }
        const viewer = this.viewer;
        const scene = viewer.scene;

        const entityIds = [];

        for (var i = 0, len = objectIds.length; i < len; i++) {
            const objectId = objectIds[i];
            this.viewer.metaScene.withMetaObjectsInSubtree(objectId, (metaObject) => {
                if (scene.objects[metaObject.id]) {
                    entityIds.push(metaObject.id);
                }
            });
        }
        if (entityIds.length === 0) {
            if (done) {
                done();
            }
            return;
        }
        scene.setObjectsVisible(entityIds, true);
        scene.setObjectsHighlighted(entityIds, true);
        const aabb = scene.getAABB(entityIds);
        viewer.cameraFlight.flyTo({
            aabb: aabb
        }, () => {
            if (done) {
                done();
            }
            setTimeout(function () {
                scene.setObjectsHighlighted(scene.highlightedObjectIds, false);
            }, 500);
        });
        viewer.cameraControl.pivotPos = math.getAABB3Center(aabb);
    }

    /**
     * Flies the camera to fit all objects in view.
     *
     * @param {Function} done Callback invoked on completion
     */
    viewFitAll(done) {
        const viewer = this.viewer;
        const scene = viewer.scene;
        const aabb = scene.getAABB();
        viewer.cameraFlight.flyTo({
            aabb: aabb
        }, () => {
            if (done) {
                done();
            }
        });
        viewer.cameraControl.pivotPos = math.getAABB3Center(aabb);
    }

    /**
     * Jumps the camera to fit the given object in view.
     *
     * @param {String} objectId ID of the object
     */
    jumpToObject(objectId) {
        if (!objectId) {
            this.error("jumpToObject() - Argument expected: objectId");
            return;
        }
        const viewer = this.viewer;
        const scene = viewer.scene;
        const objectIds = [];
        this.viewer.metaScene.withMetaObjectsInSubtree(objectId, (metaObject) => {
            if (scene.objects[metaObject.id]) {
                objectIds.push(metaObject.id);
            }
        });
        if (objectIds.length === 0) {
            this.error("Object not found in viewer: '" + objectId + "'");
            return;
        }
        scene.setObjectsVisible(objectIds, true);
        const aabb = scene.getAABB(objectIds);
        viewer.cameraFlight.jumpTo({
            aabb: aabb
        });
        viewer.cameraControl.pivotPos = math.getAABB3Center(aabb);
    }

    /**
     * Sets the camera to the given position.
     *
     * @param {Number[]} [params.eye] Eye position.
     * @param {Number[]} [params.look] Point of interest.
     * @param {Number[]} [params.up] Direction of "up".
     */
    setCamera(params) {
        const viewer = this.viewer;
        const scene = viewer.scene;
        const camera = scene.camera;
        if (params.eye) {
            camera.eye = params.eye;
        }
        if (params.look) {
            camera.look = params.look;
        }
        if (params.up) {
            camera.up = params.up;
        }
    }

    /**
     * Fits the given models in view.
     *
     * @param {String[]} modelIds ID of the models.
     * @param {Function} [done] Callback invoked on completion. Will be animated if this is given, otherwise will be instantaneous.
     */
    viewFitModels(modelIds, done) {
        if (!modelIds) {
            this.error("viewFitModels() - Argument expected: modelIds");
            return;
        }
        const viewer = this.viewer;
        const scene = viewer.scene;
        const aabb = math.AABB3();
        math.collapseAABB3(aabb);
        for (var i = 0, len = modelIds.length; i < len; i++) {
            const modelId = modelIds[i];
            const model = scene.models[modelId];
            if (!model) {
                this.error("Model not found in viewer: '" + modelId + "'");
                continue;
            }
            model.visible = true;
            model.highlighted = true;
            math.expandAABB3(aabb, model.aabb);
        }
        if (done) {
            viewer.cameraFlight.flyTo({
                aabb: aabb
            }, () => {
                done();
                setTimeout(function () {
                    scene.setObjectsHighlighted(scene.highlightedObjectIds, false);
                }, 500);
            });
        } else {
            viewer.cameraFlight.jumpTo({
                aabb: aabb
            });
            setTimeout(function () {
                scene.setObjectsHighlighted(scene.highlightedObjectIds, false);
            }, 500);
        }
        viewer.cameraControl.pivotPos = math.getAABB3Center(aabb);
    }

    /**
     * Opens the specified viewer tab.
     *
     * The available tabs are:
     *
     *  * "models" - the Models tab, which lists the models available within the currently loaded project,
     *  * "objects" - the Objects tab, which contains a tree view for each loaded model, organized to indicate the containment hierarchy of their objects,
     *  * "classes" - the Classes tab, which contains a tree view for each loaded model, with nodes grouped by IFC types of their objects,
     *  * "storeys" - the Storeys tab, which contains a tree view for each loaded model, with nodes grouped within ````IfcBuildingStoreys````, sub-grouped by their IFC types, and
     *  * "properties" - the Properties tab, which shows property sets for a given object.
     *
     * @param {String} tabId ID of the tab to open - see method description.
     */
    openTab(tabId) {
        if (!tabId) {
            this.error("openTab() - Argument expected: tabId");
            return;
        }
        let tabSelector;
        switch (tabId) {
            case "models":
                tabSelector = "xeokit-modelsTab";
                break;
            case "objects":
                tabSelector = "xeokit-objectsTab";
                break;
            case "classes":
                tabSelector = "xeokit-classesTab";
                break;
            case "storeys":
                tabSelector = "xeokit-storeysTab";
                break;
            case "properties":
                tabSelector = "xeokit-propertiesTab";
                break;
            default:
                this.error("openTab() - tab not recognized: '" + tabId + "'");
                return;
        }
        this._openTab(this._explorerElement, tabSelector);
        //     this._openTab(this._inspectorElement, tabSelector);
    }

    _openTab(element, tabSelector) {
        const tabClass = 'xeokit-tab';
        const activeClass = 'active';
        let tabs = element.querySelectorAll("." + tabClass);
        let tab = element.querySelector("." + tabSelector);
        for (let i = 0; i < tabs.length; i++) {
            let tabElement = tabs[i];
            if (tabElement.isEqualNode(tab)) {
                tabElement.classList.add(activeClass);
            } else {
                tabElement.classList.remove(activeClass);
            }
        }
    }

    /**
     * Returns the ID of the currently open viewer tab.
     *
     * The available tabs are:
     *
     *  * "models" - the Models tab, which lists the models available within the currently loaded project,
     *  * "objects" - the Objects tab, which contains a tree view for each loaded model, organized to indicate the containment hierarchy of their objects,
     *  * "classes" - the Classes tab, which contains a tree view for each loaded model, with nodes grouped by IFC types of their objects,
     *  * "storeys" - the Storeys tab, which contains a tree view for each loaded model, with nodes grouped within ````IfcBuildingStoreys````, sub-grouped by their IFC types,
     *  * "properties" - the Properties tab, which shows property sets for a given object, and
     *  * "none" - no tab is open; this is unlikely, since one of the above tabs should be open at a any time, but here for robustness.
     */
    getOpenTab() {
        function hasClass(element, className) {
            if (!element) {
                return false;
            }
            return (" " + element.className + " ").indexOf(" " + className + " ") > -1;
        }

        const activeClass = 'active';
        let modelsTab = this._explorerElement.querySelector(".xeokit-modelsTab");
        if (hasClass(modelsTab, activeClass)) {
            return "models";
        }
        let objectsTab = this._explorerElement.querySelector(".xeokit-objectsTab");
        if (hasClass(objectsTab, activeClass)) {
            return "objects";
        }
        let classesTab = this._explorerElement.querySelector(".xeokit-classesTab");
        if (hasClass(classesTab, activeClass)) {
            return "classes";
        }
        let storeysTab = this._explorerElement.querySelector(".xeokit-storeysTab");
        if (hasClass(storeysTab, activeClass)) {
            return "storeys";
        }
        let propertiesTab = this._inspectorElement.querySelector(".xeokit-propertiesTab");
        if (hasClass(propertiesTab, activeClass)) {
            return "properties";
        }
        return "none";
    }

    /**
     * Switches the viewer between 2D and 3D viewing modes.
     *
     * @param {Boolean} enabled Set true to switch into 3D mode, else false to switch into 2D mode.
     * @param {Function} done Callback to invoke when switch complete. Supplying this callback causes an animated transition. Otherwise, the transition will be instant.
     */
    set3DEnabled(enabled, done) {
        this._threeDMode.setActive(enabled, done);
    }

    /**
     * Gets whether the viewer is in 3D or 2D viewing mode.
     *
     * @returns {boolean} True when in 3D mode, else false.
     */
    get3DEnabled() {
        return this._threeDMode.getActive();
    }

    /**
     * Sets whether IFCSpace types are ever shown.
     *
     * @param {Boolean} shown Set true to allow IFCSpaces to be shown, else false to always keep them hidden.
     */
    setSpacesShown(shown) {
        this._showSpacesMode.setActive(shown);
    }

    /**
     * Gets whether the viewer allows IFCSpace types to be shown.
     *
     * @returns {boolean} True to allow IFCSpaces to be shown, else false to always keep them hidden.
     */
    getSpacesShown() {
        return this._showSpacesMode.getActive();
    }

    /**
     * Sets whether the viewer is in orthographic viewing mode.
     *
     * The viewer is either in orthographic mode or perspective mode. The viewer is in perspective mode by default.
     *
     * @param {Boolean} enabled Set true to switch into ortho mode, else false to switch into perspective mode.
     * @param {Function} done Callback to invoke when switch complete. Supplying this callback causes an animated transition. Otherwise, the transition will be instant.
     */
    setOrthoEnabled(enabled, done) {
        this._orthoMode.setActive(enabled, done);
    }

    /**
     * Gets whether the viewer is in orthographic viewing mode.
     *
     * The viewer is either in orthographic mode or perspective mode. The viewer is in perspective mode by default.
     *
     * @returns {boolean} True when in ortho mode, else false when in perspective mode.
     */
    getOrthoEnabled() {
        return this._orthoMode.getActive();
    }

    /**
     * Transitions the viewer into an isolated view of the given building storey.
     *
     * Does nothing and logs an error if no object of the given ID is in the viewer, or if the object is not an ````IfcBuildingStorey````.
     *
     * @param {String} storeyObjectId ID of an ````IfcBuildingStorey```` object.
     * @param {Function} [done] Optional callback to invoke on completion. When provided, the transition will be animated, with the camera flying into position. Otherwise, the transition will be instant, with the camera jumping into position.
     */
    selectStorey(storeyObjectId, done) {
        const metaScene = this.viewer.metaScene;
        const storeyMetaObject = metaScene.metaObjects[storeyObjectId];
        if (!storeyMetaObject) {
            this.error("selectStorey() - Object is not found: '" + storeyObjectId + "'");
            return;
        }
        if (storeyMetaObject.type !== "IfcBuildingStorey") {
            this.error("selectStorey() - Object is not an IfcBuildingStorey: '" + storeyObjectId + "'");
            return;
        }
        this._storeysExplorer.selectStorey(storeyObjectId, done);
    }

    /**
     * Saves viewer state to a BCF viewpoint.
     *
     * This does not save information about the project and model(s) that are currently loaded. When loading the viewpoint,
     * the viewer will assume that the same project and models will be currently loaded (the BCF viewpoint specification
     * does not contain that information).
     *
     * Note that xeokit's {@link Camera#look} is the **point-of-interest**, whereas the BCF ````camera_direction```` is a
     * direction vector. Therefore, we save ````camera_direction```` as the vector from {@link Camera#eye} to {@link Camera#look}.
     *
     * @param {*} [options] Options for getting the viewpoint.
     * @param {Boolean} [options.spacesVisible=false] Indicates whether ````IfcSpace```` types should be forced visible in the viewpoint.
     * @param {Boolean} [options.openingsVisible=false] Indicates whether ````IfcOpening```` types should be forced visible in the viewpoint.
     * @param {Boolean} [options.spaceBoundariesVisible=false] Indicates whether the boundaries of ````IfcSpace```` types should be visible in the viewpoint.
     * @param {Boolean} [options.reverseClippingPlanes=false] When ````true````, clipping planes are reversed (https://github.com/buildingSMART/BCF-XML/issues/193)
     * @param {Boolean} [options.defaultInvisible=false] When ````true````, will save the default visibility of all objects
     * as ````false````. This means that when we load the viewpoint again, and there are additional models loaded that
     * were not saved in the viewpoint, those models will be hidden when we load the viewpoint, and that only the
     * objects in the viewpoint will be visible.
     * @returns {*} BCF JSON viewpoint object
     * @example
     *
     * const viewpoint = bimViewer.saveBCFViewpoint({
     *     spacesVisible: false,          // Default
     *     spaceBoundariesVisible: false, // Default
     *     openingsVisible: false         // Default
     * });
     *
     * // viewpoint will resemble the following:
     *
     * {
     *     perspective_camera: {
     *         camera_view_point: {
     *             x: 0.0,
     *             y: 0.0,
     *             z: 0.0
     *         },
     *         camera_direction: {
     *             x: 1.0,
     *             y: 1.0,
     *             z: 2.0
     *         },
     *         camera_up_vector: {
     *             x: 0.0,
     *             y: 0.0,
     *             z: 1.0
     *         },
     *         field_of_view: 90.0
     *     },
     *     lines: [],
     *     clipping_planes: [{
     *         location: {
     *             x: 0.5,
     *             y: 0.5,
     *             z: 0.5
     *         },
     *         direction: {
     *             x: 1.0,
     *             y: 0.0,
     *             z: 0.0
     *         }
     *     }],
     *     bitmaps: [],
     *     snapshot: {
     *         snapshot_type: png,
     *         snapshot_data: "data:image/png;base64,......"
     *     },
     *     components: {
     *         visibility: {
     *             default_visibility: false,
     *             exceptions: [{
     *                 ifc_guid: 4$cshxZO9AJBebsni$z9Yk,
     *                 originating_system: xeokit.io,
     *                 authoring_tool_id: xeokit/v1.0
     *             }]
     *        },
     *         selection: [{
     *            ifc_guid: "4$cshxZO9AJBebsni$z9Yk",
     *         }]
     *     }
     * }
     */
    saveBCFViewpoint(options) {
        return this._bcfViewpointsPlugin.getViewpoint(options);
    }

    /**
     * Sets viewer state to the given BCF viewpoint.
     *
     * This assumes that the viewer currently contains the same project and model(s) that were loaded at the time that the
     * viewpoint was originally saved (the BCF viewpoint specification does not contain that information).
     *
     * Note that xeokit's {@link Camera#look} is the **point-of-interest**, whereas the BCF ````camera_direction```` is a
     * direction vector. Therefore, when loading a BCF viewpoint, we set {@link Camera#look} to the absolute position
     * obtained by offsetting the BCF ````camera_view_point````  along ````camera_direction````.
     *
     * When loading a viewpoint, we also have the option to find {@link Camera#look} as the closest point of intersection
     * (on the surface of any visible and pickable {@link Entity}) with a 3D ray fired from ````camera_view_point```` in
     * the direction of ````camera_direction````.
     *
     * @param {*} bcfViewpoint  BCF JSON viewpoint object or "reset" / "RESET" to reset the viewer, which clears SectionPlanes,
     * shows default visible entities and restores camera to initial default position.
     * @param {*} [options] Options for setting the viewpoint.
     * @param {Boolean} [options.rayCast=true] When ````true```` (default), will attempt to set {@link Camera#look} to the closest
     * point of surface intersection with a ray fired from the BCF ````camera_view_point```` in the direction of ````camera_direction````.
     * @param {Boolean} [options.immediate=true] When ````true```` (default), immediately set camera position.
     * @param {Boolean} [options.duration] Flight duration in seconds.  Overrides {@link CameraFlightAnimation#duration}.
     * @param {Boolean} [options.reset=true] When ````true```` (default), set {@link Entity#xrayed} and {@link Entity#highlighted} ````false```` on all scene objects.
     * @param {Boolean} [options.reverseClippingPlanes=false] When ````true````, clipping planes are reversed (https://github.com/buildingSMART/BCF-XML/issues/193)
     * @param {Boolean} [options.updateCompositeObjects=false] When ````true````, then when visibility and selection updates refer to composite objects (eg. an IfcBuildingStorey),
     * then this method will apply the updates to objects within those composites.
     */
    loadBCFViewpoint(bcfViewpoint, options) {
        if (!bcfViewpoint) {
            this.error("loadBCFViewpoint() - Argument expected: bcfViewpoint");
            return;
        }
        this._orthoMode.setActive(this.viewer.camera.projection === "ortho");
        this._bcfViewpointsPlugin.setViewpoint(bcfViewpoint, options);
    }

    /**
     * Resets the view.
     *
     * This resets object appearances (visibility, selection, highlight and X-ray), sets camera to
     * default position, and removes section planes.
     */
    resetView() {
        this._resetAction.reset();
    }

    /**
     * Enables or disables the various buttons and controls throughout the viewer.
     *
     * This also makes various buttons appear disabled.
     *
     * @param {Boolean} enabled Whether or not to disable the controls.
     */
    setControlsEnabled(enabled) {

        // Explorer

        // Models tab is always enabled
        this._objectsExplorer.setEnabled(enabled);
        this._classesExplorer.setEnabled(enabled);
        this._storeysExplorer.setEnabled(enabled);

        // Toolbar

        this._resetAction.setEnabled(enabled);
        this._fitAction.setEnabled(enabled);
        this._threeDMode.setEnabled(enabled);
        this._orthoMode.setEnabled(enabled);
        this._firstPersonMode.setEnabled(enabled);
        this._queryTool.setEnabled(enabled);
        this._hideTool.setEnabled(enabled);
        this._selectionTool.setEnabled(enabled);
        this._marqueeSelectionTool.setEnabled(enabled);
        this._showSpacesMode.setEnabled(enabled);
        if (this._enableMeasurements) {
            this._measureDistanceTool.setEnabled(enabled);
            this._measureAngleTool.setEnabled(enabled);
        }
        this._sectionTool.setEnabled(enabled);

        if (this._enablePropertiesInspector) {
            this._propertiesInspector.setEnabled(enabled);
        }
    }

    /**
     * Sets whether or not keyboard camera control is enabled.
     *
     * This is useful when we don't want key events over the canvas to clash with other UI elements outside the canvas.
     *
     * Default value is ````true````.
     *
     * @param {Boolean} enabled Set ````true```` to enable keyboard input.
     */
    setKeyboardEnabled(enabled) {
        this.viewer.scene.input.keyboardEnabled = enabled;
    }

    /**
     * Gets whether keyboard camera control is enabled.
     *
     * This is useful when we don't want key events over the canvas to clash with other UI elements outside the canvas.
     *
     * Default value is ````true````.
     *
     * @returns {Boolean} Returns ````true```` if keyboard input is enabled.
     */
    getKeyboardEnabled() {
        return this.viewer.scene.input.keyboardEnabled;
    }

    /**
     * Clears sections.
     *
     * Sections are the slicing planes, that we use to section models in order to see interior structures.
     */
    clearSections() {
        this._sectionTool.clear();
    }

    /**
     * Disables all sections.
     */
    disableSections() {
        this._sectionTool.disableSections();
    }

    /**
     * Enables all sections.
     */
    enableSections() {
        this._sectionTool.enableSections();
    }

    /**
     * Inverts the direction of sections.
     */
    flipSections() {
        this._sectionTool.flipSections();
    }

    /**
     * Hides the section edition control, if currently shown.
     */
    hideSectionEditControl() {
        this._sectionTool.hideControl();
    }

    /**
     * Returns the number of sections that currently exist.
     *
     * sections are the sliceing planes, that we use to slice models in order to see interior structures.
     *
     * @returns {Number} The number of sections.
     */
    getNumSections() {
        return this._sectionTool.getNumSections();
    }

    /**
     * Gets if measurements are enabled for this BIMViewer.
     * This is immutable and is set via the BIMViewer constructor.
     * @returns {boolean}
     */
    getEnableMeasurements() {
        return this._enableMeasurements;
    }

    /**
     * Clears measurements.
     */
    clearMeasurements() {
        if (this._enableMeasurements) {
            this._measureDistanceTool.clear();
            this._measureAngleTool.clear();
        }
    }

    /**
     * Returns the number of measurements that currently exist.
     *
     * @returns {Number} The number of measurements.
     */
    getNumMeasurements() {
        return this._measureDistanceTool.getNumMeasurements() + this._measureAngleTool.getNumMeasurements();
    }

    /**
     * Sets the visibility of axis lines for measurements.
     *
     * So far, this only includes distance measurements. This is not relevant for angle measurements.
     *
     * @param {Boolean} axisVisible Set `true` to show axis wires, else `false` to hide them.
     */
    setMeasurementsAxisVisible(axisVisible) {
        if (this._enableMeasurements) {
            this._measureDistanceTool.setMeasurementsAxisVisible(axisVisible);
        }
    }

    /**
     * Gets the visibility of axis lines for measurements.
     *
     * So far, this only includes distance measurements. This is not relevant for angle measurements.
     *
     * @returns {Boolean}  `true` if axis wires are visible, else `false` if hidden.
     */
    getMeasurementsAxisVisible() {
        return (this._enableMeasurements) ? this._measureDistanceTool.getMeasurementsAxisVisible() : false;
    }

    /**
     * Sets whether pointer snapping is enabled for measurements.
     *
     * @param {Boolean} snappingEnabled Set `true` to enable snapping, else `false` to disable.
     */
    setMeasurementsSnappingEnabled(snappingEnabled) {
        if (this._enableMeasurements) {
            this._measureDistanceTool.setSnappingEnabled(snappingEnabled);
        }
    }

    /**
     * Gets whether pointer snapping is enabled for measurements.
     *
     * @returns {Boolean} `true` if snapping is enabled, else `false` if disabled.
     */
    getMeasurementsSnappingEnabled() {
        return (this._enableMeasurements) ? this._measureDistanceTool.getSnappingEnabled() : false;
    }

    /**
     * Destroys the viewer, freeing all resources.
     */
    destroy() {
        this.viewer.destroy();
        this._bcfViewpointsPlugin.destroy();
        this._canvasContextMenu.destroy();
        this._objectContextMenu.destroy();
    }
}

var faRegularWoff = "data:font/woff2;base64, ";

var faSolidWoff = "data:font/woff2;base64, ";

var faBrandsWoff = "data:font/woff2;base64, ";

var faRegularTtf = "data:font/ttf;base64, ";

var faSolidTtf = "data:font/ttf;base64, ";

var faBrandsTtf = "data:font/ttf;base64, ";

var fontStyle = "/*!\n * Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com\n * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)\n * Copyright 2024 Fonticons, Inc.\n */\n.fa {\n  font-family: var(--fa-style-family, \"Font Awesome 6 Free\");\n  font-weight: var(--fa-style, 900); }\n\n.fas,\n.far,\n.fab,\n.fa-solid,\n.fa-regular,\n.fa-brands,\n.fa {\n  -moz-osx-font-smoothing: grayscale;\n  -webkit-font-smoothing: antialiased;\n  display: var(--fa-display, inline-block);\n  font-style: normal;\n  font-variant: normal;\n  line-height: 1;\n  text-rendering: auto; }\n\n.fas::before,\n.far::before,\n.fab::before,\n.fa-solid::before,\n.fa-regular::before,\n.fa-brands::before,\n.fa::before {\n  content: var(--fa); }\n\n.fa-classic,\n.fas,\n.fa-solid,\n.far,\n.fa-regular {\n  font-family: 'Font Awesome 6 Free'; }\n\n.fa-brands,\n.fab {\n  font-family: 'Font Awesome 6 Brands'; }\n\n.fa-1x {\n  font-size: 1em; }\n\n.fa-2x {\n  font-size: 2em; }\n\n.fa-3x {\n  font-size: 3em; }\n\n.fa-4x {\n  font-size: 4em; }\n\n.fa-5x {\n  font-size: 5em; }\n\n.fa-6x {\n  font-size: 6em; }\n\n.fa-7x {\n  font-size: 7em; }\n\n.fa-8x {\n  font-size: 8em; }\n\n.fa-9x {\n  font-size: 9em; }\n\n.fa-10x {\n  font-size: 10em; }\n\n.fa-2xs {\n  font-size: 0.625em;\n  line-height: 0.1em;\n  vertical-align: 0.225em; }\n\n.fa-xs {\n  font-size: 0.75em;\n  line-height: 0.08333em;\n  vertical-align: 0.125em; }\n\n.fa-sm {\n  font-size: 0.875em;\n  line-height: 0.07143em;\n  vertical-align: 0.05357em; }\n\n.fa-lg {\n  font-size: 1.25em;\n  line-height: 0.05em;\n  vertical-align: -0.075em; }\n\n.fa-xl {\n  font-size: 1.5em;\n  line-height: 0.04167em;\n  vertical-align: -0.125em; }\n\n.fa-2xl {\n  font-size: 2em;\n  line-height: 0.03125em;\n  vertical-align: -0.1875em; }\n\n.fa-fw {\n  text-align: center;\n  width: 1.25em; }\n\n.fa-ul {\n  list-style-type: none;\n  margin-left: var(--fa-li-margin, 2.5em);\n  padding-left: 0; }\n  .fa-ul > li {\n    position: relative; }\n\n.fa-li {\n  left: calc(-1 * var(--fa-li-width, 2em));\n  position: absolute;\n  text-align: center;\n  width: var(--fa-li-width, 2em);\n  line-height: inherit; }\n\n.fa-border {\n  border-color: var(--fa-border-color, #eee);\n  border-radius: var(--fa-border-radius, 0.1em);\n  border-style: var(--fa-border-style, solid);\n  border-width: var(--fa-border-width, 0.08em);\n  padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); }\n\n.fa-pull-left {\n  float: left;\n  margin-right: var(--fa-pull-margin, 0.3em); }\n\n.fa-pull-right {\n  float: right;\n  margin-left: var(--fa-pull-margin, 0.3em); }\n\n.fa-beat {\n  animation-name: fa-beat;\n  animation-delay: var(--fa-animation-delay, 0s);\n  animation-direction: var(--fa-animation-direction, normal);\n  animation-duration: var(--fa-animation-duration, 1s);\n  animation-iteration-count: var(--fa-animation-iteration-count, infinite);\n  animation-timing-function: var(--fa-animation-timing, ease-in-out); }\n\n.fa-bounce {\n  animation-name: fa-bounce;\n  animation-delay: var(--fa-animation-delay, 0s);\n  animation-direction: var(--fa-animation-direction, normal);\n  animation-duration: var(--fa-animation-duration, 1s);\n  animation-iteration-count: var(--fa-animation-iteration-count, infinite);\n  animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); }\n\n.fa-fade {\n  animation-name: fa-fade;\n  animation-delay: var(--fa-animation-delay, 0s);\n  animation-direction: var(--fa-animation-direction, normal);\n  animation-duration: var(--fa-animation-duration, 1s);\n  animation-iteration-count: var(--fa-animation-iteration-count, infinite);\n  animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }\n\n.fa-beat-fade {\n  animation-name: fa-beat-fade;\n  animation-delay: var(--fa-animation-delay, 0s);\n  animation-direction: var(--fa-animation-direction, normal);\n  animation-duration: var(--fa-animation-duration, 1s);\n  animation-iteration-count: var(--fa-animation-iteration-count, infinite);\n  animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }\n\n.fa-flip {\n  animation-name: fa-flip;\n  animation-delay: var(--fa-animation-delay, 0s);\n  animation-direction: var(--fa-animation-direction, normal);\n  animation-duration: var(--fa-animation-duration, 1s);\n  animation-iteration-count: var(--fa-animation-iteration-count, infinite);\n  animation-timing-function: var(--fa-animation-timing, ease-in-out); }\n\n.fa-shake {\n  animation-name: fa-shake;\n  animation-delay: var(--fa-animation-delay, 0s);\n  animation-direction: var(--fa-animation-direction, normal);\n  animation-duration: var(--fa-animation-duration, 1s);\n  animation-iteration-count: var(--fa-animation-iteration-count, infinite);\n  animation-timing-function: var(--fa-animation-timing, linear); }\n\n.fa-spin {\n  animation-name: fa-spin;\n  animation-delay: var(--fa-animation-delay, 0s);\n  animation-direction: var(--fa-animation-direction, normal);\n  animation-duration: var(--fa-animation-duration, 2s);\n  animation-iteration-count: var(--fa-animation-iteration-count, infinite);\n  animation-timing-function: var(--fa-animation-timing, linear); }\n\n.fa-spin-reverse {\n  --fa-animation-direction: reverse; }\n\n.fa-pulse,\n.fa-spin-pulse {\n  animation-name: fa-spin;\n  animation-direction: var(--fa-animation-direction, normal);\n  animation-duration: var(--fa-animation-duration, 1s);\n  animation-iteration-count: var(--fa-animation-iteration-count, infinite);\n  animation-timing-function: var(--fa-animation-timing, steps(8)); }\n\n@media (prefers-reduced-motion: reduce) {\n  .fa-beat,\n  .fa-bounce,\n  .fa-fade,\n  .fa-beat-fade,\n  .fa-flip,\n  .fa-pulse,\n  .fa-shake,\n  .fa-spin,\n  .fa-spin-pulse {\n    animation-delay: -1ms;\n    animation-duration: 1ms;\n    animation-iteration-count: 1;\n    transition-delay: 0s;\n    transition-duration: 0s; } }\n\n@keyframes fa-beat {\n  0%, 90% {\n    transform: scale(1); }\n  45% {\n    transform: scale(var(--fa-beat-scale, 1.25)); } }\n\n@keyframes fa-bounce {\n  0% {\n    transform: scale(1, 1) translateY(0); }\n  10% {\n    transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }\n  30% {\n    transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }\n  50% {\n    transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }\n  57% {\n    transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }\n  64% {\n    transform: scale(1, 1) translateY(0); }\n  100% {\n    transform: scale(1, 1) translateY(0); } }\n\n@keyframes fa-fade {\n  50% {\n    opacity: var(--fa-fade-opacity, 0.4); } }\n\n@keyframes fa-beat-fade {\n  0%, 100% {\n    opacity: var(--fa-beat-fade-opacity, 0.4);\n    transform: scale(1); }\n  50% {\n    opacity: 1;\n    transform: scale(var(--fa-beat-fade-scale, 1.125)); } }\n\n@keyframes fa-flip {\n  50% {\n    transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }\n\n@keyframes fa-shake {\n  0% {\n    transform: rotate(-15deg); }\n  4% {\n    transform: rotate(15deg); }\n  8%, 24% {\n    transform: rotate(-18deg); }\n  12%, 28% {\n    transform: rotate(18deg); }\n  16% {\n    transform: rotate(-22deg); }\n  20% {\n    transform: rotate(22deg); }\n  32% {\n    transform: rotate(-12deg); }\n  36% {\n    transform: rotate(12deg); }\n  40%, 100% {\n    transform: rotate(0deg); } }\n\n@keyframes fa-spin {\n  0% {\n    transform: rotate(0deg); }\n  100% {\n    transform: rotate(360deg); } }\n\n.fa-rotate-90 {\n  transform: rotate(90deg); }\n\n.fa-rotate-180 {\n  transform: rotate(180deg); }\n\n.fa-rotate-270 {\n  transform: rotate(270deg); }\n\n.fa-flip-horizontal {\n  transform: scale(-1, 1); }\n\n.fa-flip-vertical {\n  transform: scale(1, -1); }\n\n.fa-flip-both,\n.fa-flip-horizontal.fa-flip-vertical {\n  transform: scale(-1, -1); }\n\n.fa-rotate-by {\n  transform: rotate(var(--fa-rotate-angle, 0)); }\n\n.fa-stack {\n  display: inline-block;\n  height: 2em;\n  line-height: 2em;\n  position: relative;\n  vertical-align: middle;\n  width: 2.5em; }\n\n.fa-stack-1x,\n.fa-stack-2x {\n  left: 0;\n  position: absolute;\n  text-align: center;\n  width: 100%;\n  z-index: var(--fa-stack-z-index, auto); }\n\n.fa-stack-1x {\n  line-height: inherit; }\n\n.fa-stack-2x {\n  font-size: 2em; }\n\n.fa-inverse {\n  color: var(--fa-inverse, #fff); }\n\n/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen\nreaders do not read off random characters that represent icons */\n\n.fa-0 {\n  --fa: \"\\30\"; }\n\n.fa-1 {\n  --fa: \"\\31\"; }\n\n.fa-2 {\n  --fa: \"\\32\"; }\n\n.fa-3 {\n  --fa: \"\\33\"; }\n\n.fa-4 {\n  --fa: \"\\34\"; }\n\n.fa-5 {\n  --fa: \"\\35\"; }\n\n.fa-6 {\n  --fa: \"\\36\"; }\n\n.fa-7 {\n  --fa: \"\\37\"; }\n\n.fa-8 {\n  --fa: \"\\38\"; }\n\n.fa-9 {\n  --fa: \"\\39\"; }\n\n.fa-fill-drip {\n  --fa: \"\\f576\"; }\n\n.fa-arrows-to-circle {\n  --fa: \"\\e4bd\"; }\n\n.fa-circle-chevron-right {\n  --fa: \"\\f138\"; }\n\n.fa-chevron-circle-right {\n  --fa: \"\\f138\"; }\n\n.fa-at {\n  --fa: \"\\40\"; }\n\n.fa-trash-can {\n  --fa: \"\\f2ed\"; }\n\n.fa-trash-alt {\n  --fa: \"\\f2ed\"; }\n\n.fa-text-height {\n  --fa: \"\\f034\"; }\n\n.fa-user-xmark {\n  --fa: \"\\f235\"; }\n\n.fa-user-times {\n  --fa: \"\\f235\"; }\n\n.fa-stethoscope {\n  --fa: \"\\f0f1\"; }\n\n.fa-message {\n  --fa: \"\\f27a\"; }\n\n.fa-comment-alt {\n  --fa: \"\\f27a\"; }\n\n.fa-info {\n  --fa: \"\\f129\"; }\n\n.fa-down-left-and-up-right-to-center {\n  --fa: \"\\f422\"; }\n\n.fa-compress-alt {\n  --fa: \"\\f422\"; }\n\n.fa-explosion {\n  --fa: \"\\e4e9\"; }\n\n.fa-file-lines {\n  --fa: \"\\f15c\"; }\n\n.fa-file-alt {\n  --fa: \"\\f15c\"; }\n\n.fa-file-text {\n  --fa: \"\\f15c\"; }\n\n.fa-wave-square {\n  --fa: \"\\f83e\"; }\n\n.fa-ring {\n  --fa: \"\\f70b\"; }\n\n.fa-building-un {\n  --fa: \"\\e4d9\"; }\n\n.fa-dice-three {\n  --fa: \"\\f527\"; }\n\n.fa-calendar-days {\n  --fa: \"\\f073\"; }\n\n.fa-calendar-alt {\n  --fa: \"\\f073\"; }\n\n.fa-anchor-circle-check {\n  --fa: \"\\e4aa\"; }\n\n.fa-building-circle-arrow-right {\n  --fa: \"\\e4d1\"; }\n\n.fa-volleyball {\n  --fa: \"\\f45f\"; }\n\n.fa-volleyball-ball {\n  --fa: \"\\f45f\"; }\n\n.fa-arrows-up-to-line {\n  --fa: \"\\e4c2\"; }\n\n.fa-sort-down {\n  --fa: \"\\f0dd\"; }\n\n.fa-sort-desc {\n  --fa: \"\\f0dd\"; }\n\n.fa-circle-minus {\n  --fa: \"\\f056\"; }\n\n.fa-minus-circle {\n  --fa: \"\\f056\"; }\n\n.fa-door-open {\n  --fa: \"\\f52b\"; }\n\n.fa-right-from-bracket {\n  --fa: \"\\f2f5\"; }\n\n.fa-sign-out-alt {\n  --fa: \"\\f2f5\"; }\n\n.fa-atom {\n  --fa: \"\\f5d2\"; }\n\n.fa-soap {\n  --fa: \"\\e06e\"; }\n\n.fa-icons {\n  --fa: \"\\f86d\"; }\n\n.fa-heart-music-camera-bolt {\n  --fa: \"\\f86d\"; }\n\n.fa-microphone-lines-slash {\n  --fa: \"\\f539\"; }\n\n.fa-microphone-alt-slash {\n  --fa: \"\\f539\"; }\n\n.fa-bridge-circle-check {\n  --fa: \"\\e4c9\"; }\n\n.fa-pump-medical {\n  --fa: \"\\e06a\"; }\n\n.fa-fingerprint {\n  --fa: \"\\f577\"; }\n\n.fa-hand-point-right {\n  --fa: \"\\f0a4\"; }\n\n.fa-magnifying-glass-location {\n  --fa: \"\\f689\"; }\n\n.fa-search-location {\n  --fa: \"\\f689\"; }\n\n.fa-forward-step {\n  --fa: \"\\f051\"; }\n\n.fa-step-forward {\n  --fa: \"\\f051\"; }\n\n.fa-face-smile-beam {\n  --fa: \"\\f5b8\"; }\n\n.fa-smile-beam {\n  --fa: \"\\f5b8\"; }\n\n.fa-flag-checkered {\n  --fa: \"\\f11e\"; }\n\n.fa-football {\n  --fa: \"\\f44e\"; }\n\n.fa-football-ball {\n  --fa: \"\\f44e\"; }\n\n.fa-school-circle-exclamation {\n  --fa: \"\\e56c\"; }\n\n.fa-crop {\n  --fa: \"\\f125\"; }\n\n.fa-angles-down {\n  --fa: \"\\f103\"; }\n\n.fa-angle-double-down {\n  --fa: \"\\f103\"; }\n\n.fa-users-rectangle {\n  --fa: \"\\e594\"; }\n\n.fa-people-roof {\n  --fa: \"\\e537\"; }\n\n.fa-people-line {\n  --fa: \"\\e534\"; }\n\n.fa-beer-mug-empty {\n  --fa: \"\\f0fc\"; }\n\n.fa-beer {\n  --fa: \"\\f0fc\"; }\n\n.fa-diagram-predecessor {\n  --fa: \"\\e477\"; }\n\n.fa-arrow-up-long {\n  --fa: \"\\f176\"; }\n\n.fa-long-arrow-up {\n  --fa: \"\\f176\"; }\n\n.fa-fire-flame-simple {\n  --fa: \"\\f46a\"; }\n\n.fa-burn {\n  --fa: \"\\f46a\"; }\n\n.fa-person {\n  --fa: \"\\f183\"; }\n\n.fa-male {\n  --fa: \"\\f183\"; }\n\n.fa-laptop {\n  --fa: \"\\f109\"; }\n\n.fa-file-csv {\n  --fa: \"\\f6dd\"; }\n\n.fa-menorah {\n  --fa: \"\\f676\"; }\n\n.fa-truck-plane {\n  --fa: \"\\e58f\"; }\n\n.fa-record-vinyl {\n  --fa: \"\\f8d9\"; }\n\n.fa-face-grin-stars {\n  --fa: \"\\f587\"; }\n\n.fa-grin-stars {\n  --fa: \"\\f587\"; }\n\n.fa-bong {\n  --fa: \"\\f55c\"; }\n\n.fa-spaghetti-monster-flying {\n  --fa: \"\\f67b\"; }\n\n.fa-pastafarianism {\n  --fa: \"\\f67b\"; }\n\n.fa-arrow-down-up-across-line {\n  --fa: \"\\e4af\"; }\n\n.fa-spoon {\n  --fa: \"\\f2e5\"; }\n\n.fa-utensil-spoon {\n  --fa: \"\\f2e5\"; }\n\n.fa-jar-wheat {\n  --fa: \"\\e517\"; }\n\n.fa-envelopes-bulk {\n  --fa: \"\\f674\"; }\n\n.fa-mail-bulk {\n  --fa: \"\\f674\"; }\n\n.fa-file-circle-exclamation {\n  --fa: \"\\e4eb\"; }\n\n.fa-circle-h {\n  --fa: \"\\f47e\"; }\n\n.fa-hospital-symbol {\n  --fa: \"\\f47e\"; }\n\n.fa-pager {\n  --fa: \"\\f815\"; }\n\n.fa-address-book {\n  --fa: \"\\f2b9\"; }\n\n.fa-contact-book {\n  --fa: \"\\f2b9\"; }\n\n.fa-strikethrough {\n  --fa: \"\\f0cc\"; }\n\n.fa-k {\n  --fa: \"\\4b\"; }\n\n.fa-landmark-flag {\n  --fa: \"\\e51c\"; }\n\n.fa-pencil {\n  --fa: \"\\f303\"; }\n\n.fa-pencil-alt {\n  --fa: \"\\f303\"; }\n\n.fa-backward {\n  --fa: \"\\f04a\"; }\n\n.fa-caret-right {\n  --fa: \"\\f0da\"; }\n\n.fa-comments {\n  --fa: \"\\f086\"; }\n\n.fa-paste {\n  --fa: \"\\f0ea\"; }\n\n.fa-file-clipboard {\n  --fa: \"\\f0ea\"; }\n\n.fa-code-pull-request {\n  --fa: \"\\e13c\"; }\n\n.fa-clipboard-list {\n  --fa: \"\\f46d\"; }\n\n.fa-truck-ramp-box {\n  --fa: \"\\f4de\"; }\n\n.fa-truck-loading {\n  --fa: \"\\f4de\"; }\n\n.fa-user-check {\n  --fa: \"\\f4fc\"; }\n\n.fa-vial-virus {\n  --fa: \"\\e597\"; }\n\n.fa-sheet-plastic {\n  --fa: \"\\e571\"; }\n\n.fa-blog {\n  --fa: \"\\f781\"; }\n\n.fa-user-ninja {\n  --fa: \"\\f504\"; }\n\n.fa-person-arrow-up-from-line {\n  --fa: \"\\e539\"; }\n\n.fa-scroll-torah {\n  --fa: \"\\f6a0\"; }\n\n.fa-torah {\n  --fa: \"\\f6a0\"; }\n\n.fa-broom-ball {\n  --fa: \"\\f458\"; }\n\n.fa-quidditch {\n  --fa: \"\\f458\"; }\n\n.fa-quidditch-broom-ball {\n  --fa: \"\\f458\"; }\n\n.fa-toggle-off {\n  --fa: \"\\f204\"; }\n\n.fa-box-archive {\n  --fa: \"\\f187\"; }\n\n.fa-archive {\n  --fa: \"\\f187\"; }\n\n.fa-person-drowning {\n  --fa: \"\\e545\"; }\n\n.fa-arrow-down-9-1 {\n  --fa: \"\\f886\"; }\n\n.fa-sort-numeric-desc {\n  --fa: \"\\f886\"; }\n\n.fa-sort-numeric-down-alt {\n  --fa: \"\\f886\"; }\n\n.fa-face-grin-tongue-squint {\n  --fa: \"\\f58a\"; }\n\n.fa-grin-tongue-squint {\n  --fa: \"\\f58a\"; }\n\n.fa-spray-can {\n  --fa: \"\\f5bd\"; }\n\n.fa-truck-monster {\n  --fa: \"\\f63b\"; }\n\n.fa-w {\n  --fa: \"\\57\"; }\n\n.fa-earth-africa {\n  --fa: \"\\f57c\"; }\n\n.fa-globe-africa {\n  --fa: \"\\f57c\"; }\n\n.fa-rainbow {\n  --fa: \"\\f75b\"; }\n\n.fa-circle-notch {\n  --fa: \"\\f1ce\"; }\n\n.fa-tablet-screen-button {\n  --fa: \"\\f3fa\"; }\n\n.fa-tablet-alt {\n  --fa: \"\\f3fa\"; }\n\n.fa-paw {\n  --fa: \"\\f1b0\"; }\n\n.fa-cloud {\n  --fa: \"\\f0c2\"; }\n\n.fa-trowel-bricks {\n  --fa: \"\\e58a\"; }\n\n.fa-face-flushed {\n  --fa: \"\\f579\"; }\n\n.fa-flushed {\n  --fa: \"\\f579\"; }\n\n.fa-hospital-user {\n  --fa: \"\\f80d\"; }\n\n.fa-tent-arrow-left-right {\n  --fa: \"\\e57f\"; }\n\n.fa-gavel {\n  --fa: \"\\f0e3\"; }\n\n.fa-legal {\n  --fa: \"\\f0e3\"; }\n\n.fa-binoculars {\n  --fa: \"\\f1e5\"; }\n\n.fa-microphone-slash {\n  --fa: \"\\f131\"; }\n\n.fa-box-tissue {\n  --fa: \"\\e05b\"; }\n\n.fa-motorcycle {\n  --fa: \"\\f21c\"; }\n\n.fa-bell-concierge {\n  --fa: \"\\f562\"; }\n\n.fa-concierge-bell {\n  --fa: \"\\f562\"; }\n\n.fa-pen-ruler {\n  --fa: \"\\f5ae\"; }\n\n.fa-pencil-ruler {\n  --fa: \"\\f5ae\"; }\n\n.fa-people-arrows {\n  --fa: \"\\e068\"; }\n\n.fa-people-arrows-left-right {\n  --fa: \"\\e068\"; }\n\n.fa-mars-and-venus-burst {\n  --fa: \"\\e523\"; }\n\n.fa-square-caret-right {\n  --fa: \"\\f152\"; }\n\n.fa-caret-square-right {\n  --fa: \"\\f152\"; }\n\n.fa-scissors {\n  --fa: \"\\f0c4\"; }\n\n.fa-cut {\n  --fa: \"\\f0c4\"; }\n\n.fa-sun-plant-wilt {\n  --fa: \"\\e57a\"; }\n\n.fa-toilets-portable {\n  --fa: \"\\e584\"; }\n\n.fa-hockey-puck {\n  --fa: \"\\f453\"; }\n\n.fa-table {\n  --fa: \"\\f0ce\"; }\n\n.fa-magnifying-glass-arrow-right {\n  --fa: \"\\e521\"; }\n\n.fa-tachograph-digital {\n  --fa: \"\\f566\"; }\n\n.fa-digital-tachograph {\n  --fa: \"\\f566\"; }\n\n.fa-users-slash {\n  --fa: \"\\e073\"; }\n\n.fa-clover {\n  --fa: \"\\e139\"; }\n\n.fa-reply {\n  --fa: \"\\f3e5\"; }\n\n.fa-mail-reply {\n  --fa: \"\\f3e5\"; }\n\n.fa-star-and-crescent {\n  --fa: \"\\f699\"; }\n\n.fa-house-fire {\n  --fa: \"\\e50c\"; }\n\n.fa-square-minus {\n  --fa: \"\\f146\"; }\n\n.fa-minus-square {\n  --fa: \"\\f146\"; }\n\n.fa-helicopter {\n  --fa: \"\\f533\"; }\n\n.fa-compass {\n  --fa: \"\\f14e\"; }\n\n.fa-square-caret-down {\n  --fa: \"\\f150\"; }\n\n.fa-caret-square-down {\n  --fa: \"\\f150\"; }\n\n.fa-file-circle-question {\n  --fa: \"\\e4ef\"; }\n\n.fa-laptop-code {\n  --fa: \"\\f5fc\"; }\n\n.fa-swatchbook {\n  --fa: \"\\f5c3\"; }\n\n.fa-prescription-bottle {\n  --fa: \"\\f485\"; }\n\n.fa-bars {\n  --fa: \"\\f0c9\"; }\n\n.fa-navicon {\n  --fa: \"\\f0c9\"; }\n\n.fa-people-group {\n  --fa: \"\\e533\"; }\n\n.fa-hourglass-end {\n  --fa: \"\\f253\"; }\n\n.fa-hourglass-3 {\n  --fa: \"\\f253\"; }\n\n.fa-heart-crack {\n  --fa: \"\\f7a9\"; }\n\n.fa-heart-broken {\n  --fa: \"\\f7a9\"; }\n\n.fa-square-up-right {\n  --fa: \"\\f360\"; }\n\n.fa-external-link-square-alt {\n  --fa: \"\\f360\"; }\n\n.fa-face-kiss-beam {\n  --fa: \"\\f597\"; }\n\n.fa-kiss-beam {\n  --fa: \"\\f597\"; }\n\n.fa-film {\n  --fa: \"\\f008\"; }\n\n.fa-ruler-horizontal {\n  --fa: \"\\f547\"; }\n\n.fa-people-robbery {\n  --fa: \"\\e536\"; }\n\n.fa-lightbulb {\n  --fa: \"\\f0eb\"; }\n\n.fa-caret-left {\n  --fa: \"\\f0d9\"; }\n\n.fa-circle-exclamation {\n  --fa: \"\\f06a\"; }\n\n.fa-exclamation-circle {\n  --fa: \"\\f06a\"; }\n\n.fa-school-circle-xmark {\n  --fa: \"\\e56d\"; }\n\n.fa-arrow-right-from-bracket {\n  --fa: \"\\f08b\"; }\n\n.fa-sign-out {\n  --fa: \"\\f08b\"; }\n\n.fa-circle-chevron-down {\n  --fa: \"\\f13a\"; }\n\n.fa-chevron-circle-down {\n  --fa: \"\\f13a\"; }\n\n.fa-unlock-keyhole {\n  --fa: \"\\f13e\"; }\n\n.fa-unlock-alt {\n  --fa: \"\\f13e\"; }\n\n.fa-cloud-showers-heavy {\n  --fa: \"\\f740\"; }\n\n.fa-headphones-simple {\n  --fa: \"\\f58f\"; }\n\n.fa-headphones-alt {\n  --fa: \"\\f58f\"; }\n\n.fa-sitemap {\n  --fa: \"\\f0e8\"; }\n\n.fa-circle-dollar-to-slot {\n  --fa: \"\\f4b9\"; }\n\n.fa-donate {\n  --fa: \"\\f4b9\"; }\n\n.fa-memory {\n  --fa: \"\\f538\"; }\n\n.fa-road-spikes {\n  --fa: \"\\e568\"; }\n\n.fa-fire-burner {\n  --fa: \"\\e4f1\"; }\n\n.fa-flag {\n  --fa: \"\\f024\"; }\n\n.fa-hanukiah {\n  --fa: \"\\f6e6\"; }\n\n.fa-feather {\n  --fa: \"\\f52d\"; }\n\n.fa-volume-low {\n  --fa: \"\\f027\"; }\n\n.fa-volume-down {\n  --fa: \"\\f027\"; }\n\n.fa-comment-slash {\n  --fa: \"\\f4b3\"; }\n\n.fa-cloud-sun-rain {\n  --fa: \"\\f743\"; }\n\n.fa-compress {\n  --fa: \"\\f066\"; }\n\n.fa-wheat-awn {\n  --fa: \"\\e2cd\"; }\n\n.fa-wheat-alt {\n  --fa: \"\\e2cd\"; }\n\n.fa-ankh {\n  --fa: \"\\f644\"; }\n\n.fa-hands-holding-child {\n  --fa: \"\\e4fa\"; }\n\n.fa-asterisk {\n  --fa: \"\\2a\"; }\n\n.fa-square-check {\n  --fa: \"\\f14a\"; }\n\n.fa-check-square {\n  --fa: \"\\f14a\"; }\n\n.fa-peseta-sign {\n  --fa: \"\\e221\"; }\n\n.fa-heading {\n  --fa: \"\\f1dc\"; }\n\n.fa-header {\n  --fa: \"\\f1dc\"; }\n\n.fa-ghost {\n  --fa: \"\\f6e2\"; }\n\n.fa-list {\n  --fa: \"\\f03a\"; }\n\n.fa-list-squares {\n  --fa: \"\\f03a\"; }\n\n.fa-square-phone-flip {\n  --fa: \"\\f87b\"; }\n\n.fa-phone-square-alt {\n  --fa: \"\\f87b\"; }\n\n.fa-cart-plus {\n  --fa: \"\\f217\"; }\n\n.fa-gamepad {\n  --fa: \"\\f11b\"; }\n\n.fa-circle-dot {\n  --fa: \"\\f192\"; }\n\n.fa-dot-circle {\n  --fa: \"\\f192\"; }\n\n.fa-face-dizzy {\n  --fa: \"\\f567\"; }\n\n.fa-dizzy {\n  --fa: \"\\f567\"; }\n\n.fa-egg {\n  --fa: \"\\f7fb\"; }\n\n.fa-house-medical-circle-xmark {\n  --fa: \"\\e513\"; }\n\n.fa-campground {\n  --fa: \"\\f6bb\"; }\n\n.fa-folder-plus {\n  --fa: \"\\f65e\"; }\n\n.fa-futbol {\n  --fa: \"\\f1e3\"; }\n\n.fa-futbol-ball {\n  --fa: \"\\f1e3\"; }\n\n.fa-soccer-ball {\n  --fa: \"\\f1e3\"; }\n\n.fa-paintbrush {\n  --fa: \"\\f1fc\"; }\n\n.fa-paint-brush {\n  --fa: \"\\f1fc\"; }\n\n.fa-lock {\n  --fa: \"\\f023\"; }\n\n.fa-gas-pump {\n  --fa: \"\\f52f\"; }\n\n.fa-hot-tub-person {\n  --fa: \"\\f593\"; }\n\n.fa-hot-tub {\n  --fa: \"\\f593\"; }\n\n.fa-map-location {\n  --fa: \"\\f59f\"; }\n\n.fa-map-marked {\n  --fa: \"\\f59f\"; }\n\n.fa-house-flood-water {\n  --fa: \"\\e50e\"; }\n\n.fa-tree {\n  --fa: \"\\f1bb\"; }\n\n.fa-bridge-lock {\n  --fa: \"\\e4cc\"; }\n\n.fa-sack-dollar {\n  --fa: \"\\f81d\"; }\n\n.fa-pen-to-square {\n  --fa: \"\\f044\"; }\n\n.fa-edit {\n  --fa: \"\\f044\"; }\n\n.fa-car-side {\n  --fa: \"\\f5e4\"; }\n\n.fa-share-nodes {\n  --fa: \"\\f1e0\"; }\n\n.fa-share-alt {\n  --fa: \"\\f1e0\"; }\n\n.fa-heart-circle-minus {\n  --fa: \"\\e4ff\"; }\n\n.fa-hourglass-half {\n  --fa: \"\\f252\"; }\n\n.fa-hourglass-2 {\n  --fa: \"\\f252\"; }\n\n.fa-microscope {\n  --fa: \"\\f610\"; }\n\n.fa-sink {\n  --fa: \"\\e06d\"; }\n\n.fa-bag-shopping {\n  --fa: \"\\f290\"; }\n\n.fa-shopping-bag {\n  --fa: \"\\f290\"; }\n\n.fa-arrow-down-z-a {\n  --fa: \"\\f881\"; }\n\n.fa-sort-alpha-desc {\n  --fa: \"\\f881\"; }\n\n.fa-sort-alpha-down-alt {\n  --fa: \"\\f881\"; }\n\n.fa-mitten {\n  --fa: \"\\f7b5\"; }\n\n.fa-person-rays {\n  --fa: \"\\e54d\"; }\n\n.fa-users {\n  --fa: \"\\f0c0\"; }\n\n.fa-eye-slash {\n  --fa: \"\\f070\"; }\n\n.fa-flask-vial {\n  --fa: \"\\e4f3\"; }\n\n.fa-hand {\n  --fa: \"\\f256\"; }\n\n.fa-hand-paper {\n  --fa: \"\\f256\"; }\n\n.fa-om {\n  --fa: \"\\f679\"; }\n\n.fa-worm {\n  --fa: \"\\e599\"; }\n\n.fa-house-circle-xmark {\n  --fa: \"\\e50b\"; }\n\n.fa-plug {\n  --fa: \"\\f1e6\"; }\n\n.fa-chevron-up {\n  --fa: \"\\f077\"; }\n\n.fa-hand-spock {\n  --fa: \"\\f259\"; }\n\n.fa-stopwatch {\n  --fa: \"\\f2f2\"; }\n\n.fa-face-kiss {\n  --fa: \"\\f596\"; }\n\n.fa-kiss {\n  --fa: \"\\f596\"; }\n\n.fa-bridge-circle-xmark {\n  --fa: \"\\e4cb\"; }\n\n.fa-face-grin-tongue {\n  --fa: \"\\f589\"; }\n\n.fa-grin-tongue {\n  --fa: \"\\f589\"; }\n\n.fa-chess-bishop {\n  --fa: \"\\f43a\"; }\n\n.fa-face-grin-wink {\n  --fa: \"\\f58c\"; }\n\n.fa-grin-wink {\n  --fa: \"\\f58c\"; }\n\n.fa-ear-deaf {\n  --fa: \"\\f2a4\"; }\n\n.fa-deaf {\n  --fa: \"\\f2a4\"; }\n\n.fa-deafness {\n  --fa: \"\\f2a4\"; }\n\n.fa-hard-of-hearing {\n  --fa: \"\\f2a4\"; }\n\n.fa-road-circle-check {\n  --fa: \"\\e564\"; }\n\n.fa-dice-five {\n  --fa: \"\\f523\"; }\n\n.fa-square-rss {\n  --fa: \"\\f143\"; }\n\n.fa-rss-square {\n  --fa: \"\\f143\"; }\n\n.fa-land-mine-on {\n  --fa: \"\\e51b\"; }\n\n.fa-i-cursor {\n  --fa: \"\\f246\"; }\n\n.fa-stamp {\n  --fa: \"\\f5bf\"; }\n\n.fa-stairs {\n  --fa: \"\\e289\"; }\n\n.fa-i {\n  --fa: \"\\49\"; }\n\n.fa-hryvnia-sign {\n  --fa: \"\\f6f2\"; }\n\n.fa-hryvnia {\n  --fa: \"\\f6f2\"; }\n\n.fa-pills {\n  --fa: \"\\f484\"; }\n\n.fa-face-grin-wide {\n  --fa: \"\\f581\"; }\n\n.fa-grin-alt {\n  --fa: \"\\f581\"; }\n\n.fa-tooth {\n  --fa: \"\\f5c9\"; }\n\n.fa-v {\n  --fa: \"\\56\"; }\n\n.fa-bangladeshi-taka-sign {\n  --fa: \"\\e2e6\"; }\n\n.fa-bicycle {\n  --fa: \"\\f206\"; }\n\n.fa-staff-snake {\n  --fa: \"\\e579\"; }\n\n.fa-rod-asclepius {\n  --fa: \"\\e579\"; }\n\n.fa-rod-snake {\n  --fa: \"\\e579\"; }\n\n.fa-staff-aesculapius {\n  --fa: \"\\e579\"; }\n\n.fa-head-side-cough-slash {\n  --fa: \"\\e062\"; }\n\n.fa-truck-medical {\n  --fa: \"\\f0f9\"; }\n\n.fa-ambulance {\n  --fa: \"\\f0f9\"; }\n\n.fa-wheat-awn-circle-exclamation {\n  --fa: \"\\e598\"; }\n\n.fa-snowman {\n  --fa: \"\\f7d0\"; }\n\n.fa-mortar-pestle {\n  --fa: \"\\f5a7\"; }\n\n.fa-road-barrier {\n  --fa: \"\\e562\"; }\n\n.fa-school {\n  --fa: \"\\f549\"; }\n\n.fa-igloo {\n  --fa: \"\\f7ae\"; }\n\n.fa-joint {\n  --fa: \"\\f595\"; }\n\n.fa-angle-right {\n  --fa: \"\\f105\"; }\n\n.fa-horse {\n  --fa: \"\\f6f0\"; }\n\n.fa-q {\n  --fa: \"\\51\"; }\n\n.fa-g {\n  --fa: \"\\47\"; }\n\n.fa-notes-medical {\n  --fa: \"\\f481\"; }\n\n.fa-temperature-half {\n  --fa: \"\\f2c9\"; }\n\n.fa-temperature-2 {\n  --fa: \"\\f2c9\"; }\n\n.fa-thermometer-2 {\n  --fa: \"\\f2c9\"; }\n\n.fa-thermometer-half {\n  --fa: \"\\f2c9\"; }\n\n.fa-dong-sign {\n  --fa: \"\\e169\"; }\n\n.fa-capsules {\n  --fa: \"\\f46b\"; }\n\n.fa-poo-storm {\n  --fa: \"\\f75a\"; }\n\n.fa-poo-bolt {\n  --fa: \"\\f75a\"; }\n\n.fa-face-frown-open {\n  --fa: \"\\f57a\"; }\n\n.fa-frown-open {\n  --fa: \"\\f57a\"; }\n\n.fa-hand-point-up {\n  --fa: \"\\f0a6\"; }\n\n.fa-money-bill {\n  --fa: \"\\f0d6\"; }\n\n.fa-bookmark {\n  --fa: \"\\f02e\"; }\n\n.fa-align-justify {\n  --fa: \"\\f039\"; }\n\n.fa-umbrella-beach {\n  --fa: \"\\f5ca\"; }\n\n.fa-helmet-un {\n  --fa: \"\\e503\"; }\n\n.fa-bullseye {\n  --fa: \"\\f140\"; }\n\n.fa-bacon {\n  --fa: \"\\f7e5\"; }\n\n.fa-hand-point-down {\n  --fa: \"\\f0a7\"; }\n\n.fa-arrow-up-from-bracket {\n  --fa: \"\\e09a\"; }\n\n.fa-folder {\n  --fa: \"\\f07b\"; }\n\n.fa-folder-blank {\n  --fa: \"\\f07b\"; }\n\n.fa-file-waveform {\n  --fa: \"\\f478\"; }\n\n.fa-file-medical-alt {\n  --fa: \"\\f478\"; }\n\n.fa-radiation {\n  --fa: \"\\f7b9\"; }\n\n.fa-chart-simple {\n  --fa: \"\\e473\"; }\n\n.fa-mars-stroke {\n  --fa: \"\\f229\"; }\n\n.fa-vial {\n  --fa: \"\\f492\"; }\n\n.fa-gauge {\n  --fa: \"\\f624\"; }\n\n.fa-dashboard {\n  --fa: \"\\f624\"; }\n\n.fa-gauge-med {\n  --fa: \"\\f624\"; }\n\n.fa-tachometer-alt-average {\n  --fa: \"\\f624\"; }\n\n.fa-wand-magic-sparkles {\n  --fa: \"\\e2ca\"; }\n\n.fa-magic-wand-sparkles {\n  --fa: \"\\e2ca\"; }\n\n.fa-e {\n  --fa: \"\\45\"; }\n\n.fa-pen-clip {\n  --fa: \"\\f305\"; }\n\n.fa-pen-alt {\n  --fa: \"\\f305\"; }\n\n.fa-bridge-circle-exclamation {\n  --fa: \"\\e4ca\"; }\n\n.fa-user {\n  --fa: \"\\f007\"; }\n\n.fa-school-circle-check {\n  --fa: \"\\e56b\"; }\n\n.fa-dumpster {\n  --fa: \"\\f793\"; }\n\n.fa-van-shuttle {\n  --fa: \"\\f5b6\"; }\n\n.fa-shuttle-van {\n  --fa: \"\\f5b6\"; }\n\n.fa-building-user {\n  --fa: \"\\e4da\"; }\n\n.fa-square-caret-left {\n  --fa: \"\\f191\"; }\n\n.fa-caret-square-left {\n  --fa: \"\\f191\"; }\n\n.fa-highlighter {\n  --fa: \"\\f591\"; }\n\n.fa-key {\n  --fa: \"\\f084\"; }\n\n.fa-bullhorn {\n  --fa: \"\\f0a1\"; }\n\n.fa-globe {\n  --fa: \"\\f0ac\"; }\n\n.fa-synagogue {\n  --fa: \"\\f69b\"; }\n\n.fa-person-half-dress {\n  --fa: \"\\e548\"; }\n\n.fa-road-bridge {\n  --fa: \"\\e563\"; }\n\n.fa-location-arrow {\n  --fa: \"\\f124\"; }\n\n.fa-c {\n  --fa: \"\\43\"; }\n\n.fa-tablet-button {\n  --fa: \"\\f10a\"; }\n\n.fa-building-lock {\n  --fa: \"\\e4d6\"; }\n\n.fa-pizza-slice {\n  --fa: \"\\f818\"; }\n\n.fa-money-bill-wave {\n  --fa: \"\\f53a\"; }\n\n.fa-chart-area {\n  --fa: \"\\f1fe\"; }\n\n.fa-area-chart {\n  --fa: \"\\f1fe\"; }\n\n.fa-house-flag {\n  --fa: \"\\e50d\"; }\n\n.fa-person-circle-minus {\n  --fa: \"\\e540\"; }\n\n.fa-ban {\n  --fa: \"\\f05e\"; }\n\n.fa-cancel {\n  --fa: \"\\f05e\"; }\n\n.fa-camera-rotate {\n  --fa: \"\\e0d8\"; }\n\n.fa-spray-can-sparkles {\n  --fa: \"\\f5d0\"; }\n\n.fa-air-freshener {\n  --fa: \"\\f5d0\"; }\n\n.fa-star {\n  --fa: \"\\f005\"; }\n\n.fa-repeat {\n  --fa: \"\\f363\"; }\n\n.fa-cross {\n  --fa: \"\\f654\"; }\n\n.fa-box {\n  --fa: \"\\f466\"; }\n\n.fa-venus-mars {\n  --fa: \"\\f228\"; }\n\n.fa-arrow-pointer {\n  --fa: \"\\f245\"; }\n\n.fa-mouse-pointer {\n  --fa: \"\\f245\"; }\n\n.fa-maximize {\n  --fa: \"\\f31e\"; }\n\n.fa-expand-arrows-alt {\n  --fa: \"\\f31e\"; }\n\n.fa-charging-station {\n  --fa: \"\\f5e7\"; }\n\n.fa-shapes {\n  --fa: \"\\f61f\"; }\n\n.fa-triangle-circle-square {\n  --fa: \"\\f61f\"; }\n\n.fa-shuffle {\n  --fa: \"\\f074\"; }\n\n.fa-random {\n  --fa: \"\\f074\"; }\n\n.fa-person-running {\n  --fa: \"\\f70c\"; }\n\n.fa-running {\n  --fa: \"\\f70c\"; }\n\n.fa-mobile-retro {\n  --fa: \"\\e527\"; }\n\n.fa-grip-lines-vertical {\n  --fa: \"\\f7a5\"; }\n\n.fa-spider {\n  --fa: \"\\f717\"; }\n\n.fa-hands-bound {\n  --fa: \"\\e4f9\"; }\n\n.fa-file-invoice-dollar {\n  --fa: \"\\f571\"; }\n\n.fa-plane-circle-exclamation {\n  --fa: \"\\e556\"; }\n\n.fa-x-ray {\n  --fa: \"\\f497\"; }\n\n.fa-spell-check {\n  --fa: \"\\f891\"; }\n\n.fa-slash {\n  --fa: \"\\f715\"; }\n\n.fa-computer-mouse {\n  --fa: \"\\f8cc\"; }\n\n.fa-mouse {\n  --fa: \"\\f8cc\"; }\n\n.fa-arrow-right-to-bracket {\n  --fa: \"\\f090\"; }\n\n.fa-sign-in {\n  --fa: \"\\f090\"; }\n\n.fa-shop-slash {\n  --fa: \"\\e070\"; }\n\n.fa-store-alt-slash {\n  --fa: \"\\e070\"; }\n\n.fa-server {\n  --fa: \"\\f233\"; }\n\n.fa-virus-covid-slash {\n  --fa: \"\\e4a9\"; }\n\n.fa-shop-lock {\n  --fa: \"\\e4a5\"; }\n\n.fa-hourglass-start {\n  --fa: \"\\f251\"; }\n\n.fa-hourglass-1 {\n  --fa: \"\\f251\"; }\n\n.fa-blender-phone {\n  --fa: \"\\f6b6\"; }\n\n.fa-building-wheat {\n  --fa: \"\\e4db\"; }\n\n.fa-person-breastfeeding {\n  --fa: \"\\e53a\"; }\n\n.fa-right-to-bracket {\n  --fa: \"\\f2f6\"; }\n\n.fa-sign-in-alt {\n  --fa: \"\\f2f6\"; }\n\n.fa-venus {\n  --fa: \"\\f221\"; }\n\n.fa-passport {\n  --fa: \"\\f5ab\"; }\n\n.fa-thumbtack-slash {\n  --fa: \"\\e68f\"; }\n\n.fa-thumb-tack-slash {\n  --fa: \"\\e68f\"; }\n\n.fa-heart-pulse {\n  --fa: \"\\f21e\"; }\n\n.fa-heartbeat {\n  --fa: \"\\f21e\"; }\n\n.fa-people-carry-box {\n  --fa: \"\\f4ce\"; }\n\n.fa-people-carry {\n  --fa: \"\\f4ce\"; }\n\n.fa-temperature-high {\n  --fa: \"\\f769\"; }\n\n.fa-microchip {\n  --fa: \"\\f2db\"; }\n\n.fa-crown {\n  --fa: \"\\f521\"; }\n\n.fa-weight-hanging {\n  --fa: \"\\f5cd\"; }\n\n.fa-xmarks-lines {\n  --fa: \"\\e59a\"; }\n\n.fa-file-prescription {\n  --fa: \"\\f572\"; }\n\n.fa-weight-scale {\n  --fa: \"\\f496\"; }\n\n.fa-weight {\n  --fa: \"\\f496\"; }\n\n.fa-user-group {\n  --fa: \"\\f500\"; }\n\n.fa-user-friends {\n  --fa: \"\\f500\"; }\n\n.fa-arrow-up-a-z {\n  --fa: \"\\f15e\"; }\n\n.fa-sort-alpha-up {\n  --fa: \"\\f15e\"; }\n\n.fa-chess-knight {\n  --fa: \"\\f441\"; }\n\n.fa-face-laugh-squint {\n  --fa: \"\\f59b\"; }\n\n.fa-laugh-squint {\n  --fa: \"\\f59b\"; }\n\n.fa-wheelchair {\n  --fa: \"\\f193\"; }\n\n.fa-circle-arrow-up {\n  --fa: \"\\f0aa\"; }\n\n.fa-arrow-circle-up {\n  --fa: \"\\f0aa\"; }\n\n.fa-toggle-on {\n  --fa: \"\\f205\"; }\n\n.fa-person-walking {\n  --fa: \"\\f554\"; }\n\n.fa-walking {\n  --fa: \"\\f554\"; }\n\n.fa-l {\n  --fa: \"\\4c\"; }\n\n.fa-fire {\n  --fa: \"\\f06d\"; }\n\n.fa-bed-pulse {\n  --fa: \"\\f487\"; }\n\n.fa-procedures {\n  --fa: \"\\f487\"; }\n\n.fa-shuttle-space {\n  --fa: \"\\f197\"; }\n\n.fa-space-shuttle {\n  --fa: \"\\f197\"; }\n\n.fa-face-laugh {\n  --fa: \"\\f599\"; }\n\n.fa-laugh {\n  --fa: \"\\f599\"; }\n\n.fa-folder-open {\n  --fa: \"\\f07c\"; }\n\n.fa-heart-circle-plus {\n  --fa: \"\\e500\"; }\n\n.fa-code-fork {\n  --fa: \"\\e13b\"; }\n\n.fa-city {\n  --fa: \"\\f64f\"; }\n\n.fa-microphone-lines {\n  --fa: \"\\f3c9\"; }\n\n.fa-microphone-alt {\n  --fa: \"\\f3c9\"; }\n\n.fa-pepper-hot {\n  --fa: \"\\f816\"; }\n\n.fa-unlock {\n  --fa: \"\\f09c\"; }\n\n.fa-colon-sign {\n  --fa: \"\\e140\"; }\n\n.fa-headset {\n  --fa: \"\\f590\"; }\n\n.fa-store-slash {\n  --fa: \"\\e071\"; }\n\n.fa-road-circle-xmark {\n  --fa: \"\\e566\"; }\n\n.fa-user-minus {\n  --fa: \"\\f503\"; }\n\n.fa-mars-stroke-up {\n  --fa: \"\\f22a\"; }\n\n.fa-mars-stroke-v {\n  --fa: \"\\f22a\"; }\n\n.fa-champagne-glasses {\n  --fa: \"\\f79f\"; }\n\n.fa-glass-cheers {\n  --fa: \"\\f79f\"; }\n\n.fa-clipboard {\n  --fa: \"\\f328\"; }\n\n.fa-house-circle-exclamation {\n  --fa: \"\\e50a\"; }\n\n.fa-file-arrow-up {\n  --fa: \"\\f574\"; }\n\n.fa-file-upload {\n  --fa: \"\\f574\"; }\n\n.fa-wifi {\n  --fa: \"\\f1eb\"; }\n\n.fa-wifi-3 {\n  --fa: \"\\f1eb\"; }\n\n.fa-wifi-strong {\n  --fa: \"\\f1eb\"; }\n\n.fa-bath {\n  --fa: \"\\f2cd\"; }\n\n.fa-bathtub {\n  --fa: \"\\f2cd\"; }\n\n.fa-underline {\n  --fa: \"\\f0cd\"; }\n\n.fa-user-pen {\n  --fa: \"\\f4ff\"; }\n\n.fa-user-edit {\n  --fa: \"\\f4ff\"; }\n\n.fa-signature {\n  --fa: \"\\f5b7\"; }\n\n.fa-stroopwafel {\n  --fa: \"\\f551\"; }\n\n.fa-bold {\n  --fa: \"\\f032\"; }\n\n.fa-anchor-lock {\n  --fa: \"\\e4ad\"; }\n\n.fa-building-ngo {\n  --fa: \"\\e4d7\"; }\n\n.fa-manat-sign {\n  --fa: \"\\e1d5\"; }\n\n.fa-not-equal {\n  --fa: \"\\f53e\"; }\n\n.fa-border-top-left {\n  --fa: \"\\f853\"; }\n\n.fa-border-style {\n  --fa: \"\\f853\"; }\n\n.fa-map-location-dot {\n  --fa: \"\\f5a0\"; }\n\n.fa-map-marked-alt {\n  --fa: \"\\f5a0\"; }\n\n.fa-jedi {\n  --fa: \"\\f669\"; }\n\n.fa-square-poll-vertical {\n  --fa: \"\\f681\"; }\n\n.fa-poll {\n  --fa: \"\\f681\"; }\n\n.fa-mug-hot {\n  --fa: \"\\f7b6\"; }\n\n.fa-car-battery {\n  --fa: \"\\f5df\"; }\n\n.fa-battery-car {\n  --fa: \"\\f5df\"; }\n\n.fa-gift {\n  --fa: \"\\f06b\"; }\n\n.fa-dice-two {\n  --fa: \"\\f528\"; }\n\n.fa-chess-queen {\n  --fa: \"\\f445\"; }\n\n.fa-glasses {\n  --fa: \"\\f530\"; }\n\n.fa-chess-board {\n  --fa: \"\\f43c\"; }\n\n.fa-building-circle-check {\n  --fa: \"\\e4d2\"; }\n\n.fa-person-chalkboard {\n  --fa: \"\\e53d\"; }\n\n.fa-mars-stroke-right {\n  --fa: \"\\f22b\"; }\n\n.fa-mars-stroke-h {\n  --fa: \"\\f22b\"; }\n\n.fa-hand-back-fist {\n  --fa: \"\\f255\"; }\n\n.fa-hand-rock {\n  --fa: \"\\f255\"; }\n\n.fa-square-caret-up {\n  --fa: \"\\f151\"; }\n\n.fa-caret-square-up {\n  --fa: \"\\f151\"; }\n\n.fa-cloud-showers-water {\n  --fa: \"\\e4e4\"; }\n\n.fa-chart-bar {\n  --fa: \"\\f080\"; }\n\n.fa-bar-chart {\n  --fa: \"\\f080\"; }\n\n.fa-hands-bubbles {\n  --fa: \"\\e05e\"; }\n\n.fa-hands-wash {\n  --fa: \"\\e05e\"; }\n\n.fa-less-than-equal {\n  --fa: \"\\f537\"; }\n\n.fa-train {\n  --fa: \"\\f238\"; }\n\n.fa-eye-low-vision {\n  --fa: \"\\f2a8\"; }\n\n.fa-low-vision {\n  --fa: \"\\f2a8\"; }\n\n.fa-crow {\n  --fa: \"\\f520\"; }\n\n.fa-sailboat {\n  --fa: \"\\e445\"; }\n\n.fa-window-restore {\n  --fa: \"\\f2d2\"; }\n\n.fa-square-plus {\n  --fa: \"\\f0fe\"; }\n\n.fa-plus-square {\n  --fa: \"\\f0fe\"; }\n\n.fa-torii-gate {\n  --fa: \"\\f6a1\"; }\n\n.fa-frog {\n  --fa: \"\\f52e\"; }\n\n.fa-bucket {\n  --fa: \"\\e4cf\"; }\n\n.fa-image {\n  --fa: \"\\f03e\"; }\n\n.fa-microphone {\n  --fa: \"\\f130\"; }\n\n.fa-cow {\n  --fa: \"\\f6c8\"; }\n\n.fa-caret-up {\n  --fa: \"\\f0d8\"; }\n\n.fa-screwdriver {\n  --fa: \"\\f54a\"; }\n\n.fa-folder-closed {\n  --fa: \"\\e185\"; }\n\n.fa-house-tsunami {\n  --fa: \"\\e515\"; }\n\n.fa-square-nfi {\n  --fa: \"\\e576\"; }\n\n.fa-arrow-up-from-ground-water {\n  --fa: \"\\e4b5\"; }\n\n.fa-martini-glass {\n  --fa: \"\\f57b\"; }\n\n.fa-glass-martini-alt {\n  --fa: \"\\f57b\"; }\n\n.fa-square-binary {\n  --fa: \"\\e69b\"; }\n\n.fa-rotate-left {\n  --fa: \"\\f2ea\"; }\n\n.fa-rotate-back {\n  --fa: \"\\f2ea\"; }\n\n.fa-rotate-backward {\n  --fa: \"\\f2ea\"; }\n\n.fa-undo-alt {\n  --fa: \"\\f2ea\"; }\n\n.fa-table-columns {\n  --fa: \"\\f0db\"; }\n\n.fa-columns {\n  --fa: \"\\f0db\"; }\n\n.fa-lemon {\n  --fa: \"\\f094\"; }\n\n.fa-head-side-mask {\n  --fa: \"\\e063\"; }\n\n.fa-handshake {\n  --fa: \"\\f2b5\"; }\n\n.fa-gem {\n  --fa: \"\\f3a5\"; }\n\n.fa-dolly {\n  --fa: \"\\f472\"; }\n\n.fa-dolly-box {\n  --fa: \"\\f472\"; }\n\n.fa-smoking {\n  --fa: \"\\f48d\"; }\n\n.fa-minimize {\n  --fa: \"\\f78c\"; }\n\n.fa-compress-arrows-alt {\n  --fa: \"\\f78c\"; }\n\n.fa-monument {\n  --fa: \"\\f5a6\"; }\n\n.fa-snowplow {\n  --fa: \"\\f7d2\"; }\n\n.fa-angles-right {\n  --fa: \"\\f101\"; }\n\n.fa-angle-double-right {\n  --fa: \"\\f101\"; }\n\n.fa-cannabis {\n  --fa: \"\\f55f\"; }\n\n.fa-circle-play {\n  --fa: \"\\f144\"; }\n\n.fa-play-circle {\n  --fa: \"\\f144\"; }\n\n.fa-tablets {\n  --fa: \"\\f490\"; }\n\n.fa-ethernet {\n  --fa: \"\\f796\"; }\n\n.fa-euro-sign {\n  --fa: \"\\f153\"; }\n\n.fa-eur {\n  --fa: \"\\f153\"; }\n\n.fa-euro {\n  --fa: \"\\f153\"; }\n\n.fa-chair {\n  --fa: \"\\f6c0\"; }\n\n.fa-circle-check {\n  --fa: \"\\f058\"; }\n\n.fa-check-circle {\n  --fa: \"\\f058\"; }\n\n.fa-circle-stop {\n  --fa: \"\\f28d\"; }\n\n.fa-stop-circle {\n  --fa: \"\\f28d\"; }\n\n.fa-compass-drafting {\n  --fa: \"\\f568\"; }\n\n.fa-drafting-compass {\n  --fa: \"\\f568\"; }\n\n.fa-plate-wheat {\n  --fa: \"\\e55a\"; }\n\n.fa-icicles {\n  --fa: \"\\f7ad\"; }\n\n.fa-person-shelter {\n  --fa: \"\\e54f\"; }\n\n.fa-neuter {\n  --fa: \"\\f22c\"; }\n\n.fa-id-badge {\n  --fa: \"\\f2c1\"; }\n\n.fa-marker {\n  --fa: \"\\f5a1\"; }\n\n.fa-face-laugh-beam {\n  --fa: \"\\f59a\"; }\n\n.fa-laugh-beam {\n  --fa: \"\\f59a\"; }\n\n.fa-helicopter-symbol {\n  --fa: \"\\e502\"; }\n\n.fa-universal-access {\n  --fa: \"\\f29a\"; }\n\n.fa-circle-chevron-up {\n  --fa: \"\\f139\"; }\n\n.fa-chevron-circle-up {\n  --fa: \"\\f139\"; }\n\n.fa-lari-sign {\n  --fa: \"\\e1c8\"; }\n\n.fa-volcano {\n  --fa: \"\\f770\"; }\n\n.fa-person-walking-dashed-line-arrow-right {\n  --fa: \"\\e553\"; }\n\n.fa-sterling-sign {\n  --fa: \"\\f154\"; }\n\n.fa-gbp {\n  --fa: \"\\f154\"; }\n\n.fa-pound-sign {\n  --fa: \"\\f154\"; }\n\n.fa-viruses {\n  --fa: \"\\e076\"; }\n\n.fa-square-person-confined {\n  --fa: \"\\e577\"; }\n\n.fa-user-tie {\n  --fa: \"\\f508\"; }\n\n.fa-arrow-down-long {\n  --fa: \"\\f175\"; }\n\n.fa-long-arrow-down {\n  --fa: \"\\f175\"; }\n\n.fa-tent-arrow-down-to-line {\n  --fa: \"\\e57e\"; }\n\n.fa-certificate {\n  --fa: \"\\f0a3\"; }\n\n.fa-reply-all {\n  --fa: \"\\f122\"; }\n\n.fa-mail-reply-all {\n  --fa: \"\\f122\"; }\n\n.fa-suitcase {\n  --fa: \"\\f0f2\"; }\n\n.fa-person-skating {\n  --fa: \"\\f7c5\"; }\n\n.fa-skating {\n  --fa: \"\\f7c5\"; }\n\n.fa-filter-circle-dollar {\n  --fa: \"\\f662\"; }\n\n.fa-funnel-dollar {\n  --fa: \"\\f662\"; }\n\n.fa-camera-retro {\n  --fa: \"\\f083\"; }\n\n.fa-circle-arrow-down {\n  --fa: \"\\f0ab\"; }\n\n.fa-arrow-circle-down {\n  --fa: \"\\f0ab\"; }\n\n.fa-file-import {\n  --fa: \"\\f56f\"; }\n\n.fa-arrow-right-to-file {\n  --fa: \"\\f56f\"; }\n\n.fa-square-arrow-up-right {\n  --fa: \"\\f14c\"; }\n\n.fa-external-link-square {\n  --fa: \"\\f14c\"; }\n\n.fa-box-open {\n  --fa: \"\\f49e\"; }\n\n.fa-scroll {\n  --fa: \"\\f70e\"; }\n\n.fa-spa {\n  --fa: \"\\f5bb\"; }\n\n.fa-location-pin-lock {\n  --fa: \"\\e51f\"; }\n\n.fa-pause {\n  --fa: \"\\f04c\"; }\n\n.fa-hill-avalanche {\n  --fa: \"\\e507\"; }\n\n.fa-temperature-empty {\n  --fa: \"\\f2cb\"; }\n\n.fa-temperature-0 {\n  --fa: \"\\f2cb\"; }\n\n.fa-thermometer-0 {\n  --fa: \"\\f2cb\"; }\n\n.fa-thermometer-empty {\n  --fa: \"\\f2cb\"; }\n\n.fa-bomb {\n  --fa: \"\\f1e2\"; }\n\n.fa-registered {\n  --fa: \"\\f25d\"; }\n\n.fa-address-card {\n  --fa: \"\\f2bb\"; }\n\n.fa-contact-card {\n  --fa: \"\\f2bb\"; }\n\n.fa-vcard {\n  --fa: \"\\f2bb\"; }\n\n.fa-scale-unbalanced-flip {\n  --fa: \"\\f516\"; }\n\n.fa-balance-scale-right {\n  --fa: \"\\f516\"; }\n\n.fa-subscript {\n  --fa: \"\\f12c\"; }\n\n.fa-diamond-turn-right {\n  --fa: \"\\f5eb\"; }\n\n.fa-directions {\n  --fa: \"\\f5eb\"; }\n\n.fa-burst {\n  --fa: \"\\e4dc\"; }\n\n.fa-house-laptop {\n  --fa: \"\\e066\"; }\n\n.fa-laptop-house {\n  --fa: \"\\e066\"; }\n\n.fa-face-tired {\n  --fa: \"\\f5c8\"; }\n\n.fa-tired {\n  --fa: \"\\f5c8\"; }\n\n.fa-money-bills {\n  --fa: \"\\e1f3\"; }\n\n.fa-smog {\n  --fa: \"\\f75f\"; }\n\n.fa-crutch {\n  --fa: \"\\f7f7\"; }\n\n.fa-cloud-arrow-up {\n  --fa: \"\\f0ee\"; }\n\n.fa-cloud-upload {\n  --fa: \"\\f0ee\"; }\n\n.fa-cloud-upload-alt {\n  --fa: \"\\f0ee\"; }\n\n.fa-palette {\n  --fa: \"\\f53f\"; }\n\n.fa-arrows-turn-right {\n  --fa: \"\\e4c0\"; }\n\n.fa-vest {\n  --fa: \"\\e085\"; }\n\n.fa-ferry {\n  --fa: \"\\e4ea\"; }\n\n.fa-arrows-down-to-people {\n  --fa: \"\\e4b9\"; }\n\n.fa-seedling {\n  --fa: \"\\f4d8\"; }\n\n.fa-sprout {\n  --fa: \"\\f4d8\"; }\n\n.fa-left-right {\n  --fa: \"\\f337\"; }\n\n.fa-arrows-alt-h {\n  --fa: \"\\f337\"; }\n\n.fa-boxes-packing {\n  --fa: \"\\e4c7\"; }\n\n.fa-circle-arrow-left {\n  --fa: \"\\f0a8\"; }\n\n.fa-arrow-circle-left {\n  --fa: \"\\f0a8\"; }\n\n.fa-group-arrows-rotate {\n  --fa: \"\\e4f6\"; }\n\n.fa-bowl-food {\n  --fa: \"\\e4c6\"; }\n\n.fa-candy-cane {\n  --fa: \"\\f786\"; }\n\n.fa-arrow-down-wide-short {\n  --fa: \"\\f160\"; }\n\n.fa-sort-amount-asc {\n  --fa: \"\\f160\"; }\n\n.fa-sort-amount-down {\n  --fa: \"\\f160\"; }\n\n.fa-cloud-bolt {\n  --fa: \"\\f76c\"; }\n\n.fa-thunderstorm {\n  --fa: \"\\f76c\"; }\n\n.fa-text-slash {\n  --fa: \"\\f87d\"; }\n\n.fa-remove-format {\n  --fa: \"\\f87d\"; }\n\n.fa-face-smile-wink {\n  --fa: \"\\f4da\"; }\n\n.fa-smile-wink {\n  --fa: \"\\f4da\"; }\n\n.fa-file-word {\n  --fa: \"\\f1c2\"; }\n\n.fa-file-powerpoint {\n  --fa: \"\\f1c4\"; }\n\n.fa-arrows-left-right {\n  --fa: \"\\f07e\"; }\n\n.fa-arrows-h {\n  --fa: \"\\f07e\"; }\n\n.fa-house-lock {\n  --fa: \"\\e510\"; }\n\n.fa-cloud-arrow-down {\n  --fa: \"\\f0ed\"; }\n\n.fa-cloud-download {\n  --fa: \"\\f0ed\"; }\n\n.fa-cloud-download-alt {\n  --fa: \"\\f0ed\"; }\n\n.fa-children {\n  --fa: \"\\e4e1\"; }\n\n.fa-chalkboard {\n  --fa: \"\\f51b\"; }\n\n.fa-blackboard {\n  --fa: \"\\f51b\"; }\n\n.fa-user-large-slash {\n  --fa: \"\\f4fa\"; }\n\n.fa-user-alt-slash {\n  --fa: \"\\f4fa\"; }\n\n.fa-envelope-open {\n  --fa: \"\\f2b6\"; }\n\n.fa-handshake-simple-slash {\n  --fa: \"\\e05f\"; }\n\n.fa-handshake-alt-slash {\n  --fa: \"\\e05f\"; }\n\n.fa-mattress-pillow {\n  --fa: \"\\e525\"; }\n\n.fa-guarani-sign {\n  --fa: \"\\e19a\"; }\n\n.fa-arrows-rotate {\n  --fa: \"\\f021\"; }\n\n.fa-refresh {\n  --fa: \"\\f021\"; }\n\n.fa-sync {\n  --fa: \"\\f021\"; }\n\n.fa-fire-extinguisher {\n  --fa: \"\\f134\"; }\n\n.fa-cruzeiro-sign {\n  --fa: \"\\e152\"; }\n\n.fa-greater-than-equal {\n  --fa: \"\\f532\"; }\n\n.fa-shield-halved {\n  --fa: \"\\f3ed\"; }\n\n.fa-shield-alt {\n  --fa: \"\\f3ed\"; }\n\n.fa-book-atlas {\n  --fa: \"\\f558\"; }\n\n.fa-atlas {\n  --fa: \"\\f558\"; }\n\n.fa-virus {\n  --fa: \"\\e074\"; }\n\n.fa-envelope-circle-check {\n  --fa: \"\\e4e8\"; }\n\n.fa-layer-group {\n  --fa: \"\\f5fd\"; }\n\n.fa-arrows-to-dot {\n  --fa: \"\\e4be\"; }\n\n.fa-archway {\n  --fa: \"\\f557\"; }\n\n.fa-heart-circle-check {\n  --fa: \"\\e4fd\"; }\n\n.fa-house-chimney-crack {\n  --fa: \"\\f6f1\"; }\n\n.fa-house-damage {\n  --fa: \"\\f6f1\"; }\n\n.fa-file-zipper {\n  --fa: \"\\f1c6\"; }\n\n.fa-file-archive {\n  --fa: \"\\f1c6\"; }\n\n.fa-square {\n  --fa: \"\\f0c8\"; }\n\n.fa-martini-glass-empty {\n  --fa: \"\\f000\"; }\n\n.fa-glass-martini {\n  --fa: \"\\f000\"; }\n\n.fa-couch {\n  --fa: \"\\f4b8\"; }\n\n.fa-cedi-sign {\n  --fa: \"\\e0df\"; }\n\n.fa-italic {\n  --fa: \"\\f033\"; }\n\n.fa-table-cells-column-lock {\n  --fa: \"\\e678\"; }\n\n.fa-church {\n  --fa: \"\\f51d\"; }\n\n.fa-comments-dollar {\n  --fa: \"\\f653\"; }\n\n.fa-democrat {\n  --fa: \"\\f747\"; }\n\n.fa-z {\n  --fa: \"\\5a\"; }\n\n.fa-person-skiing {\n  --fa: \"\\f7c9\"; }\n\n.fa-skiing {\n  --fa: \"\\f7c9\"; }\n\n.fa-road-lock {\n  --fa: \"\\e567\"; }\n\n.fa-a {\n  --fa: \"\\41\"; }\n\n.fa-temperature-arrow-down {\n  --fa: \"\\e03f\"; }\n\n.fa-temperature-down {\n  --fa: \"\\e03f\"; }\n\n.fa-feather-pointed {\n  --fa: \"\\f56b\"; }\n\n.fa-feather-alt {\n  --fa: \"\\f56b\"; }\n\n.fa-p {\n  --fa: \"\\50\"; }\n\n.fa-snowflake {\n  --fa: \"\\f2dc\"; }\n\n.fa-newspaper {\n  --fa: \"\\f1ea\"; }\n\n.fa-rectangle-ad {\n  --fa: \"\\f641\"; }\n\n.fa-ad {\n  --fa: \"\\f641\"; }\n\n.fa-circle-arrow-right {\n  --fa: \"\\f0a9\"; }\n\n.fa-arrow-circle-right {\n  --fa: \"\\f0a9\"; }\n\n.fa-filter-circle-xmark {\n  --fa: \"\\e17b\"; }\n\n.fa-locust {\n  --fa: \"\\e520\"; }\n\n.fa-sort {\n  --fa: \"\\f0dc\"; }\n\n.fa-unsorted {\n  --fa: \"\\f0dc\"; }\n\n.fa-list-ol {\n  --fa: \"\\f0cb\"; }\n\n.fa-list-1-2 {\n  --fa: \"\\f0cb\"; }\n\n.fa-list-numeric {\n  --fa: \"\\f0cb\"; }\n\n.fa-person-dress-burst {\n  --fa: \"\\e544\"; }\n\n.fa-money-check-dollar {\n  --fa: \"\\f53d\"; }\n\n.fa-money-check-alt {\n  --fa: \"\\f53d\"; }\n\n.fa-vector-square {\n  --fa: \"\\f5cb\"; }\n\n.fa-bread-slice {\n  --fa: \"\\f7ec\"; }\n\n.fa-language {\n  --fa: \"\\f1ab\"; }\n\n.fa-face-kiss-wink-heart {\n  --fa: \"\\f598\"; }\n\n.fa-kiss-wink-heart {\n  --fa: \"\\f598\"; }\n\n.fa-filter {\n  --fa: \"\\f0b0\"; }\n\n.fa-question {\n  --fa: \"\\3f\"; }\n\n.fa-file-signature {\n  --fa: \"\\f573\"; }\n\n.fa-up-down-left-right {\n  --fa: \"\\f0b2\"; }\n\n.fa-arrows-alt {\n  --fa: \"\\f0b2\"; }\n\n.fa-house-chimney-user {\n  --fa: \"\\e065\"; }\n\n.fa-hand-holding-heart {\n  --fa: \"\\f4be\"; }\n\n.fa-puzzle-piece {\n  --fa: \"\\f12e\"; }\n\n.fa-money-check {\n  --fa: \"\\f53c\"; }\n\n.fa-star-half-stroke {\n  --fa: \"\\f5c0\"; }\n\n.fa-star-half-alt {\n  --fa: \"\\f5c0\"; }\n\n.fa-code {\n  --fa: \"\\f121\"; }\n\n.fa-whiskey-glass {\n  --fa: \"\\f7a0\"; }\n\n.fa-glass-whiskey {\n  --fa: \"\\f7a0\"; }\n\n.fa-building-circle-exclamation {\n  --fa: \"\\e4d3\"; }\n\n.fa-magnifying-glass-chart {\n  --fa: \"\\e522\"; }\n\n.fa-arrow-up-right-from-square {\n  --fa: \"\\f08e\"; }\n\n.fa-external-link {\n  --fa: \"\\f08e\"; }\n\n.fa-cubes-stacked {\n  --fa: \"\\e4e6\"; }\n\n.fa-won-sign {\n  --fa: \"\\f159\"; }\n\n.fa-krw {\n  --fa: \"\\f159\"; }\n\n.fa-won {\n  --fa: \"\\f159\"; }\n\n.fa-virus-covid {\n  --fa: \"\\e4a8\"; }\n\n.fa-austral-sign {\n  --fa: \"\\e0a9\"; }\n\n.fa-f {\n  --fa: \"\\46\"; }\n\n.fa-leaf {\n  --fa: \"\\f06c\"; }\n\n.fa-road {\n  --fa: \"\\f018\"; }\n\n.fa-taxi {\n  --fa: \"\\f1ba\"; }\n\n.fa-cab {\n  --fa: \"\\f1ba\"; }\n\n.fa-person-circle-plus {\n  --fa: \"\\e541\"; }\n\n.fa-chart-pie {\n  --fa: \"\\f200\"; }\n\n.fa-pie-chart {\n  --fa: \"\\f200\"; }\n\n.fa-bolt-lightning {\n  --fa: \"\\e0b7\"; }\n\n.fa-sack-xmark {\n  --fa: \"\\e56a\"; }\n\n.fa-file-excel {\n  --fa: \"\\f1c3\"; }\n\n.fa-file-contract {\n  --fa: \"\\f56c\"; }\n\n.fa-fish-fins {\n  --fa: \"\\e4f2\"; }\n\n.fa-building-flag {\n  --fa: \"\\e4d5\"; }\n\n.fa-face-grin-beam {\n  --fa: \"\\f582\"; }\n\n.fa-grin-beam {\n  --fa: \"\\f582\"; }\n\n.fa-object-ungroup {\n  --fa: \"\\f248\"; }\n\n.fa-poop {\n  --fa: \"\\f619\"; }\n\n.fa-location-pin {\n  --fa: \"\\f041\"; }\n\n.fa-map-marker {\n  --fa: \"\\f041\"; }\n\n.fa-kaaba {\n  --fa: \"\\f66b\"; }\n\n.fa-toilet-paper {\n  --fa: \"\\f71e\"; }\n\n.fa-helmet-safety {\n  --fa: \"\\f807\"; }\n\n.fa-hard-hat {\n  --fa: \"\\f807\"; }\n\n.fa-hat-hard {\n  --fa: \"\\f807\"; }\n\n.fa-eject {\n  --fa: \"\\f052\"; }\n\n.fa-circle-right {\n  --fa: \"\\f35a\"; }\n\n.fa-arrow-alt-circle-right {\n  --fa: \"\\f35a\"; }\n\n.fa-plane-circle-check {\n  --fa: \"\\e555\"; }\n\n.fa-face-rolling-eyes {\n  --fa: \"\\f5a5\"; }\n\n.fa-meh-rolling-eyes {\n  --fa: \"\\f5a5\"; }\n\n.fa-object-group {\n  --fa: \"\\f247\"; }\n\n.fa-chart-line {\n  --fa: \"\\f201\"; }\n\n.fa-line-chart {\n  --fa: \"\\f201\"; }\n\n.fa-mask-ventilator {\n  --fa: \"\\e524\"; }\n\n.fa-arrow-right {\n  --fa: \"\\f061\"; }\n\n.fa-signs-post {\n  --fa: \"\\f277\"; }\n\n.fa-map-signs {\n  --fa: \"\\f277\"; }\n\n.fa-cash-register {\n  --fa: \"\\f788\"; }\n\n.fa-person-circle-question {\n  --fa: \"\\e542\"; }\n\n.fa-h {\n  --fa: \"\\48\"; }\n\n.fa-tarp {\n  --fa: \"\\e57b\"; }\n\n.fa-screwdriver-wrench {\n  --fa: \"\\f7d9\"; }\n\n.fa-tools {\n  --fa: \"\\f7d9\"; }\n\n.fa-arrows-to-eye {\n  --fa: \"\\e4bf\"; }\n\n.fa-plug-circle-bolt {\n  --fa: \"\\e55b\"; }\n\n.fa-heart {\n  --fa: \"\\f004\"; }\n\n.fa-mars-and-venus {\n  --fa: \"\\f224\"; }\n\n.fa-house-user {\n  --fa: \"\\e1b0\"; }\n\n.fa-home-user {\n  --fa: \"\\e1b0\"; }\n\n.fa-dumpster-fire {\n  --fa: \"\\f794\"; }\n\n.fa-house-crack {\n  --fa: \"\\e3b1\"; }\n\n.fa-martini-glass-citrus {\n  --fa: \"\\f561\"; }\n\n.fa-cocktail {\n  --fa: \"\\f561\"; }\n\n.fa-face-surprise {\n  --fa: \"\\f5c2\"; }\n\n.fa-surprise {\n  --fa: \"\\f5c2\"; }\n\n.fa-bottle-water {\n  --fa: \"\\e4c5\"; }\n\n.fa-circle-pause {\n  --fa: \"\\f28b\"; }\n\n.fa-pause-circle {\n  --fa: \"\\f28b\"; }\n\n.fa-toilet-paper-slash {\n  --fa: \"\\e072\"; }\n\n.fa-apple-whole {\n  --fa: \"\\f5d1\"; }\n\n.fa-apple-alt {\n  --fa: \"\\f5d1\"; }\n\n.fa-kitchen-set {\n  --fa: \"\\e51a\"; }\n\n.fa-r {\n  --fa: \"\\52\"; }\n\n.fa-temperature-quarter {\n  --fa: \"\\f2ca\"; }\n\n.fa-temperature-1 {\n  --fa: \"\\f2ca\"; }\n\n.fa-thermometer-1 {\n  --fa: \"\\f2ca\"; }\n\n.fa-thermometer-quarter {\n  --fa: \"\\f2ca\"; }\n\n.fa-cube {\n  --fa: \"\\f1b2\"; }\n\n.fa-bitcoin-sign {\n  --fa: \"\\e0b4\"; }\n\n.fa-shield-dog {\n  --fa: \"\\e573\"; }\n\n.fa-solar-panel {\n  --fa: \"\\f5ba\"; }\n\n.fa-lock-open {\n  --fa: \"\\f3c1\"; }\n\n.fa-elevator {\n  --fa: \"\\e16d\"; }\n\n.fa-money-bill-transfer {\n  --fa: \"\\e528\"; }\n\n.fa-money-bill-trend-up {\n  --fa: \"\\e529\"; }\n\n.fa-house-flood-water-circle-arrow-right {\n  --fa: \"\\e50f\"; }\n\n.fa-square-poll-horizontal {\n  --fa: \"\\f682\"; }\n\n.fa-poll-h {\n  --fa: \"\\f682\"; }\n\n.fa-circle {\n  --fa: \"\\f111\"; }\n\n.fa-backward-fast {\n  --fa: \"\\f049\"; }\n\n.fa-fast-backward {\n  --fa: \"\\f049\"; }\n\n.fa-recycle {\n  --fa: \"\\f1b8\"; }\n\n.fa-user-astronaut {\n  --fa: \"\\f4fb\"; }\n\n.fa-plane-slash {\n  --fa: \"\\e069\"; }\n\n.fa-trademark {\n  --fa: \"\\f25c\"; }\n\n.fa-basketball {\n  --fa: \"\\f434\"; }\n\n.fa-basketball-ball {\n  --fa: \"\\f434\"; }\n\n.fa-satellite-dish {\n  --fa: \"\\f7c0\"; }\n\n.fa-circle-up {\n  --fa: \"\\f35b\"; }\n\n.fa-arrow-alt-circle-up {\n  --fa: \"\\f35b\"; }\n\n.fa-mobile-screen-button {\n  --fa: \"\\f3cd\"; }\n\n.fa-mobile-alt {\n  --fa: \"\\f3cd\"; }\n\n.fa-volume-high {\n  --fa: \"\\f028\"; }\n\n.fa-volume-up {\n  --fa: \"\\f028\"; }\n\n.fa-users-rays {\n  --fa: \"\\e593\"; }\n\n.fa-wallet {\n  --fa: \"\\f555\"; }\n\n.fa-clipboard-check {\n  --fa: \"\\f46c\"; }\n\n.fa-file-audio {\n  --fa: \"\\f1c7\"; }\n\n.fa-burger {\n  --fa: \"\\f805\"; }\n\n.fa-hamburger {\n  --fa: \"\\f805\"; }\n\n.fa-wrench {\n  --fa: \"\\f0ad\"; }\n\n.fa-bugs {\n  --fa: \"\\e4d0\"; }\n\n.fa-rupee-sign {\n  --fa: \"\\f156\"; }\n\n.fa-rupee {\n  --fa: \"\\f156\"; }\n\n.fa-file-image {\n  --fa: \"\\f1c5\"; }\n\n.fa-circle-question {\n  --fa: \"\\f059\"; }\n\n.fa-question-circle {\n  --fa: \"\\f059\"; }\n\n.fa-plane-departure {\n  --fa: \"\\f5b0\"; }\n\n.fa-handshake-slash {\n  --fa: \"\\e060\"; }\n\n.fa-book-bookmark {\n  --fa: \"\\e0bb\"; }\n\n.fa-code-branch {\n  --fa: \"\\f126\"; }\n\n.fa-hat-cowboy {\n  --fa: \"\\f8c0\"; }\n\n.fa-bridge {\n  --fa: \"\\e4c8\"; }\n\n.fa-phone-flip {\n  --fa: \"\\f879\"; }\n\n.fa-phone-alt {\n  --fa: \"\\f879\"; }\n\n.fa-truck-front {\n  --fa: \"\\e2b7\"; }\n\n.fa-cat {\n  --fa: \"\\f6be\"; }\n\n.fa-anchor-circle-exclamation {\n  --fa: \"\\e4ab\"; }\n\n.fa-truck-field {\n  --fa: \"\\e58d\"; }\n\n.fa-route {\n  --fa: \"\\f4d7\"; }\n\n.fa-clipboard-question {\n  --fa: \"\\e4e3\"; }\n\n.fa-panorama {\n  --fa: \"\\e209\"; }\n\n.fa-comment-medical {\n  --fa: \"\\f7f5\"; }\n\n.fa-teeth-open {\n  --fa: \"\\f62f\"; }\n\n.fa-file-circle-minus {\n  --fa: \"\\e4ed\"; }\n\n.fa-tags {\n  --fa: \"\\f02c\"; }\n\n.fa-wine-glass {\n  --fa: \"\\f4e3\"; }\n\n.fa-forward-fast {\n  --fa: \"\\f050\"; }\n\n.fa-fast-forward {\n  --fa: \"\\f050\"; }\n\n.fa-face-meh-blank {\n  --fa: \"\\f5a4\"; }\n\n.fa-meh-blank {\n  --fa: \"\\f5a4\"; }\n\n.fa-square-parking {\n  --fa: \"\\f540\"; }\n\n.fa-parking {\n  --fa: \"\\f540\"; }\n\n.fa-house-signal {\n  --fa: \"\\e012\"; }\n\n.fa-bars-progress {\n  --fa: \"\\f828\"; }\n\n.fa-tasks-alt {\n  --fa: \"\\f828\"; }\n\n.fa-faucet-drip {\n  --fa: \"\\e006\"; }\n\n.fa-cart-flatbed {\n  --fa: \"\\f474\"; }\n\n.fa-dolly-flatbed {\n  --fa: \"\\f474\"; }\n\n.fa-ban-smoking {\n  --fa: \"\\f54d\"; }\n\n.fa-smoking-ban {\n  --fa: \"\\f54d\"; }\n\n.fa-terminal {\n  --fa: \"\\f120\"; }\n\n.fa-mobile-button {\n  --fa: \"\\f10b\"; }\n\n.fa-house-medical-flag {\n  --fa: \"\\e514\"; }\n\n.fa-basket-shopping {\n  --fa: \"\\f291\"; }\n\n.fa-shopping-basket {\n  --fa: \"\\f291\"; }\n\n.fa-tape {\n  --fa: \"\\f4db\"; }\n\n.fa-bus-simple {\n  --fa: \"\\f55e\"; }\n\n.fa-bus-alt {\n  --fa: \"\\f55e\"; }\n\n.fa-eye {\n  --fa: \"\\f06e\"; }\n\n.fa-face-sad-cry {\n  --fa: \"\\f5b3\"; }\n\n.fa-sad-cry {\n  --fa: \"\\f5b3\"; }\n\n.fa-audio-description {\n  --fa: \"\\f29e\"; }\n\n.fa-person-military-to-person {\n  --fa: \"\\e54c\"; }\n\n.fa-file-shield {\n  --fa: \"\\e4f0\"; }\n\n.fa-user-slash {\n  --fa: \"\\f506\"; }\n\n.fa-pen {\n  --fa: \"\\f304\"; }\n\n.fa-tower-observation {\n  --fa: \"\\e586\"; }\n\n.fa-file-code {\n  --fa: \"\\f1c9\"; }\n\n.fa-signal {\n  --fa: \"\\f012\"; }\n\n.fa-signal-5 {\n  --fa: \"\\f012\"; }\n\n.fa-signal-perfect {\n  --fa: \"\\f012\"; }\n\n.fa-bus {\n  --fa: \"\\f207\"; }\n\n.fa-heart-circle-xmark {\n  --fa: \"\\e501\"; }\n\n.fa-house-chimney {\n  --fa: \"\\e3af\"; }\n\n.fa-home-lg {\n  --fa: \"\\e3af\"; }\n\n.fa-window-maximize {\n  --fa: \"\\f2d0\"; }\n\n.fa-face-frown {\n  --fa: \"\\f119\"; }\n\n.fa-frown {\n  --fa: \"\\f119\"; }\n\n.fa-prescription {\n  --fa: \"\\f5b1\"; }\n\n.fa-shop {\n  --fa: \"\\f54f\"; }\n\n.fa-store-alt {\n  --fa: \"\\f54f\"; }\n\n.fa-floppy-disk {\n  --fa: \"\\f0c7\"; }\n\n.fa-save {\n  --fa: \"\\f0c7\"; }\n\n.fa-vihara {\n  --fa: \"\\f6a7\"; }\n\n.fa-scale-unbalanced {\n  --fa: \"\\f515\"; }\n\n.fa-balance-scale-left {\n  --fa: \"\\f515\"; }\n\n.fa-sort-up {\n  --fa: \"\\f0de\"; }\n\n.fa-sort-asc {\n  --fa: \"\\f0de\"; }\n\n.fa-comment-dots {\n  --fa: \"\\f4ad\"; }\n\n.fa-commenting {\n  --fa: \"\\f4ad\"; }\n\n.fa-plant-wilt {\n  --fa: \"\\e5aa\"; }\n\n.fa-diamond {\n  --fa: \"\\f219\"; }\n\n.fa-face-grin-squint {\n  --fa: \"\\f585\"; }\n\n.fa-grin-squint {\n  --fa: \"\\f585\"; }\n\n.fa-hand-holding-dollar {\n  --fa: \"\\f4c0\"; }\n\n.fa-hand-holding-usd {\n  --fa: \"\\f4c0\"; }\n\n.fa-chart-diagram {\n  --fa: \"\\e695\"; }\n\n.fa-bacterium {\n  --fa: \"\\e05a\"; }\n\n.fa-hand-pointer {\n  --fa: \"\\f25a\"; }\n\n.fa-drum-steelpan {\n  --fa: \"\\f56a\"; }\n\n.fa-hand-scissors {\n  --fa: \"\\f257\"; }\n\n.fa-hands-praying {\n  --fa: \"\\f684\"; }\n\n.fa-praying-hands {\n  --fa: \"\\f684\"; }\n\n.fa-arrow-rotate-right {\n  --fa: \"\\f01e\"; }\n\n.fa-arrow-right-rotate {\n  --fa: \"\\f01e\"; }\n\n.fa-arrow-rotate-forward {\n  --fa: \"\\f01e\"; }\n\n.fa-redo {\n  --fa: \"\\f01e\"; }\n\n.fa-biohazard {\n  --fa: \"\\f780\"; }\n\n.fa-location-crosshairs {\n  --fa: \"\\f601\"; }\n\n.fa-location {\n  --fa: \"\\f601\"; }\n\n.fa-mars-double {\n  --fa: \"\\f227\"; }\n\n.fa-child-dress {\n  --fa: \"\\e59c\"; }\n\n.fa-users-between-lines {\n  --fa: \"\\e591\"; }\n\n.fa-lungs-virus {\n  --fa: \"\\e067\"; }\n\n.fa-face-grin-tears {\n  --fa: \"\\f588\"; }\n\n.fa-grin-tears {\n  --fa: \"\\f588\"; }\n\n.fa-phone {\n  --fa: \"\\f095\"; }\n\n.fa-calendar-xmark {\n  --fa: \"\\f273\"; }\n\n.fa-calendar-times {\n  --fa: \"\\f273\"; }\n\n.fa-child-reaching {\n  --fa: \"\\e59d\"; }\n\n.fa-head-side-virus {\n  --fa: \"\\e064\"; }\n\n.fa-user-gear {\n  --fa: \"\\f4fe\"; }\n\n.fa-user-cog {\n  --fa: \"\\f4fe\"; }\n\n.fa-arrow-up-1-9 {\n  --fa: \"\\f163\"; }\n\n.fa-sort-numeric-up {\n  --fa: \"\\f163\"; }\n\n.fa-door-closed {\n  --fa: \"\\f52a\"; }\n\n.fa-shield-virus {\n  --fa: \"\\e06c\"; }\n\n.fa-dice-six {\n  --fa: \"\\f526\"; }\n\n.fa-mosquito-net {\n  --fa: \"\\e52c\"; }\n\n.fa-file-fragment {\n  --fa: \"\\e697\"; }\n\n.fa-bridge-water {\n  --fa: \"\\e4ce\"; }\n\n.fa-person-booth {\n  --fa: \"\\f756\"; }\n\n.fa-text-width {\n  --fa: \"\\f035\"; }\n\n.fa-hat-wizard {\n  --fa: \"\\f6e8\"; }\n\n.fa-pen-fancy {\n  --fa: \"\\f5ac\"; }\n\n.fa-person-digging {\n  --fa: \"\\f85e\"; }\n\n.fa-digging {\n  --fa: \"\\f85e\"; }\n\n.fa-trash {\n  --fa: \"\\f1f8\"; }\n\n.fa-gauge-simple {\n  --fa: \"\\f629\"; }\n\n.fa-gauge-simple-med {\n  --fa: \"\\f629\"; }\n\n.fa-tachometer-average {\n  --fa: \"\\f629\"; }\n\n.fa-book-medical {\n  --fa: \"\\f7e6\"; }\n\n.fa-poo {\n  --fa: \"\\f2fe\"; }\n\n.fa-quote-right {\n  --fa: \"\\f10e\"; }\n\n.fa-quote-right-alt {\n  --fa: \"\\f10e\"; }\n\n.fa-shirt {\n  --fa: \"\\f553\"; }\n\n.fa-t-shirt {\n  --fa: \"\\f553\"; }\n\n.fa-tshirt {\n  --fa: \"\\f553\"; }\n\n.fa-cubes {\n  --fa: \"\\f1b3\"; }\n\n.fa-divide {\n  --fa: \"\\f529\"; }\n\n.fa-tenge-sign {\n  --fa: \"\\f7d7\"; }\n\n.fa-tenge {\n  --fa: \"\\f7d7\"; }\n\n.fa-headphones {\n  --fa: \"\\f025\"; }\n\n.fa-hands-holding {\n  --fa: \"\\f4c2\"; }\n\n.fa-hands-clapping {\n  --fa: \"\\e1a8\"; }\n\n.fa-republican {\n  --fa: \"\\f75e\"; }\n\n.fa-arrow-left {\n  --fa: \"\\f060\"; }\n\n.fa-person-circle-xmark {\n  --fa: \"\\e543\"; }\n\n.fa-ruler {\n  --fa: \"\\f545\"; }\n\n.fa-align-left {\n  --fa: \"\\f036\"; }\n\n.fa-dice-d6 {\n  --fa: \"\\f6d1\"; }\n\n.fa-restroom {\n  --fa: \"\\f7bd\"; }\n\n.fa-j {\n  --fa: \"\\4a\"; }\n\n.fa-users-viewfinder {\n  --fa: \"\\e595\"; }\n\n.fa-file-video {\n  --fa: \"\\f1c8\"; }\n\n.fa-up-right-from-square {\n  --fa: \"\\f35d\"; }\n\n.fa-external-link-alt {\n  --fa: \"\\f35d\"; }\n\n.fa-table-cells {\n  --fa: \"\\f00a\"; }\n\n.fa-th {\n  --fa: \"\\f00a\"; }\n\n.fa-file-pdf {\n  --fa: \"\\f1c1\"; }\n\n.fa-book-bible {\n  --fa: \"\\f647\"; }\n\n.fa-bible {\n  --fa: \"\\f647\"; }\n\n.fa-o {\n  --fa: \"\\4f\"; }\n\n.fa-suitcase-medical {\n  --fa: \"\\f0fa\"; }\n\n.fa-medkit {\n  --fa: \"\\f0fa\"; }\n\n.fa-user-secret {\n  --fa: \"\\f21b\"; }\n\n.fa-otter {\n  --fa: \"\\f700\"; }\n\n.fa-person-dress {\n  --fa: \"\\f182\"; }\n\n.fa-female {\n  --fa: \"\\f182\"; }\n\n.fa-comment-dollar {\n  --fa: \"\\f651\"; }\n\n.fa-business-time {\n  --fa: \"\\f64a\"; }\n\n.fa-briefcase-clock {\n  --fa: \"\\f64a\"; }\n\n.fa-table-cells-large {\n  --fa: \"\\f009\"; }\n\n.fa-th-large {\n  --fa: \"\\f009\"; }\n\n.fa-book-tanakh {\n  --fa: \"\\f827\"; }\n\n.fa-tanakh {\n  --fa: \"\\f827\"; }\n\n.fa-phone-volume {\n  --fa: \"\\f2a0\"; }\n\n.fa-volume-control-phone {\n  --fa: \"\\f2a0\"; }\n\n.fa-hat-cowboy-side {\n  --fa: \"\\f8c1\"; }\n\n.fa-clipboard-user {\n  --fa: \"\\f7f3\"; }\n\n.fa-child {\n  --fa: \"\\f1ae\"; }\n\n.fa-lira-sign {\n  --fa: \"\\f195\"; }\n\n.fa-satellite {\n  --fa: \"\\f7bf\"; }\n\n.fa-plane-lock {\n  --fa: \"\\e558\"; }\n\n.fa-tag {\n  --fa: \"\\f02b\"; }\n\n.fa-comment {\n  --fa: \"\\f075\"; }\n\n.fa-cake-candles {\n  --fa: \"\\f1fd\"; }\n\n.fa-birthday-cake {\n  --fa: \"\\f1fd\"; }\n\n.fa-cake {\n  --fa: \"\\f1fd\"; }\n\n.fa-envelope {\n  --fa: \"\\f0e0\"; }\n\n.fa-angles-up {\n  --fa: \"\\f102\"; }\n\n.fa-angle-double-up {\n  --fa: \"\\f102\"; }\n\n.fa-paperclip {\n  --fa: \"\\f0c6\"; }\n\n.fa-arrow-right-to-city {\n  --fa: \"\\e4b3\"; }\n\n.fa-ribbon {\n  --fa: \"\\f4d6\"; }\n\n.fa-lungs {\n  --fa: \"\\f604\"; }\n\n.fa-arrow-up-9-1 {\n  --fa: \"\\f887\"; }\n\n.fa-sort-numeric-up-alt {\n  --fa: \"\\f887\"; }\n\n.fa-litecoin-sign {\n  --fa: \"\\e1d3\"; }\n\n.fa-border-none {\n  --fa: \"\\f850\"; }\n\n.fa-circle-nodes {\n  --fa: \"\\e4e2\"; }\n\n.fa-parachute-box {\n  --fa: \"\\f4cd\"; }\n\n.fa-indent {\n  --fa: \"\\f03c\"; }\n\n.fa-truck-field-un {\n  --fa: \"\\e58e\"; }\n\n.fa-hourglass {\n  --fa: \"\\f254\"; }\n\n.fa-hourglass-empty {\n  --fa: \"\\f254\"; }\n\n.fa-mountain {\n  --fa: \"\\f6fc\"; }\n\n.fa-user-doctor {\n  --fa: \"\\f0f0\"; }\n\n.fa-user-md {\n  --fa: \"\\f0f0\"; }\n\n.fa-circle-info {\n  --fa: \"\\f05a\"; }\n\n.fa-info-circle {\n  --fa: \"\\f05a\"; }\n\n.fa-cloud-meatball {\n  --fa: \"\\f73b\"; }\n\n.fa-camera {\n  --fa: \"\\f030\"; }\n\n.fa-camera-alt {\n  --fa: \"\\f030\"; }\n\n.fa-square-virus {\n  --fa: \"\\e578\"; }\n\n.fa-meteor {\n  --fa: \"\\f753\"; }\n\n.fa-car-on {\n  --fa: \"\\e4dd\"; }\n\n.fa-sleigh {\n  --fa: \"\\f7cc\"; }\n\n.fa-arrow-down-1-9 {\n  --fa: \"\\f162\"; }\n\n.fa-sort-numeric-asc {\n  --fa: \"\\f162\"; }\n\n.fa-sort-numeric-down {\n  --fa: \"\\f162\"; }\n\n.fa-hand-holding-droplet {\n  --fa: \"\\f4c1\"; }\n\n.fa-hand-holding-water {\n  --fa: \"\\f4c1\"; }\n\n.fa-water {\n  --fa: \"\\f773\"; }\n\n.fa-calendar-check {\n  --fa: \"\\f274\"; }\n\n.fa-braille {\n  --fa: \"\\f2a1\"; }\n\n.fa-prescription-bottle-medical {\n  --fa: \"\\f486\"; }\n\n.fa-prescription-bottle-alt {\n  --fa: \"\\f486\"; }\n\n.fa-landmark {\n  --fa: \"\\f66f\"; }\n\n.fa-truck {\n  --fa: \"\\f0d1\"; }\n\n.fa-crosshairs {\n  --fa: \"\\f05b\"; }\n\n.fa-person-cane {\n  --fa: \"\\e53c\"; }\n\n.fa-tent {\n  --fa: \"\\e57d\"; }\n\n.fa-vest-patches {\n  --fa: \"\\e086\"; }\n\n.fa-check-double {\n  --fa: \"\\f560\"; }\n\n.fa-arrow-down-a-z {\n  --fa: \"\\f15d\"; }\n\n.fa-sort-alpha-asc {\n  --fa: \"\\f15d\"; }\n\n.fa-sort-alpha-down {\n  --fa: \"\\f15d\"; }\n\n.fa-money-bill-wheat {\n  --fa: \"\\e52a\"; }\n\n.fa-cookie {\n  --fa: \"\\f563\"; }\n\n.fa-arrow-rotate-left {\n  --fa: \"\\f0e2\"; }\n\n.fa-arrow-left-rotate {\n  --fa: \"\\f0e2\"; }\n\n.fa-arrow-rotate-back {\n  --fa: \"\\f0e2\"; }\n\n.fa-arrow-rotate-backward {\n  --fa: \"\\f0e2\"; }\n\n.fa-undo {\n  --fa: \"\\f0e2\"; }\n\n.fa-hard-drive {\n  --fa: \"\\f0a0\"; }\n\n.fa-hdd {\n  --fa: \"\\f0a0\"; }\n\n.fa-face-grin-squint-tears {\n  --fa: \"\\f586\"; }\n\n.fa-grin-squint-tears {\n  --fa: \"\\f586\"; }\n\n.fa-dumbbell {\n  --fa: \"\\f44b\"; }\n\n.fa-rectangle-list {\n  --fa: \"\\f022\"; }\n\n.fa-list-alt {\n  --fa: \"\\f022\"; }\n\n.fa-tarp-droplet {\n  --fa: \"\\e57c\"; }\n\n.fa-house-medical-circle-check {\n  --fa: \"\\e511\"; }\n\n.fa-person-skiing-nordic {\n  --fa: \"\\f7ca\"; }\n\n.fa-skiing-nordic {\n  --fa: \"\\f7ca\"; }\n\n.fa-calendar-plus {\n  --fa: \"\\f271\"; }\n\n.fa-plane-arrival {\n  --fa: \"\\f5af\"; }\n\n.fa-circle-left {\n  --fa: \"\\f359\"; }\n\n.fa-arrow-alt-circle-left {\n  --fa: \"\\f359\"; }\n\n.fa-train-subway {\n  --fa: \"\\f239\"; }\n\n.fa-subway {\n  --fa: \"\\f239\"; }\n\n.fa-chart-gantt {\n  --fa: \"\\e0e4\"; }\n\n.fa-indian-rupee-sign {\n  --fa: \"\\e1bc\"; }\n\n.fa-indian-rupee {\n  --fa: \"\\e1bc\"; }\n\n.fa-inr {\n  --fa: \"\\e1bc\"; }\n\n.fa-crop-simple {\n  --fa: \"\\f565\"; }\n\n.fa-crop-alt {\n  --fa: \"\\f565\"; }\n\n.fa-money-bill-1 {\n  --fa: \"\\f3d1\"; }\n\n.fa-money-bill-alt {\n  --fa: \"\\f3d1\"; }\n\n.fa-left-long {\n  --fa: \"\\f30a\"; }\n\n.fa-long-arrow-alt-left {\n  --fa: \"\\f30a\"; }\n\n.fa-dna {\n  --fa: \"\\f471\"; }\n\n.fa-virus-slash {\n  --fa: \"\\e075\"; }\n\n.fa-minus {\n  --fa: \"\\f068\"; }\n\n.fa-subtract {\n  --fa: \"\\f068\"; }\n\n.fa-chess {\n  --fa: \"\\f439\"; }\n\n.fa-arrow-left-long {\n  --fa: \"\\f177\"; }\n\n.fa-long-arrow-left {\n  --fa: \"\\f177\"; }\n\n.fa-plug-circle-check {\n  --fa: \"\\e55c\"; }\n\n.fa-street-view {\n  --fa: \"\\f21d\"; }\n\n.fa-franc-sign {\n  --fa: \"\\e18f\"; }\n\n.fa-volume-off {\n  --fa: \"\\f026\"; }\n\n.fa-hands-asl-interpreting {\n  --fa: \"\\f2a3\"; }\n\n.fa-american-sign-language-interpreting {\n  --fa: \"\\f2a3\"; }\n\n.fa-asl-interpreting {\n  --fa: \"\\f2a3\"; }\n\n.fa-hands-american-sign-language-interpreting {\n  --fa: \"\\f2a3\"; }\n\n.fa-gear {\n  --fa: \"\\f013\"; }\n\n.fa-cog {\n  --fa: \"\\f013\"; }\n\n.fa-droplet-slash {\n  --fa: \"\\f5c7\"; }\n\n.fa-tint-slash {\n  --fa: \"\\f5c7\"; }\n\n.fa-mosque {\n  --fa: \"\\f678\"; }\n\n.fa-mosquito {\n  --fa: \"\\e52b\"; }\n\n.fa-star-of-david {\n  --fa: \"\\f69a\"; }\n\n.fa-person-military-rifle {\n  --fa: \"\\e54b\"; }\n\n.fa-cart-shopping {\n  --fa: \"\\f07a\"; }\n\n.fa-shopping-cart {\n  --fa: \"\\f07a\"; }\n\n.fa-vials {\n  --fa: \"\\f493\"; }\n\n.fa-plug-circle-plus {\n  --fa: \"\\e55f\"; }\n\n.fa-place-of-worship {\n  --fa: \"\\f67f\"; }\n\n.fa-grip-vertical {\n  --fa: \"\\f58e\"; }\n\n.fa-hexagon-nodes {\n  --fa: \"\\e699\"; }\n\n.fa-arrow-turn-up {\n  --fa: \"\\f148\"; }\n\n.fa-level-up {\n  --fa: \"\\f148\"; }\n\n.fa-u {\n  --fa: \"\\55\"; }\n\n.fa-square-root-variable {\n  --fa: \"\\f698\"; }\n\n.fa-square-root-alt {\n  --fa: \"\\f698\"; }\n\n.fa-clock {\n  --fa: \"\\f017\"; }\n\n.fa-clock-four {\n  --fa: \"\\f017\"; }\n\n.fa-backward-step {\n  --fa: \"\\f048\"; }\n\n.fa-step-backward {\n  --fa: \"\\f048\"; }\n\n.fa-pallet {\n  --fa: \"\\f482\"; }\n\n.fa-faucet {\n  --fa: \"\\e005\"; }\n\n.fa-baseball-bat-ball {\n  --fa: \"\\f432\"; }\n\n.fa-s {\n  --fa: \"\\53\"; }\n\n.fa-timeline {\n  --fa: \"\\e29c\"; }\n\n.fa-keyboard {\n  --fa: \"\\f11c\"; }\n\n.fa-caret-down {\n  --fa: \"\\f0d7\"; }\n\n.fa-house-chimney-medical {\n  --fa: \"\\f7f2\"; }\n\n.fa-clinic-medical {\n  --fa: \"\\f7f2\"; }\n\n.fa-temperature-three-quarters {\n  --fa: \"\\f2c8\"; }\n\n.fa-temperature-3 {\n  --fa: \"\\f2c8\"; }\n\n.fa-thermometer-3 {\n  --fa: \"\\f2c8\"; }\n\n.fa-thermometer-three-quarters {\n  --fa: \"\\f2c8\"; }\n\n.fa-mobile-screen {\n  --fa: \"\\f3cf\"; }\n\n.fa-mobile-android-alt {\n  --fa: \"\\f3cf\"; }\n\n.fa-plane-up {\n  --fa: \"\\e22d\"; }\n\n.fa-piggy-bank {\n  --fa: \"\\f4d3\"; }\n\n.fa-battery-half {\n  --fa: \"\\f242\"; }\n\n.fa-battery-3 {\n  --fa: \"\\f242\"; }\n\n.fa-mountain-city {\n  --fa: \"\\e52e\"; }\n\n.fa-coins {\n  --fa: \"\\f51e\"; }\n\n.fa-khanda {\n  --fa: \"\\f66d\"; }\n\n.fa-sliders {\n  --fa: \"\\f1de\"; }\n\n.fa-sliders-h {\n  --fa: \"\\f1de\"; }\n\n.fa-folder-tree {\n  --fa: \"\\f802\"; }\n\n.fa-network-wired {\n  --fa: \"\\f6ff\"; }\n\n.fa-map-pin {\n  --fa: \"\\f276\"; }\n\n.fa-hamsa {\n  --fa: \"\\f665\"; }\n\n.fa-cent-sign {\n  --fa: \"\\e3f5\"; }\n\n.fa-flask {\n  --fa: \"\\f0c3\"; }\n\n.fa-person-pregnant {\n  --fa: \"\\e31e\"; }\n\n.fa-wand-sparkles {\n  --fa: \"\\f72b\"; }\n\n.fa-ellipsis-vertical {\n  --fa: \"\\f142\"; }\n\n.fa-ellipsis-v {\n  --fa: \"\\f142\"; }\n\n.fa-ticket {\n  --fa: \"\\f145\"; }\n\n.fa-power-off {\n  --fa: \"\\f011\"; }\n\n.fa-right-long {\n  --fa: \"\\f30b\"; }\n\n.fa-long-arrow-alt-right {\n  --fa: \"\\f30b\"; }\n\n.fa-flag-usa {\n  --fa: \"\\f74d\"; }\n\n.fa-laptop-file {\n  --fa: \"\\e51d\"; }\n\n.fa-tty {\n  --fa: \"\\f1e4\"; }\n\n.fa-teletype {\n  --fa: \"\\f1e4\"; }\n\n.fa-diagram-next {\n  --fa: \"\\e476\"; }\n\n.fa-person-rifle {\n  --fa: \"\\e54e\"; }\n\n.fa-house-medical-circle-exclamation {\n  --fa: \"\\e512\"; }\n\n.fa-closed-captioning {\n  --fa: \"\\f20a\"; }\n\n.fa-person-hiking {\n  --fa: \"\\f6ec\"; }\n\n.fa-hiking {\n  --fa: \"\\f6ec\"; }\n\n.fa-venus-double {\n  --fa: \"\\f226\"; }\n\n.fa-images {\n  --fa: \"\\f302\"; }\n\n.fa-calculator {\n  --fa: \"\\f1ec\"; }\n\n.fa-people-pulling {\n  --fa: \"\\e535\"; }\n\n.fa-n {\n  --fa: \"\\4e\"; }\n\n.fa-cable-car {\n  --fa: \"\\f7da\"; }\n\n.fa-tram {\n  --fa: \"\\f7da\"; }\n\n.fa-cloud-rain {\n  --fa: \"\\f73d\"; }\n\n.fa-building-circle-xmark {\n  --fa: \"\\e4d4\"; }\n\n.fa-ship {\n  --fa: \"\\f21a\"; }\n\n.fa-arrows-down-to-line {\n  --fa: \"\\e4b8\"; }\n\n.fa-download {\n  --fa: \"\\f019\"; }\n\n.fa-face-grin {\n  --fa: \"\\f580\"; }\n\n.fa-grin {\n  --fa: \"\\f580\"; }\n\n.fa-delete-left {\n  --fa: \"\\f55a\"; }\n\n.fa-backspace {\n  --fa: \"\\f55a\"; }\n\n.fa-eye-dropper {\n  --fa: \"\\f1fb\"; }\n\n.fa-eye-dropper-empty {\n  --fa: \"\\f1fb\"; }\n\n.fa-eyedropper {\n  --fa: \"\\f1fb\"; }\n\n.fa-file-circle-check {\n  --fa: \"\\e5a0\"; }\n\n.fa-forward {\n  --fa: \"\\f04e\"; }\n\n.fa-mobile {\n  --fa: \"\\f3ce\"; }\n\n.fa-mobile-android {\n  --fa: \"\\f3ce\"; }\n\n.fa-mobile-phone {\n  --fa: \"\\f3ce\"; }\n\n.fa-face-meh {\n  --fa: \"\\f11a\"; }\n\n.fa-meh {\n  --fa: \"\\f11a\"; }\n\n.fa-align-center {\n  --fa: \"\\f037\"; }\n\n.fa-book-skull {\n  --fa: \"\\f6b7\"; }\n\n.fa-book-dead {\n  --fa: \"\\f6b7\"; }\n\n.fa-id-card {\n  --fa: \"\\f2c2\"; }\n\n.fa-drivers-license {\n  --fa: \"\\f2c2\"; }\n\n.fa-outdent {\n  --fa: \"\\f03b\"; }\n\n.fa-dedent {\n  --fa: \"\\f03b\"; }\n\n.fa-heart-circle-exclamation {\n  --fa: \"\\e4fe\"; }\n\n.fa-house {\n  --fa: \"\\f015\"; }\n\n.fa-home {\n  --fa: \"\\f015\"; }\n\n.fa-home-alt {\n  --fa: \"\\f015\"; }\n\n.fa-home-lg-alt {\n  --fa: \"\\f015\"; }\n\n.fa-calendar-week {\n  --fa: \"\\f784\"; }\n\n.fa-laptop-medical {\n  --fa: \"\\f812\"; }\n\n.fa-b {\n  --fa: \"\\42\"; }\n\n.fa-file-medical {\n  --fa: \"\\f477\"; }\n\n.fa-dice-one {\n  --fa: \"\\f525\"; }\n\n.fa-kiwi-bird {\n  --fa: \"\\f535\"; }\n\n.fa-arrow-right-arrow-left {\n  --fa: \"\\f0ec\"; }\n\n.fa-exchange {\n  --fa: \"\\f0ec\"; }\n\n.fa-rotate-right {\n  --fa: \"\\f2f9\"; }\n\n.fa-redo-alt {\n  --fa: \"\\f2f9\"; }\n\n.fa-rotate-forward {\n  --fa: \"\\f2f9\"; }\n\n.fa-utensils {\n  --fa: \"\\f2e7\"; }\n\n.fa-cutlery {\n  --fa: \"\\f2e7\"; }\n\n.fa-arrow-up-wide-short {\n  --fa: \"\\f161\"; }\n\n.fa-sort-amount-up {\n  --fa: \"\\f161\"; }\n\n.fa-mill-sign {\n  --fa: \"\\e1ed\"; }\n\n.fa-bowl-rice {\n  --fa: \"\\e2eb\"; }\n\n.fa-skull {\n  --fa: \"\\f54c\"; }\n\n.fa-tower-broadcast {\n  --fa: \"\\f519\"; }\n\n.fa-broadcast-tower {\n  --fa: \"\\f519\"; }\n\n.fa-truck-pickup {\n  --fa: \"\\f63c\"; }\n\n.fa-up-long {\n  --fa: \"\\f30c\"; }\n\n.fa-long-arrow-alt-up {\n  --fa: \"\\f30c\"; }\n\n.fa-stop {\n  --fa: \"\\f04d\"; }\n\n.fa-code-merge {\n  --fa: \"\\f387\"; }\n\n.fa-upload {\n  --fa: \"\\f093\"; }\n\n.fa-hurricane {\n  --fa: \"\\f751\"; }\n\n.fa-mound {\n  --fa: \"\\e52d\"; }\n\n.fa-toilet-portable {\n  --fa: \"\\e583\"; }\n\n.fa-compact-disc {\n  --fa: \"\\f51f\"; }\n\n.fa-file-arrow-down {\n  --fa: \"\\f56d\"; }\n\n.fa-file-download {\n  --fa: \"\\f56d\"; }\n\n.fa-caravan {\n  --fa: \"\\f8ff\"; }\n\n.fa-shield-cat {\n  --fa: \"\\e572\"; }\n\n.fa-bolt {\n  --fa: \"\\f0e7\"; }\n\n.fa-zap {\n  --fa: \"\\f0e7\"; }\n\n.fa-glass-water {\n  --fa: \"\\e4f4\"; }\n\n.fa-oil-well {\n  --fa: \"\\e532\"; }\n\n.fa-vault {\n  --fa: \"\\e2c5\"; }\n\n.fa-mars {\n  --fa: \"\\f222\"; }\n\n.fa-toilet {\n  --fa: \"\\f7d8\"; }\n\n.fa-plane-circle-xmark {\n  --fa: \"\\e557\"; }\n\n.fa-yen-sign {\n  --fa: \"\\f157\"; }\n\n.fa-cny {\n  --fa: \"\\f157\"; }\n\n.fa-jpy {\n  --fa: \"\\f157\"; }\n\n.fa-rmb {\n  --fa: \"\\f157\"; }\n\n.fa-yen {\n  --fa: \"\\f157\"; }\n\n.fa-ruble-sign {\n  --fa: \"\\f158\"; }\n\n.fa-rouble {\n  --fa: \"\\f158\"; }\n\n.fa-rub {\n  --fa: \"\\f158\"; }\n\n.fa-ruble {\n  --fa: \"\\f158\"; }\n\n.fa-sun {\n  --fa: \"\\f185\"; }\n\n.fa-guitar {\n  --fa: \"\\f7a6\"; }\n\n.fa-face-laugh-wink {\n  --fa: \"\\f59c\"; }\n\n.fa-laugh-wink {\n  --fa: \"\\f59c\"; }\n\n.fa-horse-head {\n  --fa: \"\\f7ab\"; }\n\n.fa-bore-hole {\n  --fa: \"\\e4c3\"; }\n\n.fa-industry {\n  --fa: \"\\f275\"; }\n\n.fa-circle-down {\n  --fa: \"\\f358\"; }\n\n.fa-arrow-alt-circle-down {\n  --fa: \"\\f358\"; }\n\n.fa-arrows-turn-to-dots {\n  --fa: \"\\e4c1\"; }\n\n.fa-florin-sign {\n  --fa: \"\\e184\"; }\n\n.fa-arrow-down-short-wide {\n  --fa: \"\\f884\"; }\n\n.fa-sort-amount-desc {\n  --fa: \"\\f884\"; }\n\n.fa-sort-amount-down-alt {\n  --fa: \"\\f884\"; }\n\n.fa-less-than {\n  --fa: \"\\3c\"; }\n\n.fa-angle-down {\n  --fa: \"\\f107\"; }\n\n.fa-car-tunnel {\n  --fa: \"\\e4de\"; }\n\n.fa-head-side-cough {\n  --fa: \"\\e061\"; }\n\n.fa-grip-lines {\n  --fa: \"\\f7a4\"; }\n\n.fa-thumbs-down {\n  --fa: \"\\f165\"; }\n\n.fa-user-lock {\n  --fa: \"\\f502\"; }\n\n.fa-arrow-right-long {\n  --fa: \"\\f178\"; }\n\n.fa-long-arrow-right {\n  --fa: \"\\f178\"; }\n\n.fa-anchor-circle-xmark {\n  --fa: \"\\e4ac\"; }\n\n.fa-ellipsis {\n  --fa: \"\\f141\"; }\n\n.fa-ellipsis-h {\n  --fa: \"\\f141\"; }\n\n.fa-chess-pawn {\n  --fa: \"\\f443\"; }\n\n.fa-kit-medical {\n  --fa: \"\\f479\"; }\n\n.fa-first-aid {\n  --fa: \"\\f479\"; }\n\n.fa-person-through-window {\n  --fa: \"\\e5a9\"; }\n\n.fa-toolbox {\n  --fa: \"\\f552\"; }\n\n.fa-hands-holding-circle {\n  --fa: \"\\e4fb\"; }\n\n.fa-bug {\n  --fa: \"\\f188\"; }\n\n.fa-credit-card {\n  --fa: \"\\f09d\"; }\n\n.fa-credit-card-alt {\n  --fa: \"\\f09d\"; }\n\n.fa-car {\n  --fa: \"\\f1b9\"; }\n\n.fa-automobile {\n  --fa: \"\\f1b9\"; }\n\n.fa-hand-holding-hand {\n  --fa: \"\\e4f7\"; }\n\n.fa-book-open-reader {\n  --fa: \"\\f5da\"; }\n\n.fa-book-reader {\n  --fa: \"\\f5da\"; }\n\n.fa-mountain-sun {\n  --fa: \"\\e52f\"; }\n\n.fa-arrows-left-right-to-line {\n  --fa: \"\\e4ba\"; }\n\n.fa-dice-d20 {\n  --fa: \"\\f6cf\"; }\n\n.fa-truck-droplet {\n  --fa: \"\\e58c\"; }\n\n.fa-file-circle-xmark {\n  --fa: \"\\e5a1\"; }\n\n.fa-temperature-arrow-up {\n  --fa: \"\\e040\"; }\n\n.fa-temperature-up {\n  --fa: \"\\e040\"; }\n\n.fa-medal {\n  --fa: \"\\f5a2\"; }\n\n.fa-bed {\n  --fa: \"\\f236\"; }\n\n.fa-square-h {\n  --fa: \"\\f0fd\"; }\n\n.fa-h-square {\n  --fa: \"\\f0fd\"; }\n\n.fa-podcast {\n  --fa: \"\\f2ce\"; }\n\n.fa-temperature-full {\n  --fa: \"\\f2c7\"; }\n\n.fa-temperature-4 {\n  --fa: \"\\f2c7\"; }\n\n.fa-thermometer-4 {\n  --fa: \"\\f2c7\"; }\n\n.fa-thermometer-full {\n  --fa: \"\\f2c7\"; }\n\n.fa-bell {\n  --fa: \"\\f0f3\"; }\n\n.fa-superscript {\n  --fa: \"\\f12b\"; }\n\n.fa-plug-circle-xmark {\n  --fa: \"\\e560\"; }\n\n.fa-star-of-life {\n  --fa: \"\\f621\"; }\n\n.fa-phone-slash {\n  --fa: \"\\f3dd\"; }\n\n.fa-paint-roller {\n  --fa: \"\\f5aa\"; }\n\n.fa-handshake-angle {\n  --fa: \"\\f4c4\"; }\n\n.fa-hands-helping {\n  --fa: \"\\f4c4\"; }\n\n.fa-location-dot {\n  --fa: \"\\f3c5\"; }\n\n.fa-map-marker-alt {\n  --fa: \"\\f3c5\"; }\n\n.fa-file {\n  --fa: \"\\f15b\"; }\n\n.fa-greater-than {\n  --fa: \"\\3e\"; }\n\n.fa-person-swimming {\n  --fa: \"\\f5c4\"; }\n\n.fa-swimmer {\n  --fa: \"\\f5c4\"; }\n\n.fa-arrow-down {\n  --fa: \"\\f063\"; }\n\n.fa-droplet {\n  --fa: \"\\f043\"; }\n\n.fa-tint {\n  --fa: \"\\f043\"; }\n\n.fa-eraser {\n  --fa: \"\\f12d\"; }\n\n.fa-earth-americas {\n  --fa: \"\\f57d\"; }\n\n.fa-earth {\n  --fa: \"\\f57d\"; }\n\n.fa-earth-america {\n  --fa: \"\\f57d\"; }\n\n.fa-globe-americas {\n  --fa: \"\\f57d\"; }\n\n.fa-person-burst {\n  --fa: \"\\e53b\"; }\n\n.fa-dove {\n  --fa: \"\\f4ba\"; }\n\n.fa-battery-empty {\n  --fa: \"\\f244\"; }\n\n.fa-battery-0 {\n  --fa: \"\\f244\"; }\n\n.fa-socks {\n  --fa: \"\\f696\"; }\n\n.fa-inbox {\n  --fa: \"\\f01c\"; }\n\n.fa-section {\n  --fa: \"\\e447\"; }\n\n.fa-gauge-high {\n  --fa: \"\\f625\"; }\n\n.fa-tachometer-alt {\n  --fa: \"\\f625\"; }\n\n.fa-tachometer-alt-fast {\n  --fa: \"\\f625\"; }\n\n.fa-envelope-open-text {\n  --fa: \"\\f658\"; }\n\n.fa-hospital {\n  --fa: \"\\f0f8\"; }\n\n.fa-hospital-alt {\n  --fa: \"\\f0f8\"; }\n\n.fa-hospital-wide {\n  --fa: \"\\f0f8\"; }\n\n.fa-wine-bottle {\n  --fa: \"\\f72f\"; }\n\n.fa-chess-rook {\n  --fa: \"\\f447\"; }\n\n.fa-bars-staggered {\n  --fa: \"\\f550\"; }\n\n.fa-reorder {\n  --fa: \"\\f550\"; }\n\n.fa-stream {\n  --fa: \"\\f550\"; }\n\n.fa-dharmachakra {\n  --fa: \"\\f655\"; }\n\n.fa-hotdog {\n  --fa: \"\\f80f\"; }\n\n.fa-person-walking-with-cane {\n  --fa: \"\\f29d\"; }\n\n.fa-blind {\n  --fa: \"\\f29d\"; }\n\n.fa-drum {\n  --fa: \"\\f569\"; }\n\n.fa-ice-cream {\n  --fa: \"\\f810\"; }\n\n.fa-heart-circle-bolt {\n  --fa: \"\\e4fc\"; }\n\n.fa-fax {\n  --fa: \"\\f1ac\"; }\n\n.fa-paragraph {\n  --fa: \"\\f1dd\"; }\n\n.fa-check-to-slot {\n  --fa: \"\\f772\"; }\n\n.fa-vote-yea {\n  --fa: \"\\f772\"; }\n\n.fa-star-half {\n  --fa: \"\\f089\"; }\n\n.fa-boxes-stacked {\n  --fa: \"\\f468\"; }\n\n.fa-boxes {\n  --fa: \"\\f468\"; }\n\n.fa-boxes-alt {\n  --fa: \"\\f468\"; }\n\n.fa-link {\n  --fa: \"\\f0c1\"; }\n\n.fa-chain {\n  --fa: \"\\f0c1\"; }\n\n.fa-ear-listen {\n  --fa: \"\\f2a2\"; }\n\n.fa-assistive-listening-systems {\n  --fa: \"\\f2a2\"; }\n\n.fa-tree-city {\n  --fa: \"\\e587\"; }\n\n.fa-play {\n  --fa: \"\\f04b\"; }\n\n.fa-font {\n  --fa: \"\\f031\"; }\n\n.fa-table-cells-row-lock {\n  --fa: \"\\e67a\"; }\n\n.fa-rupiah-sign {\n  --fa: \"\\e23d\"; }\n\n.fa-magnifying-glass {\n  --fa: \"\\f002\"; }\n\n.fa-search {\n  --fa: \"\\f002\"; }\n\n.fa-table-tennis-paddle-ball {\n  --fa: \"\\f45d\"; }\n\n.fa-ping-pong-paddle-ball {\n  --fa: \"\\f45d\"; }\n\n.fa-table-tennis {\n  --fa: \"\\f45d\"; }\n\n.fa-person-dots-from-line {\n  --fa: \"\\f470\"; }\n\n.fa-diagnoses {\n  --fa: \"\\f470\"; }\n\n.fa-trash-can-arrow-up {\n  --fa: \"\\f82a\"; }\n\n.fa-trash-restore-alt {\n  --fa: \"\\f82a\"; }\n\n.fa-naira-sign {\n  --fa: \"\\e1f6\"; }\n\n.fa-cart-arrow-down {\n  --fa: \"\\f218\"; }\n\n.fa-walkie-talkie {\n  --fa: \"\\f8ef\"; }\n\n.fa-file-pen {\n  --fa: \"\\f31c\"; }\n\n.fa-file-edit {\n  --fa: \"\\f31c\"; }\n\n.fa-receipt {\n  --fa: \"\\f543\"; }\n\n.fa-square-pen {\n  --fa: \"\\f14b\"; }\n\n.fa-pen-square {\n  --fa: \"\\f14b\"; }\n\n.fa-pencil-square {\n  --fa: \"\\f14b\"; }\n\n.fa-suitcase-rolling {\n  --fa: \"\\f5c1\"; }\n\n.fa-person-circle-exclamation {\n  --fa: \"\\e53f\"; }\n\n.fa-chevron-down {\n  --fa: \"\\f078\"; }\n\n.fa-battery-full {\n  --fa: \"\\f240\"; }\n\n.fa-battery {\n  --fa: \"\\f240\"; }\n\n.fa-battery-5 {\n  --fa: \"\\f240\"; }\n\n.fa-skull-crossbones {\n  --fa: \"\\f714\"; }\n\n.fa-code-compare {\n  --fa: \"\\e13a\"; }\n\n.fa-list-ul {\n  --fa: \"\\f0ca\"; }\n\n.fa-list-dots {\n  --fa: \"\\f0ca\"; }\n\n.fa-school-lock {\n  --fa: \"\\e56f\"; }\n\n.fa-tower-cell {\n  --fa: \"\\e585\"; }\n\n.fa-down-long {\n  --fa: \"\\f309\"; }\n\n.fa-long-arrow-alt-down {\n  --fa: \"\\f309\"; }\n\n.fa-ranking-star {\n  --fa: \"\\e561\"; }\n\n.fa-chess-king {\n  --fa: \"\\f43f\"; }\n\n.fa-person-harassing {\n  --fa: \"\\e549\"; }\n\n.fa-brazilian-real-sign {\n  --fa: \"\\e46c\"; }\n\n.fa-landmark-dome {\n  --fa: \"\\f752\"; }\n\n.fa-landmark-alt {\n  --fa: \"\\f752\"; }\n\n.fa-arrow-up {\n  --fa: \"\\f062\"; }\n\n.fa-tv {\n  --fa: \"\\f26c\"; }\n\n.fa-television {\n  --fa: \"\\f26c\"; }\n\n.fa-tv-alt {\n  --fa: \"\\f26c\"; }\n\n.fa-shrimp {\n  --fa: \"\\e448\"; }\n\n.fa-list-check {\n  --fa: \"\\f0ae\"; }\n\n.fa-tasks {\n  --fa: \"\\f0ae\"; }\n\n.fa-jug-detergent {\n  --fa: \"\\e519\"; }\n\n.fa-circle-user {\n  --fa: \"\\f2bd\"; }\n\n.fa-user-circle {\n  --fa: \"\\f2bd\"; }\n\n.fa-user-shield {\n  --fa: \"\\f505\"; }\n\n.fa-wind {\n  --fa: \"\\f72e\"; }\n\n.fa-car-burst {\n  --fa: \"\\f5e1\"; }\n\n.fa-car-crash {\n  --fa: \"\\f5e1\"; }\n\n.fa-y {\n  --fa: \"\\59\"; }\n\n.fa-person-snowboarding {\n  --fa: \"\\f7ce\"; }\n\n.fa-snowboarding {\n  --fa: \"\\f7ce\"; }\n\n.fa-truck-fast {\n  --fa: \"\\f48b\"; }\n\n.fa-shipping-fast {\n  --fa: \"\\f48b\"; }\n\n.fa-fish {\n  --fa: \"\\f578\"; }\n\n.fa-user-graduate {\n  --fa: \"\\f501\"; }\n\n.fa-circle-half-stroke {\n  --fa: \"\\f042\"; }\n\n.fa-adjust {\n  --fa: \"\\f042\"; }\n\n.fa-clapperboard {\n  --fa: \"\\e131\"; }\n\n.fa-circle-radiation {\n  --fa: \"\\f7ba\"; }\n\n.fa-radiation-alt {\n  --fa: \"\\f7ba\"; }\n\n.fa-baseball {\n  --fa: \"\\f433\"; }\n\n.fa-baseball-ball {\n  --fa: \"\\f433\"; }\n\n.fa-jet-fighter-up {\n  --fa: \"\\e518\"; }\n\n.fa-diagram-project {\n  --fa: \"\\f542\"; }\n\n.fa-project-diagram {\n  --fa: \"\\f542\"; }\n\n.fa-copy {\n  --fa: \"\\f0c5\"; }\n\n.fa-volume-xmark {\n  --fa: \"\\f6a9\"; }\n\n.fa-volume-mute {\n  --fa: \"\\f6a9\"; }\n\n.fa-volume-times {\n  --fa: \"\\f6a9\"; }\n\n.fa-hand-sparkles {\n  --fa: \"\\e05d\"; }\n\n.fa-grip {\n  --fa: \"\\f58d\"; }\n\n.fa-grip-horizontal {\n  --fa: \"\\f58d\"; }\n\n.fa-share-from-square {\n  --fa: \"\\f14d\"; }\n\n.fa-share-square {\n  --fa: \"\\f14d\"; }\n\n.fa-child-combatant {\n  --fa: \"\\e4e0\"; }\n\n.fa-child-rifle {\n  --fa: \"\\e4e0\"; }\n\n.fa-gun {\n  --fa: \"\\e19b\"; }\n\n.fa-square-phone {\n  --fa: \"\\f098\"; }\n\n.fa-phone-square {\n  --fa: \"\\f098\"; }\n\n.fa-plus {\n  --fa: \"\\2b\"; }\n\n.fa-add {\n  --fa: \"\\2b\"; }\n\n.fa-expand {\n  --fa: \"\\f065\"; }\n\n.fa-computer {\n  --fa: \"\\e4e5\"; }\n\n.fa-xmark {\n  --fa: \"\\f00d\"; }\n\n.fa-close {\n  --fa: \"\\f00d\"; }\n\n.fa-multiply {\n  --fa: \"\\f00d\"; }\n\n.fa-remove {\n  --fa: \"\\f00d\"; }\n\n.fa-times {\n  --fa: \"\\f00d\"; }\n\n.fa-arrows-up-down-left-right {\n  --fa: \"\\f047\"; }\n\n.fa-arrows {\n  --fa: \"\\f047\"; }\n\n.fa-chalkboard-user {\n  --fa: \"\\f51c\"; }\n\n.fa-chalkboard-teacher {\n  --fa: \"\\f51c\"; }\n\n.fa-peso-sign {\n  --fa: \"\\e222\"; }\n\n.fa-building-shield {\n  --fa: \"\\e4d8\"; }\n\n.fa-baby {\n  --fa: \"\\f77c\"; }\n\n.fa-users-line {\n  --fa: \"\\e592\"; }\n\n.fa-quote-left {\n  --fa: \"\\f10d\"; }\n\n.fa-quote-left-alt {\n  --fa: \"\\f10d\"; }\n\n.fa-tractor {\n  --fa: \"\\f722\"; }\n\n.fa-trash-arrow-up {\n  --fa: \"\\f829\"; }\n\n.fa-trash-restore {\n  --fa: \"\\f829\"; }\n\n.fa-arrow-down-up-lock {\n  --fa: \"\\e4b0\"; }\n\n.fa-lines-leaning {\n  --fa: \"\\e51e\"; }\n\n.fa-ruler-combined {\n  --fa: \"\\f546\"; }\n\n.fa-copyright {\n  --fa: \"\\f1f9\"; }\n\n.fa-equals {\n  --fa: \"\\3d\"; }\n\n.fa-blender {\n  --fa: \"\\f517\"; }\n\n.fa-teeth {\n  --fa: \"\\f62e\"; }\n\n.fa-shekel-sign {\n  --fa: \"\\f20b\"; }\n\n.fa-ils {\n  --fa: \"\\f20b\"; }\n\n.fa-shekel {\n  --fa: \"\\f20b\"; }\n\n.fa-sheqel {\n  --fa: \"\\f20b\"; }\n\n.fa-sheqel-sign {\n  --fa: \"\\f20b\"; }\n\n.fa-map {\n  --fa: \"\\f279\"; }\n\n.fa-rocket {\n  --fa: \"\\f135\"; }\n\n.fa-photo-film {\n  --fa: \"\\f87c\"; }\n\n.fa-photo-video {\n  --fa: \"\\f87c\"; }\n\n.fa-folder-minus {\n  --fa: \"\\f65d\"; }\n\n.fa-hexagon-nodes-bolt {\n  --fa: \"\\e69a\"; }\n\n.fa-store {\n  --fa: \"\\f54e\"; }\n\n.fa-arrow-trend-up {\n  --fa: \"\\e098\"; }\n\n.fa-plug-circle-minus {\n  --fa: \"\\e55e\"; }\n\n.fa-sign-hanging {\n  --fa: \"\\f4d9\"; }\n\n.fa-sign {\n  --fa: \"\\f4d9\"; }\n\n.fa-bezier-curve {\n  --fa: \"\\f55b\"; }\n\n.fa-bell-slash {\n  --fa: \"\\f1f6\"; }\n\n.fa-tablet {\n  --fa: \"\\f3fb\"; }\n\n.fa-tablet-android {\n  --fa: \"\\f3fb\"; }\n\n.fa-school-flag {\n  --fa: \"\\e56e\"; }\n\n.fa-fill {\n  --fa: \"\\f575\"; }\n\n.fa-angle-up {\n  --fa: \"\\f106\"; }\n\n.fa-drumstick-bite {\n  --fa: \"\\f6d7\"; }\n\n.fa-holly-berry {\n  --fa: \"\\f7aa\"; }\n\n.fa-chevron-left {\n  --fa: \"\\f053\"; }\n\n.fa-bacteria {\n  --fa: \"\\e059\"; }\n\n.fa-hand-lizard {\n  --fa: \"\\f258\"; }\n\n.fa-notdef {\n  --fa: \"\\e1fe\"; }\n\n.fa-disease {\n  --fa: \"\\f7fa\"; }\n\n.fa-briefcase-medical {\n  --fa: \"\\f469\"; }\n\n.fa-genderless {\n  --fa: \"\\f22d\"; }\n\n.fa-chevron-right {\n  --fa: \"\\f054\"; }\n\n.fa-retweet {\n  --fa: \"\\f079\"; }\n\n.fa-car-rear {\n  --fa: \"\\f5de\"; }\n\n.fa-car-alt {\n  --fa: \"\\f5de\"; }\n\n.fa-pump-soap {\n  --fa: \"\\e06b\"; }\n\n.fa-video-slash {\n  --fa: \"\\f4e2\"; }\n\n.fa-battery-quarter {\n  --fa: \"\\f243\"; }\n\n.fa-battery-2 {\n  --fa: \"\\f243\"; }\n\n.fa-radio {\n  --fa: \"\\f8d7\"; }\n\n.fa-baby-carriage {\n  --fa: \"\\f77d\"; }\n\n.fa-carriage-baby {\n  --fa: \"\\f77d\"; }\n\n.fa-traffic-light {\n  --fa: \"\\f637\"; }\n\n.fa-thermometer {\n  --fa: \"\\f491\"; }\n\n.fa-vr-cardboard {\n  --fa: \"\\f729\"; }\n\n.fa-hand-middle-finger {\n  --fa: \"\\f806\"; }\n\n.fa-percent {\n  --fa: \"\\25\"; }\n\n.fa-percentage {\n  --fa: \"\\25\"; }\n\n.fa-truck-moving {\n  --fa: \"\\f4df\"; }\n\n.fa-glass-water-droplet {\n  --fa: \"\\e4f5\"; }\n\n.fa-display {\n  --fa: \"\\e163\"; }\n\n.fa-face-smile {\n  --fa: \"\\f118\"; }\n\n.fa-smile {\n  --fa: \"\\f118\"; }\n\n.fa-thumbtack {\n  --fa: \"\\f08d\"; }\n\n.fa-thumb-tack {\n  --fa: \"\\f08d\"; }\n\n.fa-trophy {\n  --fa: \"\\f091\"; }\n\n.fa-person-praying {\n  --fa: \"\\f683\"; }\n\n.fa-pray {\n  --fa: \"\\f683\"; }\n\n.fa-hammer {\n  --fa: \"\\f6e3\"; }\n\n.fa-hand-peace {\n  --fa: \"\\f25b\"; }\n\n.fa-rotate {\n  --fa: \"\\f2f1\"; }\n\n.fa-sync-alt {\n  --fa: \"\\f2f1\"; }\n\n.fa-spinner {\n  --fa: \"\\f110\"; }\n\n.fa-robot {\n  --fa: \"\\f544\"; }\n\n.fa-peace {\n  --fa: \"\\f67c\"; }\n\n.fa-gears {\n  --fa: \"\\f085\"; }\n\n.fa-cogs {\n  --fa: \"\\f085\"; }\n\n.fa-warehouse {\n  --fa: \"\\f494\"; }\n\n.fa-arrow-up-right-dots {\n  --fa: \"\\e4b7\"; }\n\n.fa-splotch {\n  --fa: \"\\f5bc\"; }\n\n.fa-face-grin-hearts {\n  --fa: \"\\f584\"; }\n\n.fa-grin-hearts {\n  --fa: \"\\f584\"; }\n\n.fa-dice-four {\n  --fa: \"\\f524\"; }\n\n.fa-sim-card {\n  --fa: \"\\f7c4\"; }\n\n.fa-transgender {\n  --fa: \"\\f225\"; }\n\n.fa-transgender-alt {\n  --fa: \"\\f225\"; }\n\n.fa-mercury {\n  --fa: \"\\f223\"; }\n\n.fa-arrow-turn-down {\n  --fa: \"\\f149\"; }\n\n.fa-level-down {\n  --fa: \"\\f149\"; }\n\n.fa-person-falling-burst {\n  --fa: \"\\e547\"; }\n\n.fa-award {\n  --fa: \"\\f559\"; }\n\n.fa-ticket-simple {\n  --fa: \"\\f3ff\"; }\n\n.fa-ticket-alt {\n  --fa: \"\\f3ff\"; }\n\n.fa-building {\n  --fa: \"\\f1ad\"; }\n\n.fa-angles-left {\n  --fa: \"\\f100\"; }\n\n.fa-angle-double-left {\n  --fa: \"\\f100\"; }\n\n.fa-qrcode {\n  --fa: \"\\f029\"; }\n\n.fa-clock-rotate-left {\n  --fa: \"\\f1da\"; }\n\n.fa-history {\n  --fa: \"\\f1da\"; }\n\n.fa-face-grin-beam-sweat {\n  --fa: \"\\f583\"; }\n\n.fa-grin-beam-sweat {\n  --fa: \"\\f583\"; }\n\n.fa-file-export {\n  --fa: \"\\f56e\"; }\n\n.fa-arrow-right-from-file {\n  --fa: \"\\f56e\"; }\n\n.fa-shield {\n  --fa: \"\\f132\"; }\n\n.fa-shield-blank {\n  --fa: \"\\f132\"; }\n\n.fa-arrow-up-short-wide {\n  --fa: \"\\f885\"; }\n\n.fa-sort-amount-up-alt {\n  --fa: \"\\f885\"; }\n\n.fa-comment-nodes {\n  --fa: \"\\e696\"; }\n\n.fa-house-medical {\n  --fa: \"\\e3b2\"; }\n\n.fa-golf-ball-tee {\n  --fa: \"\\f450\"; }\n\n.fa-golf-ball {\n  --fa: \"\\f450\"; }\n\n.fa-circle-chevron-left {\n  --fa: \"\\f137\"; }\n\n.fa-chevron-circle-left {\n  --fa: \"\\f137\"; }\n\n.fa-house-chimney-window {\n  --fa: \"\\e00d\"; }\n\n.fa-pen-nib {\n  --fa: \"\\f5ad\"; }\n\n.fa-tent-arrow-turn-left {\n  --fa: \"\\e580\"; }\n\n.fa-tents {\n  --fa: \"\\e582\"; }\n\n.fa-wand-magic {\n  --fa: \"\\f0d0\"; }\n\n.fa-magic {\n  --fa: \"\\f0d0\"; }\n\n.fa-dog {\n  --fa: \"\\f6d3\"; }\n\n.fa-carrot {\n  --fa: \"\\f787\"; }\n\n.fa-moon {\n  --fa: \"\\f186\"; }\n\n.fa-wine-glass-empty {\n  --fa: \"\\f5ce\"; }\n\n.fa-wine-glass-alt {\n  --fa: \"\\f5ce\"; }\n\n.fa-cheese {\n  --fa: \"\\f7ef\"; }\n\n.fa-yin-yang {\n  --fa: \"\\f6ad\"; }\n\n.fa-music {\n  --fa: \"\\f001\"; }\n\n.fa-code-commit {\n  --fa: \"\\f386\"; }\n\n.fa-temperature-low {\n  --fa: \"\\f76b\"; }\n\n.fa-person-biking {\n  --fa: \"\\f84a\"; }\n\n.fa-biking {\n  --fa: \"\\f84a\"; }\n\n.fa-broom {\n  --fa: \"\\f51a\"; }\n\n.fa-shield-heart {\n  --fa: \"\\e574\"; }\n\n.fa-gopuram {\n  --fa: \"\\f664\"; }\n\n.fa-earth-oceania {\n  --fa: \"\\e47b\"; }\n\n.fa-globe-oceania {\n  --fa: \"\\e47b\"; }\n\n.fa-square-xmark {\n  --fa: \"\\f2d3\"; }\n\n.fa-times-square {\n  --fa: \"\\f2d3\"; }\n\n.fa-xmark-square {\n  --fa: \"\\f2d3\"; }\n\n.fa-hashtag {\n  --fa: \"\\23\"; }\n\n.fa-up-right-and-down-left-from-center {\n  --fa: \"\\f424\"; }\n\n.fa-expand-alt {\n  --fa: \"\\f424\"; }\n\n.fa-oil-can {\n  --fa: \"\\f613\"; }\n\n.fa-t {\n  --fa: \"\\54\"; }\n\n.fa-hippo {\n  --fa: \"\\f6ed\"; }\n\n.fa-chart-column {\n  --fa: \"\\e0e3\"; }\n\n.fa-infinity {\n  --fa: \"\\f534\"; }\n\n.fa-vial-circle-check {\n  --fa: \"\\e596\"; }\n\n.fa-person-arrow-down-to-line {\n  --fa: \"\\e538\"; }\n\n.fa-voicemail {\n  --fa: \"\\f897\"; }\n\n.fa-fan {\n  --fa: \"\\f863\"; }\n\n.fa-person-walking-luggage {\n  --fa: \"\\e554\"; }\n\n.fa-up-down {\n  --fa: \"\\f338\"; }\n\n.fa-arrows-alt-v {\n  --fa: \"\\f338\"; }\n\n.fa-cloud-moon-rain {\n  --fa: \"\\f73c\"; }\n\n.fa-calendar {\n  --fa: \"\\f133\"; }\n\n.fa-trailer {\n  --fa: \"\\e041\"; }\n\n.fa-bahai {\n  --fa: \"\\f666\"; }\n\n.fa-haykal {\n  --fa: \"\\f666\"; }\n\n.fa-sd-card {\n  --fa: \"\\f7c2\"; }\n\n.fa-dragon {\n  --fa: \"\\f6d5\"; }\n\n.fa-shoe-prints {\n  --fa: \"\\f54b\"; }\n\n.fa-circle-plus {\n  --fa: \"\\f055\"; }\n\n.fa-plus-circle {\n  --fa: \"\\f055\"; }\n\n.fa-face-grin-tongue-wink {\n  --fa: \"\\f58b\"; }\n\n.fa-grin-tongue-wink {\n  --fa: \"\\f58b\"; }\n\n.fa-hand-holding {\n  --fa: \"\\f4bd\"; }\n\n.fa-plug-circle-exclamation {\n  --fa: \"\\e55d\"; }\n\n.fa-link-slash {\n  --fa: \"\\f127\"; }\n\n.fa-chain-broken {\n  --fa: \"\\f127\"; }\n\n.fa-chain-slash {\n  --fa: \"\\f127\"; }\n\n.fa-unlink {\n  --fa: \"\\f127\"; }\n\n.fa-clone {\n  --fa: \"\\f24d\"; }\n\n.fa-person-walking-arrow-loop-left {\n  --fa: \"\\e551\"; }\n\n.fa-arrow-up-z-a {\n  --fa: \"\\f882\"; }\n\n.fa-sort-alpha-up-alt {\n  --fa: \"\\f882\"; }\n\n.fa-fire-flame-curved {\n  --fa: \"\\f7e4\"; }\n\n.fa-fire-alt {\n  --fa: \"\\f7e4\"; }\n\n.fa-tornado {\n  --fa: \"\\f76f\"; }\n\n.fa-file-circle-plus {\n  --fa: \"\\e494\"; }\n\n.fa-book-quran {\n  --fa: \"\\f687\"; }\n\n.fa-quran {\n  --fa: \"\\f687\"; }\n\n.fa-anchor {\n  --fa: \"\\f13d\"; }\n\n.fa-border-all {\n  --fa: \"\\f84c\"; }\n\n.fa-face-angry {\n  --fa: \"\\f556\"; }\n\n.fa-angry {\n  --fa: \"\\f556\"; }\n\n.fa-cookie-bite {\n  --fa: \"\\f564\"; }\n\n.fa-arrow-trend-down {\n  --fa: \"\\e097\"; }\n\n.fa-rss {\n  --fa: \"\\f09e\"; }\n\n.fa-feed {\n  --fa: \"\\f09e\"; }\n\n.fa-draw-polygon {\n  --fa: \"\\f5ee\"; }\n\n.fa-scale-balanced {\n  --fa: \"\\f24e\"; }\n\n.fa-balance-scale {\n  --fa: \"\\f24e\"; }\n\n.fa-gauge-simple-high {\n  --fa: \"\\f62a\"; }\n\n.fa-tachometer {\n  --fa: \"\\f62a\"; }\n\n.fa-tachometer-fast {\n  --fa: \"\\f62a\"; }\n\n.fa-shower {\n  --fa: \"\\f2cc\"; }\n\n.fa-desktop {\n  --fa: \"\\f390\"; }\n\n.fa-desktop-alt {\n  --fa: \"\\f390\"; }\n\n.fa-m {\n  --fa: \"\\4d\"; }\n\n.fa-table-list {\n  --fa: \"\\f00b\"; }\n\n.fa-th-list {\n  --fa: \"\\f00b\"; }\n\n.fa-comment-sms {\n  --fa: \"\\f7cd\"; }\n\n.fa-sms {\n  --fa: \"\\f7cd\"; }\n\n.fa-book {\n  --fa: \"\\f02d\"; }\n\n.fa-user-plus {\n  --fa: \"\\f234\"; }\n\n.fa-check {\n  --fa: \"\\f00c\"; }\n\n.fa-battery-three-quarters {\n  --fa: \"\\f241\"; }\n\n.fa-battery-4 {\n  --fa: \"\\f241\"; }\n\n.fa-house-circle-check {\n  --fa: \"\\e509\"; }\n\n.fa-angle-left {\n  --fa: \"\\f104\"; }\n\n.fa-diagram-successor {\n  --fa: \"\\e47a\"; }\n\n.fa-truck-arrow-right {\n  --fa: \"\\e58b\"; }\n\n.fa-arrows-split-up-and-left {\n  --fa: \"\\e4bc\"; }\n\n.fa-hand-fist {\n  --fa: \"\\f6de\"; }\n\n.fa-fist-raised {\n  --fa: \"\\f6de\"; }\n\n.fa-cloud-moon {\n  --fa: \"\\f6c3\"; }\n\n.fa-briefcase {\n  --fa: \"\\f0b1\"; }\n\n.fa-person-falling {\n  --fa: \"\\e546\"; }\n\n.fa-image-portrait {\n  --fa: \"\\f3e0\"; }\n\n.fa-portrait {\n  --fa: \"\\f3e0\"; }\n\n.fa-user-tag {\n  --fa: \"\\f507\"; }\n\n.fa-rug {\n  --fa: \"\\e569\"; }\n\n.fa-earth-europe {\n  --fa: \"\\f7a2\"; }\n\n.fa-globe-europe {\n  --fa: \"\\f7a2\"; }\n\n.fa-cart-flatbed-suitcase {\n  --fa: \"\\f59d\"; }\n\n.fa-luggage-cart {\n  --fa: \"\\f59d\"; }\n\n.fa-rectangle-xmark {\n  --fa: \"\\f410\"; }\n\n.fa-rectangle-times {\n  --fa: \"\\f410\"; }\n\n.fa-times-rectangle {\n  --fa: \"\\f410\"; }\n\n.fa-window-close {\n  --fa: \"\\f410\"; }\n\n.fa-baht-sign {\n  --fa: \"\\e0ac\"; }\n\n.fa-book-open {\n  --fa: \"\\f518\"; }\n\n.fa-book-journal-whills {\n  --fa: \"\\f66a\"; }\n\n.fa-journal-whills {\n  --fa: \"\\f66a\"; }\n\n.fa-handcuffs {\n  --fa: \"\\e4f8\"; }\n\n.fa-triangle-exclamation {\n  --fa: \"\\f071\"; }\n\n.fa-exclamation-triangle {\n  --fa: \"\\f071\"; }\n\n.fa-warning {\n  --fa: \"\\f071\"; }\n\n.fa-database {\n  --fa: \"\\f1c0\"; }\n\n.fa-share {\n  --fa: \"\\f064\"; }\n\n.fa-mail-forward {\n  --fa: \"\\f064\"; }\n\n.fa-bottle-droplet {\n  --fa: \"\\e4c4\"; }\n\n.fa-mask-face {\n  --fa: \"\\e1d7\"; }\n\n.fa-hill-rockslide {\n  --fa: \"\\e508\"; }\n\n.fa-right-left {\n  --fa: \"\\f362\"; }\n\n.fa-exchange-alt {\n  --fa: \"\\f362\"; }\n\n.fa-paper-plane {\n  --fa: \"\\f1d8\"; }\n\n.fa-road-circle-exclamation {\n  --fa: \"\\e565\"; }\n\n.fa-dungeon {\n  --fa: \"\\f6d9\"; }\n\n.fa-align-right {\n  --fa: \"\\f038\"; }\n\n.fa-money-bill-1-wave {\n  --fa: \"\\f53b\"; }\n\n.fa-money-bill-wave-alt {\n  --fa: \"\\f53b\"; }\n\n.fa-life-ring {\n  --fa: \"\\f1cd\"; }\n\n.fa-hands {\n  --fa: \"\\f2a7\"; }\n\n.fa-sign-language {\n  --fa: \"\\f2a7\"; }\n\n.fa-signing {\n  --fa: \"\\f2a7\"; }\n\n.fa-calendar-day {\n  --fa: \"\\f783\"; }\n\n.fa-water-ladder {\n  --fa: \"\\f5c5\"; }\n\n.fa-ladder-water {\n  --fa: \"\\f5c5\"; }\n\n.fa-swimming-pool {\n  --fa: \"\\f5c5\"; }\n\n.fa-arrows-up-down {\n  --fa: \"\\f07d\"; }\n\n.fa-arrows-v {\n  --fa: \"\\f07d\"; }\n\n.fa-face-grimace {\n  --fa: \"\\f57f\"; }\n\n.fa-grimace {\n  --fa: \"\\f57f\"; }\n\n.fa-wheelchair-move {\n  --fa: \"\\e2ce\"; }\n\n.fa-wheelchair-alt {\n  --fa: \"\\e2ce\"; }\n\n.fa-turn-down {\n  --fa: \"\\f3be\"; }\n\n.fa-level-down-alt {\n  --fa: \"\\f3be\"; }\n\n.fa-person-walking-arrow-right {\n  --fa: \"\\e552\"; }\n\n.fa-square-envelope {\n  --fa: \"\\f199\"; }\n\n.fa-envelope-square {\n  --fa: \"\\f199\"; }\n\n.fa-dice {\n  --fa: \"\\f522\"; }\n\n.fa-bowling-ball {\n  --fa: \"\\f436\"; }\n\n.fa-brain {\n  --fa: \"\\f5dc\"; }\n\n.fa-bandage {\n  --fa: \"\\f462\"; }\n\n.fa-band-aid {\n  --fa: \"\\f462\"; }\n\n.fa-calendar-minus {\n  --fa: \"\\f272\"; }\n\n.fa-circle-xmark {\n  --fa: \"\\f057\"; }\n\n.fa-times-circle {\n  --fa: \"\\f057\"; }\n\n.fa-xmark-circle {\n  --fa: \"\\f057\"; }\n\n.fa-gifts {\n  --fa: \"\\f79c\"; }\n\n.fa-hotel {\n  --fa: \"\\f594\"; }\n\n.fa-earth-asia {\n  --fa: \"\\f57e\"; }\n\n.fa-globe-asia {\n  --fa: \"\\f57e\"; }\n\n.fa-id-card-clip {\n  --fa: \"\\f47f\"; }\n\n.fa-id-card-alt {\n  --fa: \"\\f47f\"; }\n\n.fa-magnifying-glass-plus {\n  --fa: \"\\f00e\"; }\n\n.fa-search-plus {\n  --fa: \"\\f00e\"; }\n\n.fa-thumbs-up {\n  --fa: \"\\f164\"; }\n\n.fa-user-clock {\n  --fa: \"\\f4fd\"; }\n\n.fa-hand-dots {\n  --fa: \"\\f461\"; }\n\n.fa-allergies {\n  --fa: \"\\f461\"; }\n\n.fa-file-invoice {\n  --fa: \"\\f570\"; }\n\n.fa-window-minimize {\n  --fa: \"\\f2d1\"; }\n\n.fa-mug-saucer {\n  --fa: \"\\f0f4\"; }\n\n.fa-coffee {\n  --fa: \"\\f0f4\"; }\n\n.fa-brush {\n  --fa: \"\\f55d\"; }\n\n.fa-file-half-dashed {\n  --fa: \"\\e698\"; }\n\n.fa-mask {\n  --fa: \"\\f6fa\"; }\n\n.fa-magnifying-glass-minus {\n  --fa: \"\\f010\"; }\n\n.fa-search-minus {\n  --fa: \"\\f010\"; }\n\n.fa-ruler-vertical {\n  --fa: \"\\f548\"; }\n\n.fa-user-large {\n  --fa: \"\\f406\"; }\n\n.fa-user-alt {\n  --fa: \"\\f406\"; }\n\n.fa-train-tram {\n  --fa: \"\\e5b4\"; }\n\n.fa-user-nurse {\n  --fa: \"\\f82f\"; }\n\n.fa-syringe {\n  --fa: \"\\f48e\"; }\n\n.fa-cloud-sun {\n  --fa: \"\\f6c4\"; }\n\n.fa-stopwatch-20 {\n  --fa: \"\\e06f\"; }\n\n.fa-square-full {\n  --fa: \"\\f45c\"; }\n\n.fa-magnet {\n  --fa: \"\\f076\"; }\n\n.fa-jar {\n  --fa: \"\\e516\"; }\n\n.fa-note-sticky {\n  --fa: \"\\f249\"; }\n\n.fa-sticky-note {\n  --fa: \"\\f249\"; }\n\n.fa-bug-slash {\n  --fa: \"\\e490\"; }\n\n.fa-arrow-up-from-water-pump {\n  --fa: \"\\e4b6\"; }\n\n.fa-bone {\n  --fa: \"\\f5d7\"; }\n\n.fa-table-cells-row-unlock {\n  --fa: \"\\e691\"; }\n\n.fa-user-injured {\n  --fa: \"\\f728\"; }\n\n.fa-face-sad-tear {\n  --fa: \"\\f5b4\"; }\n\n.fa-sad-tear {\n  --fa: \"\\f5b4\"; }\n\n.fa-plane {\n  --fa: \"\\f072\"; }\n\n.fa-tent-arrows-down {\n  --fa: \"\\e581\"; }\n\n.fa-exclamation {\n  --fa: \"\\21\"; }\n\n.fa-arrows-spin {\n  --fa: \"\\e4bb\"; }\n\n.fa-print {\n  --fa: \"\\f02f\"; }\n\n.fa-turkish-lira-sign {\n  --fa: \"\\e2bb\"; }\n\n.fa-try {\n  --fa: \"\\e2bb\"; }\n\n.fa-turkish-lira {\n  --fa: \"\\e2bb\"; }\n\n.fa-dollar-sign {\n  --fa: \"\\24\"; }\n\n.fa-dollar {\n  --fa: \"\\24\"; }\n\n.fa-usd {\n  --fa: \"\\24\"; }\n\n.fa-x {\n  --fa: \"\\58\"; }\n\n.fa-magnifying-glass-dollar {\n  --fa: \"\\f688\"; }\n\n.fa-search-dollar {\n  --fa: \"\\f688\"; }\n\n.fa-users-gear {\n  --fa: \"\\f509\"; }\n\n.fa-users-cog {\n  --fa: \"\\f509\"; }\n\n.fa-person-military-pointing {\n  --fa: \"\\e54a\"; }\n\n.fa-building-columns {\n  --fa: \"\\f19c\"; }\n\n.fa-bank {\n  --fa: \"\\f19c\"; }\n\n.fa-institution {\n  --fa: \"\\f19c\"; }\n\n.fa-museum {\n  --fa: \"\\f19c\"; }\n\n.fa-university {\n  --fa: \"\\f19c\"; }\n\n.fa-umbrella {\n  --fa: \"\\f0e9\"; }\n\n.fa-trowel {\n  --fa: \"\\e589\"; }\n\n.fa-d {\n  --fa: \"\\44\"; }\n\n.fa-stapler {\n  --fa: \"\\e5af\"; }\n\n.fa-masks-theater {\n  --fa: \"\\f630\"; }\n\n.fa-theater-masks {\n  --fa: \"\\f630\"; }\n\n.fa-kip-sign {\n  --fa: \"\\e1c4\"; }\n\n.fa-hand-point-left {\n  --fa: \"\\f0a5\"; }\n\n.fa-handshake-simple {\n  --fa: \"\\f4c6\"; }\n\n.fa-handshake-alt {\n  --fa: \"\\f4c6\"; }\n\n.fa-jet-fighter {\n  --fa: \"\\f0fb\"; }\n\n.fa-fighter-jet {\n  --fa: \"\\f0fb\"; }\n\n.fa-square-share-nodes {\n  --fa: \"\\f1e1\"; }\n\n.fa-share-alt-square {\n  --fa: \"\\f1e1\"; }\n\n.fa-barcode {\n  --fa: \"\\f02a\"; }\n\n.fa-plus-minus {\n  --fa: \"\\e43c\"; }\n\n.fa-video {\n  --fa: \"\\f03d\"; }\n\n.fa-video-camera {\n  --fa: \"\\f03d\"; }\n\n.fa-graduation-cap {\n  --fa: \"\\f19d\"; }\n\n.fa-mortar-board {\n  --fa: \"\\f19d\"; }\n\n.fa-hand-holding-medical {\n  --fa: \"\\e05c\"; }\n\n.fa-person-circle-check {\n  --fa: \"\\e53e\"; }\n\n.fa-turn-up {\n  --fa: \"\\f3bf\"; }\n\n.fa-level-up-alt {\n  --fa: \"\\f3bf\"; }\n\n.sr-only,\n.fa-sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0; }\n\n.sr-only-focusable:not(:focus),\n.fa-sr-only-focusable:not(:focus) {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0; }\n:root, :host {\n  --fa-style-family-brands: 'Font Awesome 6 Brands';\n  --fa-font-brands: normal 400 1em/1 'Font Awesome 6 Brands'; }\n\n@font-face {\n  font-family: 'Font Awesome 6 Brands';\n  font-style: normal;\n  font-weight: 400;\n  font-display: block;\n  src: url(\"../webfonts/fa-brands-400.woff2\") format(\"woff2\"), url(\"../webfonts/fa-brands-400.ttf\") format(\"truetype\"); }\n\n.fab,\n.fa-brands {\n  font-weight: 400; }\n\n.fa-monero {\n  --fa: \"\\f3d0\"; }\n\n.fa-hooli {\n  --fa: \"\\f427\"; }\n\n.fa-yelp {\n  --fa: \"\\f1e9\"; }\n\n.fa-cc-visa {\n  --fa: \"\\f1f0\"; }\n\n.fa-lastfm {\n  --fa: \"\\f202\"; }\n\n.fa-shopware {\n  --fa: \"\\f5b5\"; }\n\n.fa-creative-commons-nc {\n  --fa: \"\\f4e8\"; }\n\n.fa-aws {\n  --fa: \"\\f375\"; }\n\n.fa-redhat {\n  --fa: \"\\f7bc\"; }\n\n.fa-yoast {\n  --fa: \"\\f2b1\"; }\n\n.fa-cloudflare {\n  --fa: \"\\e07d\"; }\n\n.fa-ups {\n  --fa: \"\\f7e0\"; }\n\n.fa-pixiv {\n  --fa: \"\\e640\"; }\n\n.fa-wpexplorer {\n  --fa: \"\\f2de\"; }\n\n.fa-dyalog {\n  --fa: \"\\f399\"; }\n\n.fa-bity {\n  --fa: \"\\f37a\"; }\n\n.fa-stackpath {\n  --fa: \"\\f842\"; }\n\n.fa-buysellads {\n  --fa: \"\\f20d\"; }\n\n.fa-first-order {\n  --fa: \"\\f2b0\"; }\n\n.fa-modx {\n  --fa: \"\\f285\"; }\n\n.fa-guilded {\n  --fa: \"\\e07e\"; }\n\n.fa-vnv {\n  --fa: \"\\f40b\"; }\n\n.fa-square-js {\n  --fa: \"\\f3b9\"; }\n\n.fa-js-square {\n  --fa: \"\\f3b9\"; }\n\n.fa-microsoft {\n  --fa: \"\\f3ca\"; }\n\n.fa-qq {\n  --fa: \"\\f1d6\"; }\n\n.fa-orcid {\n  --fa: \"\\f8d2\"; }\n\n.fa-java {\n  --fa: \"\\f4e4\"; }\n\n.fa-invision {\n  --fa: \"\\f7b0\"; }\n\n.fa-creative-commons-pd-alt {\n  --fa: \"\\f4ed\"; }\n\n.fa-centercode {\n  --fa: \"\\f380\"; }\n\n.fa-glide-g {\n  --fa: \"\\f2a6\"; }\n\n.fa-drupal {\n  --fa: \"\\f1a9\"; }\n\n.fa-jxl {\n  --fa: \"\\e67b\"; }\n\n.fa-dart-lang {\n  --fa: \"\\e693\"; }\n\n.fa-hire-a-helper {\n  --fa: \"\\f3b0\"; }\n\n.fa-creative-commons-by {\n  --fa: \"\\f4e7\"; }\n\n.fa-unity {\n  --fa: \"\\e049\"; }\n\n.fa-whmcs {\n  --fa: \"\\f40d\"; }\n\n.fa-rocketchat {\n  --fa: \"\\f3e8\"; }\n\n.fa-vk {\n  --fa: \"\\f189\"; }\n\n.fa-untappd {\n  --fa: \"\\f405\"; }\n\n.fa-mailchimp {\n  --fa: \"\\f59e\"; }\n\n.fa-css3-alt {\n  --fa: \"\\f38b\"; }\n\n.fa-square-reddit {\n  --fa: \"\\f1a2\"; }\n\n.fa-reddit-square {\n  --fa: \"\\f1a2\"; }\n\n.fa-vimeo-v {\n  --fa: \"\\f27d\"; }\n\n.fa-contao {\n  --fa: \"\\f26d\"; }\n\n.fa-square-font-awesome {\n  --fa: \"\\e5ad\"; }\n\n.fa-deskpro {\n  --fa: \"\\f38f\"; }\n\n.fa-brave {\n  --fa: \"\\e63c\"; }\n\n.fa-sistrix {\n  --fa: \"\\f3ee\"; }\n\n.fa-square-instagram {\n  --fa: \"\\e055\"; }\n\n.fa-instagram-square {\n  --fa: \"\\e055\"; }\n\n.fa-battle-net {\n  --fa: \"\\f835\"; }\n\n.fa-the-red-yeti {\n  --fa: \"\\f69d\"; }\n\n.fa-square-hacker-news {\n  --fa: \"\\f3af\"; }\n\n.fa-hacker-news-square {\n  --fa: \"\\f3af\"; }\n\n.fa-edge {\n  --fa: \"\\f282\"; }\n\n.fa-threads {\n  --fa: \"\\e618\"; }\n\n.fa-napster {\n  --fa: \"\\f3d2\"; }\n\n.fa-square-snapchat {\n  --fa: \"\\f2ad\"; }\n\n.fa-snapchat-square {\n  --fa: \"\\f2ad\"; }\n\n.fa-google-plus-g {\n  --fa: \"\\f0d5\"; }\n\n.fa-artstation {\n  --fa: \"\\f77a\"; }\n\n.fa-markdown {\n  --fa: \"\\f60f\"; }\n\n.fa-sourcetree {\n  --fa: \"\\f7d3\"; }\n\n.fa-google-plus {\n  --fa: \"\\f2b3\"; }\n\n.fa-diaspora {\n  --fa: \"\\f791\"; }\n\n.fa-foursquare {\n  --fa: \"\\f180\"; }\n\n.fa-stack-overflow {\n  --fa: \"\\f16c\"; }\n\n.fa-github-alt {\n  --fa: \"\\f113\"; }\n\n.fa-phoenix-squadron {\n  --fa: \"\\f511\"; }\n\n.fa-pagelines {\n  --fa: \"\\f18c\"; }\n\n.fa-algolia {\n  --fa: \"\\f36c\"; }\n\n.fa-red-river {\n  --fa: \"\\f3e3\"; }\n\n.fa-creative-commons-sa {\n  --fa: \"\\f4ef\"; }\n\n.fa-safari {\n  --fa: \"\\f267\"; }\n\n.fa-google {\n  --fa: \"\\f1a0\"; }\n\n.fa-square-font-awesome-stroke {\n  --fa: \"\\f35c\"; }\n\n.fa-font-awesome-alt {\n  --fa: \"\\f35c\"; }\n\n.fa-atlassian {\n  --fa: \"\\f77b\"; }\n\n.fa-linkedin-in {\n  --fa: \"\\f0e1\"; }\n\n.fa-digital-ocean {\n  --fa: \"\\f391\"; }\n\n.fa-nimblr {\n  --fa: \"\\f5a8\"; }\n\n.fa-chromecast {\n  --fa: \"\\f838\"; }\n\n.fa-evernote {\n  --fa: \"\\f839\"; }\n\n.fa-hacker-news {\n  --fa: \"\\f1d4\"; }\n\n.fa-creative-commons-sampling {\n  --fa: \"\\f4f0\"; }\n\n.fa-adversal {\n  --fa: \"\\f36a\"; }\n\n.fa-creative-commons {\n  --fa: \"\\f25e\"; }\n\n.fa-watchman-monitoring {\n  --fa: \"\\e087\"; }\n\n.fa-fonticons {\n  --fa: \"\\f280\"; }\n\n.fa-weixin {\n  --fa: \"\\f1d7\"; }\n\n.fa-shirtsinbulk {\n  --fa: \"\\f214\"; }\n\n.fa-codepen {\n  --fa: \"\\f1cb\"; }\n\n.fa-git-alt {\n  --fa: \"\\f841\"; }\n\n.fa-lyft {\n  --fa: \"\\f3c3\"; }\n\n.fa-rev {\n  --fa: \"\\f5b2\"; }\n\n.fa-windows {\n  --fa: \"\\f17a\"; }\n\n.fa-wizards-of-the-coast {\n  --fa: \"\\f730\"; }\n\n.fa-square-viadeo {\n  --fa: \"\\f2aa\"; }\n\n.fa-viadeo-square {\n  --fa: \"\\f2aa\"; }\n\n.fa-meetup {\n  --fa: \"\\f2e0\"; }\n\n.fa-centos {\n  --fa: \"\\f789\"; }\n\n.fa-adn {\n  --fa: \"\\f170\"; }\n\n.fa-cloudsmith {\n  --fa: \"\\f384\"; }\n\n.fa-opensuse {\n  --fa: \"\\e62b\"; }\n\n.fa-pied-piper-alt {\n  --fa: \"\\f1a8\"; }\n\n.fa-square-dribbble {\n  --fa: \"\\f397\"; }\n\n.fa-dribbble-square {\n  --fa: \"\\f397\"; }\n\n.fa-codiepie {\n  --fa: \"\\f284\"; }\n\n.fa-node {\n  --fa: \"\\f419\"; }\n\n.fa-mix {\n  --fa: \"\\f3cb\"; }\n\n.fa-steam {\n  --fa: \"\\f1b6\"; }\n\n.fa-cc-apple-pay {\n  --fa: \"\\f416\"; }\n\n.fa-scribd {\n  --fa: \"\\f28a\"; }\n\n.fa-debian {\n  --fa: \"\\e60b\"; }\n\n.fa-openid {\n  --fa: \"\\f19b\"; }\n\n.fa-instalod {\n  --fa: \"\\e081\"; }\n\n.fa-files-pinwheel {\n  --fa: \"\\e69f\"; }\n\n.fa-expeditedssl {\n  --fa: \"\\f23e\"; }\n\n.fa-sellcast {\n  --fa: \"\\f2da\"; }\n\n.fa-square-twitter {\n  --fa: \"\\f081\"; }\n\n.fa-twitter-square {\n  --fa: \"\\f081\"; }\n\n.fa-r-project {\n  --fa: \"\\f4f7\"; }\n\n.fa-delicious {\n  --fa: \"\\f1a5\"; }\n\n.fa-freebsd {\n  --fa: \"\\f3a4\"; }\n\n.fa-vuejs {\n  --fa: \"\\f41f\"; }\n\n.fa-accusoft {\n  --fa: \"\\f369\"; }\n\n.fa-ioxhost {\n  --fa: \"\\f208\"; }\n\n.fa-fonticons-fi {\n  --fa: \"\\f3a2\"; }\n\n.fa-app-store {\n  --fa: \"\\f36f\"; }\n\n.fa-cc-mastercard {\n  --fa: \"\\f1f1\"; }\n\n.fa-itunes-note {\n  --fa: \"\\f3b5\"; }\n\n.fa-golang {\n  --fa: \"\\e40f\"; }\n\n.fa-kickstarter {\n  --fa: \"\\f3bb\"; }\n\n.fa-square-kickstarter {\n  --fa: \"\\f3bb\"; }\n\n.fa-grav {\n  --fa: \"\\f2d6\"; }\n\n.fa-weibo {\n  --fa: \"\\f18a\"; }\n\n.fa-uncharted {\n  --fa: \"\\e084\"; }\n\n.fa-firstdraft {\n  --fa: \"\\f3a1\"; }\n\n.fa-square-youtube {\n  --fa: \"\\f431\"; }\n\n.fa-youtube-square {\n  --fa: \"\\f431\"; }\n\n.fa-wikipedia-w {\n  --fa: \"\\f266\"; }\n\n.fa-wpressr {\n  --fa: \"\\f3e4\"; }\n\n.fa-rendact {\n  --fa: \"\\f3e4\"; }\n\n.fa-angellist {\n  --fa: \"\\f209\"; }\n\n.fa-galactic-republic {\n  --fa: \"\\f50c\"; }\n\n.fa-nfc-directional {\n  --fa: \"\\e530\"; }\n\n.fa-skype {\n  --fa: \"\\f17e\"; }\n\n.fa-joget {\n  --fa: \"\\f3b7\"; }\n\n.fa-fedora {\n  --fa: \"\\f798\"; }\n\n.fa-stripe-s {\n  --fa: \"\\f42a\"; }\n\n.fa-meta {\n  --fa: \"\\e49b\"; }\n\n.fa-laravel {\n  --fa: \"\\f3bd\"; }\n\n.fa-hotjar {\n  --fa: \"\\f3b1\"; }\n\n.fa-bluetooth-b {\n  --fa: \"\\f294\"; }\n\n.fa-square-letterboxd {\n  --fa: \"\\e62e\"; }\n\n.fa-sticker-mule {\n  --fa: \"\\f3f7\"; }\n\n.fa-creative-commons-zero {\n  --fa: \"\\f4f3\"; }\n\n.fa-hips {\n  --fa: \"\\f452\"; }\n\n.fa-css {\n  --fa: \"\\e6a2\"; }\n\n.fa-behance {\n  --fa: \"\\f1b4\"; }\n\n.fa-reddit {\n  --fa: \"\\f1a1\"; }\n\n.fa-discord {\n  --fa: \"\\f392\"; }\n\n.fa-chrome {\n  --fa: \"\\f268\"; }\n\n.fa-app-store-ios {\n  --fa: \"\\f370\"; }\n\n.fa-cc-discover {\n  --fa: \"\\f1f2\"; }\n\n.fa-wpbeginner {\n  --fa: \"\\f297\"; }\n\n.fa-confluence {\n  --fa: \"\\f78d\"; }\n\n.fa-shoelace {\n  --fa: \"\\e60c\"; }\n\n.fa-mdb {\n  --fa: \"\\f8ca\"; }\n\n.fa-dochub {\n  --fa: \"\\f394\"; }\n\n.fa-accessible-icon {\n  --fa: \"\\f368\"; }\n\n.fa-ebay {\n  --fa: \"\\f4f4\"; }\n\n.fa-amazon {\n  --fa: \"\\f270\"; }\n\n.fa-unsplash {\n  --fa: \"\\e07c\"; }\n\n.fa-yarn {\n  --fa: \"\\f7e3\"; }\n\n.fa-square-steam {\n  --fa: \"\\f1b7\"; }\n\n.fa-steam-square {\n  --fa: \"\\f1b7\"; }\n\n.fa-500px {\n  --fa: \"\\f26e\"; }\n\n.fa-square-vimeo {\n  --fa: \"\\f194\"; }\n\n.fa-vimeo-square {\n  --fa: \"\\f194\"; }\n\n.fa-asymmetrik {\n  --fa: \"\\f372\"; }\n\n.fa-font-awesome {\n  --fa: \"\\f2b4\"; }\n\n.fa-font-awesome-flag {\n  --fa: \"\\f2b4\"; }\n\n.fa-font-awesome-logo-full {\n  --fa: \"\\f2b4\"; }\n\n.fa-gratipay {\n  --fa: \"\\f184\"; }\n\n.fa-apple {\n  --fa: \"\\f179\"; }\n\n.fa-hive {\n  --fa: \"\\e07f\"; }\n\n.fa-gitkraken {\n  --fa: \"\\f3a6\"; }\n\n.fa-keybase {\n  --fa: \"\\f4f5\"; }\n\n.fa-apple-pay {\n  --fa: \"\\f415\"; }\n\n.fa-padlet {\n  --fa: \"\\e4a0\"; }\n\n.fa-amazon-pay {\n  --fa: \"\\f42c\"; }\n\n.fa-square-github {\n  --fa: \"\\f092\"; }\n\n.fa-github-square {\n  --fa: \"\\f092\"; }\n\n.fa-stumbleupon {\n  --fa: \"\\f1a4\"; }\n\n.fa-fedex {\n  --fa: \"\\f797\"; }\n\n.fa-phoenix-framework {\n  --fa: \"\\f3dc\"; }\n\n.fa-shopify {\n  --fa: \"\\e057\"; }\n\n.fa-neos {\n  --fa: \"\\f612\"; }\n\n.fa-square-threads {\n  --fa: \"\\e619\"; }\n\n.fa-hackerrank {\n  --fa: \"\\f5f7\"; }\n\n.fa-researchgate {\n  --fa: \"\\f4f8\"; }\n\n.fa-swift {\n  --fa: \"\\f8e1\"; }\n\n.fa-angular {\n  --fa: \"\\f420\"; }\n\n.fa-speakap {\n  --fa: \"\\f3f3\"; }\n\n.fa-angrycreative {\n  --fa: \"\\f36e\"; }\n\n.fa-y-combinator {\n  --fa: \"\\f23b\"; }\n\n.fa-empire {\n  --fa: \"\\f1d1\"; }\n\n.fa-envira {\n  --fa: \"\\f299\"; }\n\n.fa-google-scholar {\n  --fa: \"\\e63b\"; }\n\n.fa-square-gitlab {\n  --fa: \"\\e5ae\"; }\n\n.fa-gitlab-square {\n  --fa: \"\\e5ae\"; }\n\n.fa-studiovinari {\n  --fa: \"\\f3f8\"; }\n\n.fa-pied-piper {\n  --fa: \"\\f2ae\"; }\n\n.fa-wordpress {\n  --fa: \"\\f19a\"; }\n\n.fa-product-hunt {\n  --fa: \"\\f288\"; }\n\n.fa-firefox {\n  --fa: \"\\f269\"; }\n\n.fa-linode {\n  --fa: \"\\f2b8\"; }\n\n.fa-goodreads {\n  --fa: \"\\f3a8\"; }\n\n.fa-square-odnoklassniki {\n  --fa: \"\\f264\"; }\n\n.fa-odnoklassniki-square {\n  --fa: \"\\f264\"; }\n\n.fa-jsfiddle {\n  --fa: \"\\f1cc\"; }\n\n.fa-sith {\n  --fa: \"\\f512\"; }\n\n.fa-themeisle {\n  --fa: \"\\f2b2\"; }\n\n.fa-page4 {\n  --fa: \"\\f3d7\"; }\n\n.fa-hashnode {\n  --fa: \"\\e499\"; }\n\n.fa-react {\n  --fa: \"\\f41b\"; }\n\n.fa-cc-paypal {\n  --fa: \"\\f1f4\"; }\n\n.fa-squarespace {\n  --fa: \"\\f5be\"; }\n\n.fa-cc-stripe {\n  --fa: \"\\f1f5\"; }\n\n.fa-creative-commons-share {\n  --fa: \"\\f4f2\"; }\n\n.fa-bitcoin {\n  --fa: \"\\f379\"; }\n\n.fa-keycdn {\n  --fa: \"\\f3ba\"; }\n\n.fa-opera {\n  --fa: \"\\f26a\"; }\n\n.fa-itch-io {\n  --fa: \"\\f83a\"; }\n\n.fa-umbraco {\n  --fa: \"\\f8e8\"; }\n\n.fa-galactic-senate {\n  --fa: \"\\f50d\"; }\n\n.fa-ubuntu {\n  --fa: \"\\f7df\"; }\n\n.fa-draft2digital {\n  --fa: \"\\f396\"; }\n\n.fa-stripe {\n  --fa: \"\\f429\"; }\n\n.fa-houzz {\n  --fa: \"\\f27c\"; }\n\n.fa-gg {\n  --fa: \"\\f260\"; }\n\n.fa-dhl {\n  --fa: \"\\f790\"; }\n\n.fa-square-pinterest {\n  --fa: \"\\f0d3\"; }\n\n.fa-pinterest-square {\n  --fa: \"\\f0d3\"; }\n\n.fa-xing {\n  --fa: \"\\f168\"; }\n\n.fa-blackberry {\n  --fa: \"\\f37b\"; }\n\n.fa-creative-commons-pd {\n  --fa: \"\\f4ec\"; }\n\n.fa-playstation {\n  --fa: \"\\f3df\"; }\n\n.fa-quinscape {\n  --fa: \"\\f459\"; }\n\n.fa-less {\n  --fa: \"\\f41d\"; }\n\n.fa-blogger-b {\n  --fa: \"\\f37d\"; }\n\n.fa-opencart {\n  --fa: \"\\f23d\"; }\n\n.fa-vine {\n  --fa: \"\\f1ca\"; }\n\n.fa-signal-messenger {\n  --fa: \"\\e663\"; }\n\n.fa-paypal {\n  --fa: \"\\f1ed\"; }\n\n.fa-gitlab {\n  --fa: \"\\f296\"; }\n\n.fa-typo3 {\n  --fa: \"\\f42b\"; }\n\n.fa-reddit-alien {\n  --fa: \"\\f281\"; }\n\n.fa-yahoo {\n  --fa: \"\\f19e\"; }\n\n.fa-dailymotion {\n  --fa: \"\\e052\"; }\n\n.fa-affiliatetheme {\n  --fa: \"\\f36b\"; }\n\n.fa-pied-piper-pp {\n  --fa: \"\\f1a7\"; }\n\n.fa-bootstrap {\n  --fa: \"\\f836\"; }\n\n.fa-odnoklassniki {\n  --fa: \"\\f263\"; }\n\n.fa-nfc-symbol {\n  --fa: \"\\e531\"; }\n\n.fa-mintbit {\n  --fa: \"\\e62f\"; }\n\n.fa-ethereum {\n  --fa: \"\\f42e\"; }\n\n.fa-speaker-deck {\n  --fa: \"\\f83c\"; }\n\n.fa-creative-commons-nc-eu {\n  --fa: \"\\f4e9\"; }\n\n.fa-patreon {\n  --fa: \"\\f3d9\"; }\n\n.fa-avianex {\n  --fa: \"\\f374\"; }\n\n.fa-ello {\n  --fa: \"\\f5f1\"; }\n\n.fa-gofore {\n  --fa: \"\\f3a7\"; }\n\n.fa-bimobject {\n  --fa: \"\\f378\"; }\n\n.fa-brave-reverse {\n  --fa: \"\\e63d\"; }\n\n.fa-facebook-f {\n  --fa: \"\\f39e\"; }\n\n.fa-square-google-plus {\n  --fa: \"\\f0d4\"; }\n\n.fa-google-plus-square {\n  --fa: \"\\f0d4\"; }\n\n.fa-web-awesome {\n  --fa: \"\\e682\"; }\n\n.fa-mandalorian {\n  --fa: \"\\f50f\"; }\n\n.fa-first-order-alt {\n  --fa: \"\\f50a\"; }\n\n.fa-osi {\n  --fa: \"\\f41a\"; }\n\n.fa-google-wallet {\n  --fa: \"\\f1ee\"; }\n\n.fa-d-and-d-beyond {\n  --fa: \"\\f6ca\"; }\n\n.fa-periscope {\n  --fa: \"\\f3da\"; }\n\n.fa-fulcrum {\n  --fa: \"\\f50b\"; }\n\n.fa-cloudscale {\n  --fa: \"\\f383\"; }\n\n.fa-forumbee {\n  --fa: \"\\f211\"; }\n\n.fa-mizuni {\n  --fa: \"\\f3cc\"; }\n\n.fa-schlix {\n  --fa: \"\\f3ea\"; }\n\n.fa-square-xing {\n  --fa: \"\\f169\"; }\n\n.fa-xing-square {\n  --fa: \"\\f169\"; }\n\n.fa-bandcamp {\n  --fa: \"\\f2d5\"; }\n\n.fa-wpforms {\n  --fa: \"\\f298\"; }\n\n.fa-cloudversify {\n  --fa: \"\\f385\"; }\n\n.fa-usps {\n  --fa: \"\\f7e1\"; }\n\n.fa-megaport {\n  --fa: \"\\f5a3\"; }\n\n.fa-magento {\n  --fa: \"\\f3c4\"; }\n\n.fa-spotify {\n  --fa: \"\\f1bc\"; }\n\n.fa-optin-monster {\n  --fa: \"\\f23c\"; }\n\n.fa-fly {\n  --fa: \"\\f417\"; }\n\n.fa-square-bluesky {\n  --fa: \"\\e6a3\"; }\n\n.fa-aviato {\n  --fa: \"\\f421\"; }\n\n.fa-itunes {\n  --fa: \"\\f3b4\"; }\n\n.fa-cuttlefish {\n  --fa: \"\\f38c\"; }\n\n.fa-blogger {\n  --fa: \"\\f37c\"; }\n\n.fa-flickr {\n  --fa: \"\\f16e\"; }\n\n.fa-viber {\n  --fa: \"\\f409\"; }\n\n.fa-soundcloud {\n  --fa: \"\\f1be\"; }\n\n.fa-digg {\n  --fa: \"\\f1a6\"; }\n\n.fa-tencent-weibo {\n  --fa: \"\\f1d5\"; }\n\n.fa-letterboxd {\n  --fa: \"\\e62d\"; }\n\n.fa-symfony {\n  --fa: \"\\f83d\"; }\n\n.fa-maxcdn {\n  --fa: \"\\f136\"; }\n\n.fa-etsy {\n  --fa: \"\\f2d7\"; }\n\n.fa-facebook-messenger {\n  --fa: \"\\f39f\"; }\n\n.fa-audible {\n  --fa: \"\\f373\"; }\n\n.fa-think-peaks {\n  --fa: \"\\f731\"; }\n\n.fa-bilibili {\n  --fa: \"\\e3d9\"; }\n\n.fa-erlang {\n  --fa: \"\\f39d\"; }\n\n.fa-x-twitter {\n  --fa: \"\\e61b\"; }\n\n.fa-cotton-bureau {\n  --fa: \"\\f89e\"; }\n\n.fa-dashcube {\n  --fa: \"\\f210\"; }\n\n.fa-42-group {\n  --fa: \"\\e080\"; }\n\n.fa-innosoft {\n  --fa: \"\\e080\"; }\n\n.fa-stack-exchange {\n  --fa: \"\\f18d\"; }\n\n.fa-elementor {\n  --fa: \"\\f430\"; }\n\n.fa-square-pied-piper {\n  --fa: \"\\e01e\"; }\n\n.fa-pied-piper-square {\n  --fa: \"\\e01e\"; }\n\n.fa-creative-commons-nd {\n  --fa: \"\\f4eb\"; }\n\n.fa-palfed {\n  --fa: \"\\f3d8\"; }\n\n.fa-superpowers {\n  --fa: \"\\f2dd\"; }\n\n.fa-resolving {\n  --fa: \"\\f3e7\"; }\n\n.fa-xbox {\n  --fa: \"\\f412\"; }\n\n.fa-square-web-awesome-stroke {\n  --fa: \"\\e684\"; }\n\n.fa-searchengin {\n  --fa: \"\\f3eb\"; }\n\n.fa-tiktok {\n  --fa: \"\\e07b\"; }\n\n.fa-square-facebook {\n  --fa: \"\\f082\"; }\n\n.fa-facebook-square {\n  --fa: \"\\f082\"; }\n\n.fa-renren {\n  --fa: \"\\f18b\"; }\n\n.fa-linux {\n  --fa: \"\\f17c\"; }\n\n.fa-glide {\n  --fa: \"\\f2a5\"; }\n\n.fa-linkedin {\n  --fa: \"\\f08c\"; }\n\n.fa-hubspot {\n  --fa: \"\\f3b2\"; }\n\n.fa-deploydog {\n  --fa: \"\\f38e\"; }\n\n.fa-twitch {\n  --fa: \"\\f1e8\"; }\n\n.fa-flutter {\n  --fa: \"\\e694\"; }\n\n.fa-ravelry {\n  --fa: \"\\f2d9\"; }\n\n.fa-mixer {\n  --fa: \"\\e056\"; }\n\n.fa-square-lastfm {\n  --fa: \"\\f203\"; }\n\n.fa-lastfm-square {\n  --fa: \"\\f203\"; }\n\n.fa-vimeo {\n  --fa: \"\\f40a\"; }\n\n.fa-mendeley {\n  --fa: \"\\f7b3\"; }\n\n.fa-uniregistry {\n  --fa: \"\\f404\"; }\n\n.fa-figma {\n  --fa: \"\\f799\"; }\n\n.fa-creative-commons-remix {\n  --fa: \"\\f4ee\"; }\n\n.fa-cc-amazon-pay {\n  --fa: \"\\f42d\"; }\n\n.fa-dropbox {\n  --fa: \"\\f16b\"; }\n\n.fa-instagram {\n  --fa: \"\\f16d\"; }\n\n.fa-cmplid {\n  --fa: \"\\e360\"; }\n\n.fa-upwork {\n  --fa: \"\\e641\"; }\n\n.fa-facebook {\n  --fa: \"\\f09a\"; }\n\n.fa-gripfire {\n  --fa: \"\\f3ac\"; }\n\n.fa-jedi-order {\n  --fa: \"\\f50e\"; }\n\n.fa-uikit {\n  --fa: \"\\f403\"; }\n\n.fa-fort-awesome-alt {\n  --fa: \"\\f3a3\"; }\n\n.fa-phabricator {\n  --fa: \"\\f3db\"; }\n\n.fa-ussunnah {\n  --fa: \"\\f407\"; }\n\n.fa-earlybirds {\n  --fa: \"\\f39a\"; }\n\n.fa-trade-federation {\n  --fa: \"\\f513\"; }\n\n.fa-autoprefixer {\n  --fa: \"\\f41c\"; }\n\n.fa-whatsapp {\n  --fa: \"\\f232\"; }\n\n.fa-square-upwork {\n  --fa: \"\\e67c\"; }\n\n.fa-slideshare {\n  --fa: \"\\f1e7\"; }\n\n.fa-google-play {\n  --fa: \"\\f3ab\"; }\n\n.fa-viadeo {\n  --fa: \"\\f2a9\"; }\n\n.fa-line {\n  --fa: \"\\f3c0\"; }\n\n.fa-google-drive {\n  --fa: \"\\f3aa\"; }\n\n.fa-servicestack {\n  --fa: \"\\f3ec\"; }\n\n.fa-simplybuilt {\n  --fa: \"\\f215\"; }\n\n.fa-bitbucket {\n  --fa: \"\\f171\"; }\n\n.fa-imdb {\n  --fa: \"\\f2d8\"; }\n\n.fa-deezer {\n  --fa: \"\\e077\"; }\n\n.fa-raspberry-pi {\n  --fa: \"\\f7bb\"; }\n\n.fa-jira {\n  --fa: \"\\f7b1\"; }\n\n.fa-docker {\n  --fa: \"\\f395\"; }\n\n.fa-screenpal {\n  --fa: \"\\e570\"; }\n\n.fa-bluetooth {\n  --fa: \"\\f293\"; }\n\n.fa-gitter {\n  --fa: \"\\f426\"; }\n\n.fa-d-and-d {\n  --fa: \"\\f38d\"; }\n\n.fa-microblog {\n  --fa: \"\\e01a\"; }\n\n.fa-cc-diners-club {\n  --fa: \"\\f24c\"; }\n\n.fa-gg-circle {\n  --fa: \"\\f261\"; }\n\n.fa-pied-piper-hat {\n  --fa: \"\\f4e5\"; }\n\n.fa-kickstarter-k {\n  --fa: \"\\f3bc\"; }\n\n.fa-yandex {\n  --fa: \"\\f413\"; }\n\n.fa-readme {\n  --fa: \"\\f4d5\"; }\n\n.fa-html5 {\n  --fa: \"\\f13b\"; }\n\n.fa-sellsy {\n  --fa: \"\\f213\"; }\n\n.fa-square-web-awesome {\n  --fa: \"\\e683\"; }\n\n.fa-sass {\n  --fa: \"\\f41e\"; }\n\n.fa-wirsindhandwerk {\n  --fa: \"\\e2d0\"; }\n\n.fa-wsh {\n  --fa: \"\\e2d0\"; }\n\n.fa-buromobelexperte {\n  --fa: \"\\f37f\"; }\n\n.fa-salesforce {\n  --fa: \"\\f83b\"; }\n\n.fa-octopus-deploy {\n  --fa: \"\\e082\"; }\n\n.fa-medapps {\n  --fa: \"\\f3c6\"; }\n\n.fa-ns8 {\n  --fa: \"\\f3d5\"; }\n\n.fa-pinterest-p {\n  --fa: \"\\f231\"; }\n\n.fa-apper {\n  --fa: \"\\f371\"; }\n\n.fa-fort-awesome {\n  --fa: \"\\f286\"; }\n\n.fa-waze {\n  --fa: \"\\f83f\"; }\n\n.fa-bluesky {\n  --fa: \"\\e671\"; }\n\n.fa-cc-jcb {\n  --fa: \"\\f24b\"; }\n\n.fa-snapchat {\n  --fa: \"\\f2ab\"; }\n\n.fa-snapchat-ghost {\n  --fa: \"\\f2ab\"; }\n\n.fa-fantasy-flight-games {\n  --fa: \"\\f6dc\"; }\n\n.fa-rust {\n  --fa: \"\\e07a\"; }\n\n.fa-wix {\n  --fa: \"\\f5cf\"; }\n\n.fa-square-behance {\n  --fa: \"\\f1b5\"; }\n\n.fa-behance-square {\n  --fa: \"\\f1b5\"; }\n\n.fa-supple {\n  --fa: \"\\f3f9\"; }\n\n.fa-webflow {\n  --fa: \"\\e65c\"; }\n\n.fa-rebel {\n  --fa: \"\\f1d0\"; }\n\n.fa-css3 {\n  --fa: \"\\f13c\"; }\n\n.fa-staylinked {\n  --fa: \"\\f3f5\"; }\n\n.fa-kaggle {\n  --fa: \"\\f5fa\"; }\n\n.fa-space-awesome {\n  --fa: \"\\e5ac\"; }\n\n.fa-deviantart {\n  --fa: \"\\f1bd\"; }\n\n.fa-cpanel {\n  --fa: \"\\f388\"; }\n\n.fa-goodreads-g {\n  --fa: \"\\f3a9\"; }\n\n.fa-square-git {\n  --fa: \"\\f1d2\"; }\n\n.fa-git-square {\n  --fa: \"\\f1d2\"; }\n\n.fa-square-tumblr {\n  --fa: \"\\f174\"; }\n\n.fa-tumblr-square {\n  --fa: \"\\f174\"; }\n\n.fa-trello {\n  --fa: \"\\f181\"; }\n\n.fa-creative-commons-nc-jp {\n  --fa: \"\\f4ea\"; }\n\n.fa-get-pocket {\n  --fa: \"\\f265\"; }\n\n.fa-perbyte {\n  --fa: \"\\e083\"; }\n\n.fa-grunt {\n  --fa: \"\\f3ad\"; }\n\n.fa-weebly {\n  --fa: \"\\f5cc\"; }\n\n.fa-connectdevelop {\n  --fa: \"\\f20e\"; }\n\n.fa-leanpub {\n  --fa: \"\\f212\"; }\n\n.fa-black-tie {\n  --fa: \"\\f27e\"; }\n\n.fa-themeco {\n  --fa: \"\\f5c6\"; }\n\n.fa-python {\n  --fa: \"\\f3e2\"; }\n\n.fa-android {\n  --fa: \"\\f17b\"; }\n\n.fa-bots {\n  --fa: \"\\e340\"; }\n\n.fa-free-code-camp {\n  --fa: \"\\f2c5\"; }\n\n.fa-hornbill {\n  --fa: \"\\f592\"; }\n\n.fa-js {\n  --fa: \"\\f3b8\"; }\n\n.fa-ideal {\n  --fa: \"\\e013\"; }\n\n.fa-git {\n  --fa: \"\\f1d3\"; }\n\n.fa-dev {\n  --fa: \"\\f6cc\"; }\n\n.fa-sketch {\n  --fa: \"\\f7c6\"; }\n\n.fa-yandex-international {\n  --fa: \"\\f414\"; }\n\n.fa-cc-amex {\n  --fa: \"\\f1f3\"; }\n\n.fa-uber {\n  --fa: \"\\f402\"; }\n\n.fa-github {\n  --fa: \"\\f09b\"; }\n\n.fa-php {\n  --fa: \"\\f457\"; }\n\n.fa-alipay {\n  --fa: \"\\f642\"; }\n\n.fa-youtube {\n  --fa: \"\\f167\"; }\n\n.fa-skyatlas {\n  --fa: \"\\f216\"; }\n\n.fa-firefox-browser {\n  --fa: \"\\e007\"; }\n\n.fa-replyd {\n  --fa: \"\\f3e6\"; }\n\n.fa-suse {\n  --fa: \"\\f7d6\"; }\n\n.fa-jenkins {\n  --fa: \"\\f3b6\"; }\n\n.fa-twitter {\n  --fa: \"\\f099\"; }\n\n.fa-rockrms {\n  --fa: \"\\f3e9\"; }\n\n.fa-pinterest {\n  --fa: \"\\f0d2\"; }\n\n.fa-buffer {\n  --fa: \"\\f837\"; }\n\n.fa-npm {\n  --fa: \"\\f3d4\"; }\n\n.fa-yammer {\n  --fa: \"\\f840\"; }\n\n.fa-btc {\n  --fa: \"\\f15a\"; }\n\n.fa-dribbble {\n  --fa: \"\\f17d\"; }\n\n.fa-stumbleupon-circle {\n  --fa: \"\\f1a3\"; }\n\n.fa-internet-explorer {\n  --fa: \"\\f26b\"; }\n\n.fa-stubber {\n  --fa: \"\\e5c7\"; }\n\n.fa-telegram {\n  --fa: \"\\f2c6\"; }\n\n.fa-telegram-plane {\n  --fa: \"\\f2c6\"; }\n\n.fa-old-republic {\n  --fa: \"\\f510\"; }\n\n.fa-odysee {\n  --fa: \"\\e5c6\"; }\n\n.fa-square-whatsapp {\n  --fa: \"\\f40c\"; }\n\n.fa-whatsapp-square {\n  --fa: \"\\f40c\"; }\n\n.fa-node-js {\n  --fa: \"\\f3d3\"; }\n\n.fa-edge-legacy {\n  --fa: \"\\e078\"; }\n\n.fa-slack {\n  --fa: \"\\f198\"; }\n\n.fa-slack-hash {\n  --fa: \"\\f198\"; }\n\n.fa-medrt {\n  --fa: \"\\f3c8\"; }\n\n.fa-usb {\n  --fa: \"\\f287\"; }\n\n.fa-tumblr {\n  --fa: \"\\f173\"; }\n\n.fa-vaadin {\n  --fa: \"\\f408\"; }\n\n.fa-quora {\n  --fa: \"\\f2c4\"; }\n\n.fa-square-x-twitter {\n  --fa: \"\\e61a\"; }\n\n.fa-reacteurope {\n  --fa: \"\\f75d\"; }\n\n.fa-medium {\n  --fa: \"\\f23a\"; }\n\n.fa-medium-m {\n  --fa: \"\\f23a\"; }\n\n.fa-amilia {\n  --fa: \"\\f36d\"; }\n\n.fa-mixcloud {\n  --fa: \"\\f289\"; }\n\n.fa-flipboard {\n  --fa: \"\\f44d\"; }\n\n.fa-viacoin {\n  --fa: \"\\f237\"; }\n\n.fa-critical-role {\n  --fa: \"\\f6c9\"; }\n\n.fa-sitrox {\n  --fa: \"\\e44a\"; }\n\n.fa-discourse {\n  --fa: \"\\f393\"; }\n\n.fa-joomla {\n  --fa: \"\\f1aa\"; }\n\n.fa-mastodon {\n  --fa: \"\\f4f6\"; }\n\n.fa-airbnb {\n  --fa: \"\\f834\"; }\n\n.fa-wolf-pack-battalion {\n  --fa: \"\\f514\"; }\n\n.fa-buy-n-large {\n  --fa: \"\\f8a6\"; }\n\n.fa-gulp {\n  --fa: \"\\f3ae\"; }\n\n.fa-creative-commons-sampling-plus {\n  --fa: \"\\f4f1\"; }\n\n.fa-strava {\n  --fa: \"\\f428\"; }\n\n.fa-ember {\n  --fa: \"\\f423\"; }\n\n.fa-canadian-maple-leaf {\n  --fa: \"\\f785\"; }\n\n.fa-teamspeak {\n  --fa: \"\\f4f9\"; }\n\n.fa-pushed {\n  --fa: \"\\f3e1\"; }\n\n.fa-wordpress-simple {\n  --fa: \"\\f411\"; }\n\n.fa-nutritionix {\n  --fa: \"\\f3d6\"; }\n\n.fa-wodu {\n  --fa: \"\\e088\"; }\n\n.fa-google-pay {\n  --fa: \"\\e079\"; }\n\n.fa-intercom {\n  --fa: \"\\f7af\"; }\n\n.fa-zhihu {\n  --fa: \"\\f63f\"; }\n\n.fa-korvue {\n  --fa: \"\\f42f\"; }\n\n.fa-pix {\n  --fa: \"\\e43a\"; }\n\n.fa-steam-symbol {\n  --fa: \"\\f3f6\"; }\n:root, :host {\n  --fa-style-family-classic: 'Font Awesome 6 Free';\n  --fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free'; }\n\n@font-face {\n  font-family: 'Font Awesome 6 Free';\n  font-style: normal;\n  font-weight: 400;\n  font-display: block;\n  src: url(\"../webfonts/fa-regular-400.woff2\") format(\"woff2\"), url(\"../webfonts/fa-regular-400.ttf\") format(\"truetype\"); }\n\n.far,\n.fa-regular {\n  font-weight: 400; }\n:root, :host {\n  --fa-style-family-classic: 'Font Awesome 6 Free';\n  --fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free'; }\n\n@font-face {\n  font-family: 'Font Awesome 6 Free';\n  font-style: normal;\n  font-weight: 900;\n  font-display: block;\n  src: url(\"../webfonts/fa-solid-900.woff2\") format(\"woff2\"), url(\"../webfonts/fa-solid-900.ttf\") format(\"truetype\"); }\n\n.fas,\n.fa-solid {\n  font-weight: 900; }\n@font-face {\n  font-family: 'Font Awesome 5 Brands';\n  font-display: block;\n  font-weight: 400;\n  src: url(\"../webfonts/fa-brands-400.woff2\") format(\"woff2\"), url(\"../webfonts/fa-brands-400.ttf\") format(\"truetype\"); }\n\n@font-face {\n  font-family: 'Font Awesome 5 Free';\n  font-display: block;\n  font-weight: 900;\n  src: url(\"../webfonts/fa-solid-900.woff2\") format(\"woff2\"), url(\"../webfonts/fa-solid-900.ttf\") format(\"truetype\"); }\n\n@font-face {\n  font-family: 'Font Awesome 5 Free';\n  font-display: block;\n  font-weight: 400;\n  src: url(\"../webfonts/fa-regular-400.woff2\") format(\"woff2\"), url(\"../webfonts/fa-regular-400.ttf\") format(\"truetype\"); }\n@font-face {\n  font-family: 'FontAwesome';\n  font-display: block;\n  src: url(\"../webfonts/fa-solid-900.woff2\") format(\"woff2\"), url(\"../webfonts/fa-solid-900.ttf\") format(\"truetype\"); }\n\n@font-face {\n  font-family: 'FontAwesome';\n  font-display: block;\n  src: url(\"../webfonts/fa-brands-400.woff2\") format(\"woff2\"), url(\"../webfonts/fa-brands-400.ttf\") format(\"truetype\"); }\n\n@font-face {\n  font-family: 'FontAwesome';\n  font-display: block;\n  src: url(\"../webfonts/fa-regular-400.woff2\") format(\"woff2\"), url(\"../webfonts/fa-regular-400.ttf\") format(\"truetype\");\n  unicode-range: U+F003,U+F006,U+F014,U+F016-F017,U+F01A-F01B,U+F01D,U+F022,U+F03E,U+F044,U+F046,U+F05C-F05D,U+F06E,U+F070,U+F087-F088,U+F08A,U+F094,U+F096-F097,U+F09D,U+F0A0,U+F0A2,U+F0A4-F0A7,U+F0C5,U+F0C7,U+F0E5-F0E6,U+F0EB,U+F0F6-F0F8,U+F10C,U+F114-F115,U+F118-F11A,U+F11C-F11D,U+F133,U+F147,U+F14E,U+F150-F152,U+F185-F186,U+F18E,U+F190-F192,U+F196,U+F1C1-F1C9,U+F1D9,U+F1DB,U+F1E3,U+F1EA,U+F1F7,U+F1F9,U+F20A,U+F247-F248,U+F24A,U+F24D,U+F255-F25B,U+F25D,U+F271-F274,U+F278,U+F27B,U+F28C,U+F28E,U+F29C,U+F2B5,U+F2B7,U+F2BA,U+F2BC,U+F2BE,U+F2C0-F2C1,U+F2C3,U+F2D0,U+F2D2,U+F2D4,U+F2DC; }\n\n@font-face {\n  font-family: 'FontAwesome';\n  font-display: block;\n  src: url(\"../webfonts/fa-v4compatibility.woff2\") format(\"woff2\"), url(\"../webfonts/fa-v4compatibility.ttf\") format(\"truetype\");\n  unicode-range: U+F041,U+F047,U+F065-F066,U+F07D-F07E,U+F080,U+F08B,U+F08E,U+F090,U+F09A,U+F0AC,U+F0AE,U+F0B2,U+F0D0,U+F0D6,U+F0E4,U+F0EC,U+F10A-F10B,U+F123,U+F13E,U+F148-F149,U+F14C,U+F156,U+F15E,U+F160-F161,U+F163,U+F175-F178,U+F195,U+F1F8,U+F219,U+F27A; }\n";

var bimViewerStyles = "/* Toolbars and buttons */\n\n.xeokit-toolbar {\n    display: flex;\n    flex-wrap: wrap;\n    justify-content: flex-start;\n    background: transparent;\n    pointer-events: none;\n    padding: 10px;\n}\n\n.xeokit-btn-group {\n    position: relative;\n    display: inline-flex;\n    vertical-align: middle;\n    margin-bottom: 3px;\n    margin-right: 1.2rem !important;\n    pointer-events: none;\n}\n\n.xeokit-btn {\n    position: relative;\n    flex: 1 1 auto;\n    color: #03103F;\n    background-color: white;\n    text-align: center;\n    vertical-align: middle;\n    border: 2px solid #1d2453;\n    padding: .375rem .75rem;\n    border-radius: .25rem;\n    transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out;\n    -webkit-appearance: button;\n    overflow: visible;\n    margin: 0 2px 0 0;\n    box-sizing: border-box;\n    align-items: flex-start;\n    pointer-events: all;\n}\n\n.xeokit-btn.disabled {\n    opacity: .55;\n    color: #99A4AD;\n    border-color: #6c757d !important;\n    pointer-events: none;\n}\n\n.xeokit-btn.active {\n    color: #fff;\n    background-color: #03103F;\n    border-color: #03103F !important;\n}\n\n.xeokit-btn:hover {\n    color: #477dca;\n    border-color: #477dca !important;\n}\n\n.xeokit-btn.active:hover {\n    /*color: transparent;*/\n    color: white;\n    background: #477dca;\n    border-color: #477dca !important;\n}\n\n.xeokit-btn.disabled:hover {\n    opacity: .55;\n    color: #99A4AD;\n    border-color: #6c757d !important;\n}\n\n/*  Tabs    */\n\n.xeokit-tabs {\n    line-height: 1.5;\n    padding: 0;\n    /*padding-left: 10px;*/\n    height: 100%;\n    overflow: clip;\n}\n\n.xeokit-tabs::before, .xeokit-tabs::after {\n    display: table;\n    content: '';\n    clear: both;\n}\n\n@media (max-width: 40em) {\n    .xeokit-tabs {\n\n    }\n}\n\n.xeokit-tabs .xeokit-tab {\n    list-style: none;\n}\n\n@media (min-width: 40em) {\n    .xeokit-tabs .xeokit-tab {\n        display: inline;\n    }\n}\n\n.xeokit-tabs .xeokit-tab:first-child .xeokit-tab-btn {\n}\n\n@media (max-width: 40em) {\n    .xeokit-tabs .xeokit-tab:first-child .xeokit-tab-btn {\n    }\n}\n\n@media (max-width: 40em) {\n    .xeokit-tabs .xeokit-tab:last-child .xeokit-tab-btn {\n    }\n}\n\n.xeokit-tabs .xeokit-tab.active .xeokit-tab-btn {\n    background-color: #313961;\n}\n\n@media (min-width: 40em) {\n    .xeokit-tabs .xeokit-tab.active .xeokit-tab-btn {\n        background-color: transparent;\n        border-bottom: #CFCFCF solid 3px;\n        margin-bottom: -1px;\n    }\n}\n\n.xeokit-tabs .xeokit-tab.active .xeokit-tab-content {\n    display: block;\n}\n\n.xeokit-tabs .xeokit-tab-btn {\n    font-size: 16px;\n    background-color: transparent;\n    border-top: 2px solid #dcdcdc;\n    color: #CFCFCF;\n    display: block;\n    font-weight: bold;\n    padding: 0.75em 5px;\n    padding-left: 0;\n    padding-right: 10px;\n    text-decoration: none;\n}\n\n@media (min-width: 40em) {\n    .xeokit-tabs .xeokit-tab-btn {\n        font-size: 20px;\n        font-weight: normal;\n        border-top-left-radius: 3px;\n        border-top-right-radius: 3px;\n        border-top: 0;\n        display: inline-block;\n    }\n}\n\n.xeokit-tabs .xeokit-tab-btn:hover {\n    color: #477dca;\n}\n\n.xeokit-tabs .xeokit-tab-btn:focus {\n    outline: none;\n}\n\n.xeokit-tabs .xeokit-tab-content {\n    background: #03103F;\n    display: none;\n    padding-top: 2em;\n    padding-left: 0;\n    padding-right: 0px;\n    width: calc(100% - 0px);\n    height: calc(100% - 30px);\n}\n\n@media (min-width: 40em) {\n    .xeokit-tabs .xeokit-tab-content {\n        float: left;\n    }\n}\n\n.xeokit-tabs .xeokit-btn-group {\n    flex-wrap: wrap;\n    justify-content: flex-start;\n    margin-bottom: 10px;\n}\n\n.xeokit-tabs .xeokit-btn {\n    font-size: 16px;\n    font-weight: normal;\n    position: relative;\n    flex: 1 1 auto;\n    color: #CFCFCF;\n    background-color: transparent;\n    text-align: center;\n    vertical-align: middle;\n    border: 2px solid #CFCFCF;\n    padding: .75rem .75rem;\n    margin: 0 5px 10px 0;\n    border-radius: .35rem;\n    transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out;\n    -webkit-appearance: button;\n    overflow: visible;\n    box-sizing: border-box;\n    align-items: flex-start;\n    /*font: 400 13.3333px Arial;*/\n}\n\n.xeokit-tabs .xeokit-btn:hover {\n    color: #477dca;\n    border: 2px solid #477dca;\n    /*background: black;*/\n}\n\n.xeokit-tabs .xeokit-tab-btn.disabled {\n    opacity: .55;\n    color: #99A4AD;\n}\n\n.xeokit-modelsTab {\n\n}\n\n.xeokit-modelsTab .xeokit-form-check {\n    padding: 2px 0 2px 15px;\n    line-height: 3ex;\n}\n\n.xeokit-modelsTab .xeokit-form-check input {\n    vertical-align: middle;\n}\n\n.xeokit-modelsTab .xeokit-form-check span {\n    display: inline-block;\n    color: #ffff;\n    background-color: #03103F;\n    padding-left: 3px;\n    width: calc(100% - 30px);\n    vertical-align: middle;\n}\n\n.xeokit-modelsTab .xeokit-form-check span.disabled {\n    color: #99A4AD;\n    background-color: #03103F;\n}\n\n.xeokit-modelsTab .xeokit-form-check span:hover {\n    color: #ffffff;\n    cursor: pointer;\n    background: rgba(255, 255, 255, 0.2);\n    padding-left: 3px;\n}\n\n.xeokit-modelsTab .xeokit-form-check span.disabled:hover {\n    color: #99A4AD;\n    background-color: #03103F;\n    cursor: default;\n}\n\n.xeokit-objectsTab ul {\n    list-style: none;\n    padding-left: 1.75em;\n}\n\n.xeokit-objectsTab ul li {\n    margin: 2px 0;\n    position: relative;\n    width: 500px;\n    line-height: 3ex;\n}\n\n.xeokit-objectsTab ul li a {\n    background-color: #eee;\n    border-radius: 50%;\n    color: #000;\n    display: inline-block;\n    height: 1.5em;\n    left: -1.5em;\n    /*line-height: 1.5em;*/\n    position: absolute;\n    text-align: center;\n    text-decoration: none;\n    width: 1.5em;\n}\n\n.xeokit-objectsTab ul li a.plus {\n    background-color: #ded;\n}\n\n.xeokit-objectsTab ul li a.minus {\n    background-color: #eee;\n}\n\n.xeokit-objectsTab ul li a:active {\n    top: 1px;\n}\n\n.xeokit-objectsTab ul li input {\n    vertical-align: middle;\n}\n\n.xeokit-objectsTab ul li span {\n    display: inline-block;\n    width: calc(100% - 50px);\n    padding-left: 2px;\n    vertical-align: middle;\n}\n\n.xeokit-objectsTab ul li span:hover {\n    color: #ffffff;\n    cursor: pointer;\n    background: rgba(255, 255, 255, 0.2);\n    padding-left: 2px;\n    vertical-align: middle;\n}\n\n.xeokit-objectsTab .top-right {\n    font-size: small;\n    position: fixed;\n    right: 1em;\n    top: 1em;\n}\n\n.xeokit-objectsTab .highlighted-node { /* Appearance of node highlighted with BIMViewer#showObjectInExplorers() */\n    border: black solid 1px;\n    background: yellow;\n    color: black;\n    padding-left: 1px;\n    padding-right: 5px;\n}\n\n.xeokit-storeysTab ul {\n    list-style: none;\n    padding-left: 1.75em;\n}\n\n.xeokit-storeysTab ul li {\n    margin: 2px 0;\n    position: relative;\n    width: 500px;\n    line-height: 3ex;\n}\n\n.xeokit-storeysTab ul li a {\n    background-color: #eee;\n    border-radius: 50%;\n    color: #000;\n    display: inline-block;\n    height: 1.5em;\n    left: -1.5em;\n    /*line-height: 1.5em;*/\n    position: absolute;\n    text-align: center;\n    text-decoration: none;\n    width: 1.5em;\n}\n\n.xeokit-storeysTab ul li a.plus {\n    background-color: #ded;\n}\n\n.xeokit-storeysTab ul li a.minus {\n    background-color: #eee;\n}\n\n.xeokit-storeysTab ul li a:active {\n    top: 1px;\n}\n\n.xeokit-storeysTab ul li input {\n    vertical-align: middle;\n}\n\n.xeokit-storeysTab ul li span {\n    display: inline-block;\n    width: calc(100% - 50px);\n    padding-left: 2px;\n    vertical-align: middle;\n}\n\n.xeokit-storeysTab ul li span:hover {\n    color: white;\n    cursor: pointer;\n    background: rgba(255, 255, 255, 0.2);\n    padding-left: 2px;\n}\n\n.xeokit-storeysTab .top-right {\n    font-size: small;\n    position: fixed;\n    right: 1em;\n    top: 1em;\n}\n\n.xeokit-storeysTab .highlighted-node { /* Appearance of node highlighted with BIMViewer#showObjectInExplorers() */\n    border: black solid 1px;\n    background: yellow;\n    color: black;\n    padding-left: 1px;\n    padding-right: 5px;\n}\n\n.xeokit-classesTab ul {\n    list-style: none;\n    padding-left: 1.75em;\n}\n\n.xeokit-classesTab ul li {\n    margin: 2px 0;\n    position: relative;\n    width: 500px;\n    line-height: 3ex;\n}\n\n.xeokit-classesTab ul li a {\n    background-color: #eee;\n    border-radius: 50%;\n    color: #000;\n    display: inline-block;\n    height: 1.5em;\n    left: -1.5em;\n    /*line-height: 1.5em;*/\n    position: absolute;\n    text-align: center;\n    text-decoration: none;\n    width: 1.5em;\n}\n\n.xeokit-classesTab ul li a.plus {\n    background-color: #ded;\n}\n\n.xeokit-classesTab ul li a.minus {\n    background-color: #eee;\n}\n\n.xeokit-classesTab ul li a:active {\n    top: 1px;\n}\n\n.xeokit-classesTab ul li input {\n    vertical-align: middle;\n}\n\n.xeokit-classesTab ul li span {\n    display: inline-block;\n    width: calc(100% - 50px);\n    padding-left: 2px;\n    vertical-align: middle;\n}\n\n.xeokit-classesTab ul li span:hover {\n    color: white;\n    cursor: pointer;\n    background: rgba(255, 255, 255, 0.2);\n    padding-left: 2px;\n    vertical-align: middle;\n}\n\n.xeokit-classesTab .top-right {\n    font-size: small;\n    position: fixed;\n    right: 1em;\n    top: 1em;\n}\n\n.xeokit-classesTab .highlighted-node { /* Appearance of node highlighted with BIMViewer#showObjectInExplorers() */\n    border: black solid 1px;\n    background: yellow;\n    color: black;\n    padding-left: 1px;\n    padding-right: 5px;\n}\n\n.xeokit-tree-panel {\n    overflow-y: auto;\n    overflow-x: auto;\n    height: calc(100% - 150px);\n}\n\n.xeokit-tree-panel .warn,\n.xeokit-tree-panel .warn::before,\n.xeokit-tree-panel .warn::after {\n    /*position: relative;*/\n    padding: 0;\n    margin: 0;\n}\n\n.xeokit-tree-panel .warn {\n    font-size: 20px;\n    color: transparent;\n}\n\n.xeokit-tree-panel .warn.warning {\n    display: inline-block;\n    top: 0.225em;\n    width: 1.15em;\n    height: 1.15em;\n    overflow: hidden;\n    border: none;\n    background-color: transparent;\n    border-radius: 0.625em;\n}\n\n.xeokit-tree-panel .warn.warning::before {\n    content: \"\";\n    display: block;\n    top: 0.0em;\n    left: 0.0em;\n    position: absolute;\n    border: transparent 0.6em solid;\n    border-bottom-color: #fd3;\n    border-bottom-width: 1em;\n    border-top-width: 0;\n    box-shadow: #999 0 1px 1px;\n}\n\n.xeokit-tree-panel .warn.warning::after {\n    display: block;\n    position: absolute;\n    top: 0.3em;\n    left: 0;\n    width: 100%;\n    padding: 0 1px;\n    text-align: center;\n    font-family: \"Garamond\";\n    content: \"!\";\n    font-size: 0.65em;\n    font-weight: bold;\n    color: #333;\n}\n\n/* Context menu */\n\n.xeokit-context-menu {\n    font-family: 'Roboto', sans-serif;\n    font-size: 15px;\n    display: none;\n    z-index: 300000;\n    background: rgba(255, 255, 255, 0.46);\n    border: 1px solid black;\n    border-radius: 6px;\n    padding: 0;\n    width: 200px;\n}\n\n.xeokit-context-menu ul {\n    list-style: none;\n    margin-left: 0;\n    padding: 0;\n}\n\n.xeokit-context-menu-item {\n    list-style-type: none;\n    padding-left: 10px;\n    padding-right: 20px;\n    padding-top: 4px;\n    padding-bottom: 4px;\n    color: black;\n    background: rgba(255, 255, 255, 0.46);\n    cursor: pointer;\n    width: calc(100% - 30px);\n}\n\n.xeokit-context-menu-item:hover {\n    background: black;\n    color: white;\n    font-weight: normal;\n}\n\n.xeokit-context-menu-item span {\n    display: inline-block;\n}\n\n.xeokit-context-menu .disabled {\n    display: inline-block;\n    color: gray;\n    cursor: default;\n    font-weight: normal;\n}\n\n.xeokit-context-menu .disabled:hover {\n    color: gray;\n    cursor: default;\n    background: #eeeeee;\n    font-weight: normal;\n}\n\n.xeokit-context-menu-item-separator {\n    background: rgba(0, 0, 0, 1);\n    height: 1px;\n    width: 100%;\n}\n\n.xeokit-context-submenu {\n    font-family: 'Roboto', sans-serif;\n    font-size: 15px;\n    display: none;\n    z-index: 300000;\n    background: rgba(255, 255, 255, 0.46);\n    border: 1px solid black;\n    border-radius: 0 6px 6px 6px !important;\n    padding: 0;\n    width: 220px;\n}\n\n/* Busy modal dialog */\n\n.xeokit-busy-modal-backdrop {\n    position: relative;\n}\n\n.xeokit-busy-modal {\n    display: none;\n    position: absolute;\n    z-index: 1000000;\n    padding-top: 100px;\n    left: 0;\n    top: 0;\n    width: 100%;\n    height: 100%;\n    background-color: rgba(3, 16, 63, 0.4);\n}\n\n.xeokit-busy-modal-content {\n    position: relative;\n    background-color: #FFFFFF;\n    margin: auto;\n    padding: 0;\n    border: 2px solid #888;\n    border-radius: 0.5em;\n    width: 30%;\n    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);\n    -webkit-animation-name: xeokit-busy-modal-animatetop;\n    -webkit-animation-duration: 0.6s;\n    animation-name: xeokit-busy-modal-animatetop;\n    animation-duration: .6s\n}\n\n/* Add Animation */\n@-webkit-keyframes xeokit-busy-modal-animatetop {\n    from {\n        opacity: 0\n    }\n    to {\n        opacity: 1\n    }\n}\n\n@keyframes xeokit-busy-modal-animatetop {\n    from {\n        opacity: 0\n    }\n    to {\n        opacity: 1\n    }\n}\n\n.xeokit-busy-modal-message {\n    font-size: 22px;\n    padding: 2px 14px;\n    background-color: #FFFFFF;\n    color: #212529;\n}\n\n.xeokit-busy-modal-body {\n    padding: 2px 14px;\n    margin: 20px 2px\n}\n\n/* Styles the dot that indicates the camera's current pivot position */\n\n.xeokit-camera-pivot-marker {\n    color: #ffffff;\n    line-height: 1.8;\n    text-align: center;\n    font-family: \"monospace\";\n    font-weight: bold;\n    position: absolute;\n    width: 25px;\n    height: 25px;\n    border-radius: 15px;\n    border: 2px solid #ebebeb;\n    background: black;\n    visibility: hidden;\n    box-shadow: 5px 5px 15px 1px #000000;\n    z-index: 1000000;\n    pointer-events: none;\n}\n\n.xeokit-section {\n\n}\n\n.xeokit-section-counter {\n    opacity: 1.0;\n    color: black;\n    padding: 1px;\n    margin: 0;\n    margin-left: 10px;\n    width: 20px;\n    height: 20px;\n    float: right;\n    font-family: 'Roboto', sans-serif;\n    font-size: 18px;\n    line-height: 1.50;\n}\n\n.xeokit-section-counter.disabled {\n    color: #99A4AD;\n}\n\n.xeokit-section-counter.active {\n    color: #fff;\n    border-color: #ebebeb;\n}\n\n.xeokit-section-counter:hover {\n    color: #477dca;\n}\n\n.xeokit-section-counter.active:hover {\n    color: white;\n}\n\n.xeokit-section-counter.disabled:hover {\n    color: #99A4AD;\n}\n\n.xeokit-section-menu-button {\n    opacity: 1.0;\n    border-radius: 5px;\n    border: 0;\n    color: black;\n    padding: 1px;\n    margin: 0;\n    margin-left: 10px;\n    width: 20px;\n    height: 20px;\n    float: right;\n    font-family: 'Roboto', sans-serif;\n    font-size: 14px;\n    line-height: 1.50;\n}\n\n.xeokit-section-menu-button.disabled {\n    color: #99A4AD;\n    border: 0;\n}\n\n.xeokit-section-menu-button.active {\n    color: #fff;\n    border-color: #ebebeb;\n}\n\n.xeokit-section-menu-button:hover {\n    color: #477dca;\n}\n\n.xeokit-section-menu-button.active:hover {\n    color: white;\n    /*border: 2px solid #477dca;*/\n}\n\n.xeokit-section-menu-button.disabled:hover {\n    color: #99A4AD;\n}\n\n.xeokit-arrow-up {\n    width: 0;\n    height: 0;\n    display: inline-block;\n    vertical-align: middle;\n    border-left: 8px solid transparent;\n    border-right: 8px solid transparent;\n    border-bottom: 8px solid black !important;\n}\n\n.xeokit-arrow-up.disabled {\n    border-bottom: 8px solid #99A4AD !important;\n}\n\n.xeokit-arrow-up.active {\n    border-bottom: 8px solid white !important;\n}\n\n.xeokit-arrow-up:hover {\n    border-bottom: 8px solid #477dca !important;\n}\n\n.xeokit-arrow-up.active:hover {\n    border-bottom: 8px solid white !important;\n}\n\n.xeokit-arrow-up.disabled:hover {\n    border-bottom: 8px solid #99A4AD !important;\n}\n\n.xeokit-arrow-down {\n    width: 0;\n    height: 0;\n    display: inline-block;\n    vertical-align: middle;\n    border-style: solid;\n    border-width: 8px 8px 0 8px;\n    border-color: black transparent transparent transparent !important;\n}\n\n.xeokit-arrow-down.disabled {\n    color: #99A4AD !important;\n}\n\n.xeokit-arrow-down.active {\n    color: white !important;\n    border-color: white transparent transparent transparent !important;\n}\n\n.xeokit-arrow-down:hover {\n    color: white !important;\n    border-color: #477dca transparent transparent transparent !important;\n}\n\n.xeokit-arrow-down.active:hover {\n    color: white !important;\n    border-color: white transparent transparent transparent !important;\n}\n\n.xeokit-arrow-down.disabled:hover {\n    border-color: #99A4AD transparent transparent transparent !important;\n}\n\n.xeokit-properties {\n    display: flex;\n    flex-direction: column;\n    height: calc(100% - 2em - 30px);\n}\n\n.xeokit-accordion {\n    display: flex;\n    flex-direction: column;\n    flex: 1 1 auto;\n    width: 100%;\n    overflow-y: auto;\n    overflow-x: hidden;\n    \n}\n.xeokit-accordion .xeokit-accordion-container {\n    display: flex;\n    flex-direction: column;\n    width: 100%;\n    padding-bottom: 5px;\n}\n.xeokit-accordion .xeokit-accordion-container .xeokit-accordion-button {\n    margin: 0;\n    position: relative;\n    padding: 15px 15px;\n    color: #bdbdbd;\n    display: block;\n    font-weight: 500;\n    background-color: #313961;\n    cursor: pointer;\n    transition: all 0.3s ease-in-out;\n    border-radius: 5px;\n    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, .15), 0 10px 10px -5px rgba(0, 0, 0, .1) !important;\n}\n.xeokit-accordion .xeokit-accordion-container .xeokit-accordion-button span {\n    display: block;\n    position: absolute;\n    height: 14px;\n    width: 14px;\n    right: 20px;\n    top: 18px;\n}\n.xeokit-accordion .xeokit-accordion-container .xeokit-accordion-button span:after {\n    content: '';\n    width: 14px;\n    height: 3px;\n    border-radius: 2px;\n    background-color: #fff;\n    position: absolute;\n    top: 6px;\n}\n.xeokit-accordion .xeokit-accordion-container .xeokit-accordion-button span:before {\n    content: '';\n    width: 14px;\n    height: 3px;\n    border-radius: 2px;\n    background-color: #fff;\n    position: absolute;\n    top: 6px;\n    transform: rotate(90deg);\n    transition: all 0.3s ease-in-out;\n}\n.xeokit-accordion .xeokit-accordion-container .xeokit-accordion-panel {\n    display: none;\n    width: 100%;\n    color: #cfcfcf;\n    transition: height 0.3s ease-in-out, opacity 0.2s ease-in-out;\n    opacity: 0;\n    height: auto;\n    height: 0;\n    padding: 0 10px;\n}\n.xeokit-accordion .xeokit-accordion-container.active .xeokit-accordion-button {\n    color: #cfcfcf;\n}\n.xeokit-accordion .xeokit-accordion-container.active .xeokit-accordion-button span::before {\n    transform: rotate(0deg);\n}\n.xeokit-accordion .xeokit-accordion-container.active .xeokit-accordion-panel {\n    display: block;\n    padding: 0;\n    opacity: 1;\n}\n\n\n\n\n.xeokit-propertiesTab .element-attributes {\n    display: flex;\n    flex: 0 0 auto;\n    padding-left: 10px;\n}\n\n.xeokit-propertiesTab .xeokit-tab-btn {\n    margin-left: 15px;\n}\n\n.xeokit-propertiesTab .title {\n    color: #CFCFCF;\n    font-size: 20px;\n    font-weight: normal;\n    padding-left: 0;\n    padding-top: 0;\n    padding-bottom: 20px;\n    margin-top: 12px;\n}\n\n.xeokit-propertiesTab .subtitle {\n    color: #CFCFCF;\n    font-size: 16px;\n    font-weight: normal;\n    padding-left: 0;\n}\n\n.xeokit-propertiesTab .subsubtitle {\n    color: #CFCFCF;\n    font-size: 14px;\n    font-weight: normal;\n    padding-left: 0;\n}\n\n.xeokit-propertiesTab .panel {\n    padding: 0 18px;\n    background: #03103F;\n    color: #CFCFCF;\n    max-height: 0;\n    overflow: hidden;\n    transition: max-height 0.15s;\n}\n\n.xeokit-propertiesTab input {\n    position: absolute;\n    opacity: 0;\n    z-index: -1;\n}\n\n.xeokit-propertiesTab .row {\n    display: flex;\n}\n\n.xeokit-propertiesTab .row .col {\n    flex: 1;\n}\n\n.xeokit-propertiesTab .row .col:last-child {\n    margin-left: 1em;\n}\n\n.xeokit-propertiesTab .tabs {\n    border-radius: 8px;\n    overflow: hidden;\n    box-shadow: 0 4px 4px -2px rgba(0, 0, 0, 0.5);\n}\n\n.xeokit-propertiesTab .tab {\n    width: 100%;\n    color: #CFCFCF;\n    overflow: hidden;\n}\n\n.xeokit-propertiesTab .tab-label {\n    display: flex;\n    padding: .5em .5em .5em 1em;\n    font-weight: normal;\n    cursor: pointer;\n}\n\n.xeokit-propertiesTab .tab-label:hover {\n    cursor: pointer;\n    background: rgba(255, 255, 255, 0.2);\n}\n\n.xeokit-propertiesTab .tab-label::before {\n    content: \"\\276F\";\n    width: 1em;\n    height: 1em;\n    text-align: center;\n    transition: all 0.35s;\n}\n\n.xeokit-propertiesTab .tab-content {\n    max-height: 0;\n    padding: 0 1em;\n    transition: all 0.35s;\n    overflow: hidden;\n}\n\n.xeokit-btn {\n    box-shadow: 1px 2px 1px rgba(0, 0, 0, 0.1);\n}\n\n.xeokit-table {\n    width: 100%;\n    margin-bottom: 1rem;\n    display: table;\n    /*border-collapse: separate;*/\n    box-sizing: border-box;\n    text-indent: initial;\n    /*border-spacing: 2px;*/\n}\n\n.xeokit-table .td1, .xeokit-table th {\n    padding: .6em;\n    vertical-align: top;\n    border-top: 1px solid #81819a;\n    border-right: 1px solid #81819a;\n    white-space: nowrap;\n    font-weight: bold;\n    width: 150px;\n    max-width: 150px;\n}\n\n.xeokit-table tr:first-child td {\n    border-top: 0;\n}\n\n.xeokit-table .td2, .xeokit-table th {\n    padding: .5em;\n    vertical-align: top;\n    border-top: 1px solid #81819a;\n}\n\n.xeokit-table th {\n    text-align: inherit;\n}\n\n.xeokit-table th {\n    display: table-cell;\n    vertical-align: inherit;\n    font-weight: bold;\n}\n\n.xeokit-no-prop-set-warning {\n    visibility: visible\n}\n\n.no-object-selected-warning {\n\n}\n\n.viewer-ruler-wire-highlighted {\n    border: 2px solid white !important;\n}\n\n.viewer-ruler-label-highlighted {\n    border: 2px solid white !important;\n}\n\n.viewer-ruler-dot-highlighted {\n    border: 2px solid white !important;\n}\n\n";

const headStyleInnerHtml = `
    @font-face {
        font-family: 'Font Awesome 6 Free';
        font-display: block;
        font-weight: 900;
        src: url(${faSolidWoff}) format("woff2"), url(${faSolidTtf}) format("truetype");
    }
    @font-face {
        font-family: 'Font Awesome 6 Free';
        font-display: block;
        font-weight: 400;
        src: url(${faRegularWoff}) format("woff2"), url(${faRegularTtf}) format("truetype");
    }
    @font-face {
        font-family: 'Font Awesome 5 Free';
        font-display: block;
        font-weight: 900;
        src: url(${faSolidWoff}) format("woff2"), url(${faSolidTtf}) format("truetype");
    }
    @font-face {
        font-family: 'Font Awesome 5 Free';
        font-display: block;
        font-weight: 400;
        src: url(${faRegularWoff}) format("woff2"), url(${faRegularTtf}) format("truetype");
    }
    @font-face {
        font-family: 'Font Awesome 6 Brands';
        font-display: block;
        font-weight: 400;
        src: url(${faBrandsWoff}) format("woff2"), url(${faBrandsTtf}) format("truetype");
    }
    @font-face {
        font-family: 'Font Awesome 5 Brands';
        font-display: block;
        font-weight: 400;
        src: url(${faBrandsWoff}) format("woff2"), url(${faBrandsTtf}) format("truetype");
    }
`;

const innerHtml = `
    <input type="checkbox" id="explorer_toggle" />
    <label for="explorer_toggle" class="xeokit-i18n explorer_toggle_label xeokit-btn fas fa-2x fa-sitemap"
        data-xeokit-i18ntip="toolbar.toggleExplorer" data-tippy-content="Toggle explorer"></label>
    <input type="checkbox" id="inspector_toggle" />
    <label id="inspector_toggle_label" for="inspector_toggle"
        class="xeokit-i18n inspector_toggle_label xeokit-btn fas fa-info-circle fa-2x"
        data-xeokit-i18ntip="toolbar.toggleProperties" data-tippy-content="Toggle properties"></label>
    <div id="myExplorer"></div>
    <div id="myToolbar"></div>
    <div id="myInspector"></div>
    <div id="myViewer">
        <canvas id="myCanvas"></canvas>
        <canvas id="myNavCubeCanvas"></canvas>
    </div>
    <div class="xeokit-marker"></div>
    <style type="text/css">
        ${fontStyle}
        ${bimViewerStyles}

        :host {
            display: block;
            position: relative;
            width: 100%;
            height: 100%;
            font-family: "Roboto", sans-serif;
            font-size: 14px;
            line-height: 1.5;
            -webkit-font-smoothing: antialiased;
            margin: 0;
            overflow: clip;
            background: #f2f2f2;
        }

        :host #myViewer {
            display: flex;
            width: 100%;
            height: 100%;
            align-items: stretch;
            z-index: 10000;
            overflow: clip;
            position: absolute;
            left: 0;
            right: 0;
            --left: 0;
            --right: 0;
            transition: all 300ms ease-in-out;
        }

        :host #myToolbar {
            font-family: 'Font Awesome 6 Free';
            min-width: 400px;
            top: 0;
            align-items: center;
            justify-content: center;
            padding: 0;
            z-index: 100000;
            pointer-events: none;
            position: absolute;
            left: 95px;
            transition: all 300ms ease-in-out;
        }

        :host #myCanvas {
            width: 100%;
            height: 100%;
            cursor: default;
            pointer-events: all;
            margin: 0;
            position: relative;
        }

        :host #myNavCubeCanvas {
            position: absolute;
            width: 200px;
            height: 200px;
            bottom: 0;
            right: 0;
            z-index: 200000;
        }

        :host #myExplorer {
            position: absolute;
            height: 100%;
            color: #fff;
            background: #03103f;
            overflow: auto;
            border-right: 2px solid #212529;
            padding: 0px;
            padding-bottom: 100px;
            padding-left: 15px;
            user-select: none;
            top: 0;
            left: -460px;
            z-index: 10;
            width: 460px;
            transition: all 300ms ease-in-out;
        }

        :host #explorer_toggle {
            display: none;
        }

        :host .explorer_toggle_label {
            position: absolute;
            top: 10px;
            left: 20px;
            -webkit-user-select: none;
            -ms-user-select: none;
            user-select: none;
            flex: 1 1 auto;
            color: #03103f;
            background-color: white;
            text-align: center;
            vertical-align: middle;
            border: 2px solid #1d2453;
            padding: 0.375rem 0.75rem;
            border-radius: 0.25rem;
            -webkit-appearance: button;
            overflow: visible;
            margin: 0 2px 0 0;
            box-sizing: border-box;
            align-items: flex-start;
            pointer-events: all;
            z-index: 100000;
            transition: all 300ms ease-in-out;
        }

        :host .explorer_toggle_label:hover {
            cursor: pointer;
        }

        :host #explorer_toggle:checked + .explorer_toggle_label {
            left: 480px;
            color: #fff;
            background-color: #03103f;
            border-color: #03103f;
        }

        :host #explorer_toggle:checked ~ #myExplorer {
            left: 0;
        }

        :host #explorer_toggle:checked ~ #myViewer {
            left: 460px;
            --left: 460;
            width: calc(100% - (var(--left) + var(--right)));
        }

        :host #explorer_toggle:checked ~ #myToolbar {
            left: 555px;
        }

        :host #explorer_toggle:not(:checked) + .explorer_toggle_label {
            left: 20px;
            color: #fff;
            background-color: #03103f;
            border-color: #03103f;
        }

        :host #explorer_toggle:not(:checked) ~ #myExplorer {
            left: -460px;
        }

        :host #explorer_toggle:not(:checked) ~ #myViewer {
            left: 0;
            --left: 0;
            width: calc(100% - var(--right));
        }

        :host #explorer_toggle:not(:checked) ~ #myToolbar {
            left: 95px;
        }

        :host #myInspector {
            position: absolute;
            height: 100%;
            color: #fff;
            background: #03103f;
            overflow: auto;
            border-left: 2px solid #212529;
            padding: 0;
            top: 0;
            right: -360px;
            z-index: 40;
            width: 358px;
            transition: all 300ms ease-in-out;
        }

        :host #inspector_toggle {
            display: none;
        }

        :host .inspector_toggle_label {
            position: absolute;
            top: 10px;
            right: 20px;
            -webkit-user-select: none;
            -ms-user-select: none;
            user-select: none;
            flex: 1 1 auto;
            color: #03103f;
            background-color: white;
            text-align: center;
            vertical-align: middle;
            border: 2px solid #1d2453;
            padding: 0.375rem 0.75rem; /* FIXME */
            border-radius: 0.25rem;
            -webkit-appearance: button;
            overflow: visible;
            margin: 0 2px 0 0; /* FIXME */
            box-sizing: border-box;
            align-items: flex-start;
            pointer-events: all;
            z-index: 100000;
            transition: all 300ms ease-in-out;
        }

        :host .inspector_toggle_label:hover {
            cursor: pointer;
        }

        :host #inspector_toggle:checked + .inspector_toggle_label {
            right: 380px;
            color: #fff;
            background-color: #03103f;
            border-color: #03103f;
        }

        :host #inspector_toggle:checked ~ #myViewer {
            --right: 360;
            right: 360px;
            width: calc(100% - (var(--left) + var(--right)));
        }

        :host #inspector_toggle:checked ~ #myInspector {
            right: 0;
            --right: 0;
        }

        :host #inspector_toggle:not(:checked) ~ #myInspector {
            right: -380px;
            --right: -380px;
        }

        :host #inspector_toggle:not(:checked) ~ #myViewer {
            --right: 0;
            right: 0;
            width: calc(100% - var(--left));
        }

        :host .xeokit-camera-pivot-marker {
            display: none !important;
        }

        :host .xeokit-marker {
            color: #ffffff;
            position: absolute;
            width: 15px;
            height: 15px;
            border-radius: 100%;
            border: 1px solid #ebebeb;
            background: #444444;
            box-shadow: 3px 3px 5px 1px #212529;
            z-index: 10000;
            pointer-events: none;
            display: none;
        }
    </style>
`;
class BimViewerWebComponent extends HTMLElement {
    static get observedAttributes() {
        return [
            "projectid",
            "modelid",
            "datadir",
            "tab",
            "configs",
            "openexplorer",
            "enableeditmodels"
        ];
    }

    constructor() {
        super();
        this.attachShadow({ mode: "open" });
        this.shadowRoot.innerHTML = innerHtml;
        this.bimViewer = null;
        this.projectId = "";
        this.modelId = "";
        this.dataDir = "/app/data";
        this.tab = "";
        this.configs = "";
        this.openExplorer = false;
        this.enableEditModels = false;
        this._debounceTimeout = null;
        this._pendingAttributes = new Map();

        const canvas = this.shadowRoot.getElementById("myCanvas");

        canvas.addEventListener("wheel", (e) => {
            e.preventDefault();
        });
    }

        /**
     * Register a custom HTMLElement.
     *
     * @param {String} name Element's name, defaults to 'xeokit-bim-viewer', must be lowercase and contain at least one hyphen.
    */
    static register(name = "xeokit-bim-viewer") {
        customElements.define(name, this);
    }

    dispatchEvent(event) {
        super.dispatchEvent(event);

        const attrName = 'on' + event.type;
        const handler = this.getAttribute(attrName);
        if (handler) {
            try {
                const func = new Function('event', handler);
                func.call(this, event);
            } catch (e) {
                console.error('Error executing inline event handler:', e);
            }
        }
    }

    connectedCallback() {

        const style = document.createElement('style');
        style.innerHTML = headStyleInnerHtml;
        document.getElementsByTagName('head')[0].appendChild(style);

        this.projectId = this.getAttribute("projectId");
        this.modelId = this.getAttribute("modelId");

        this.openExplorer = this.getAttribute("openExplorer");

        this.setExplorerOpen(this.openExplorer === "true");

        this.enableEditModels = this.getAttribute("enableEditModels") === "true";

        this.dataDir = this.getAttribute("dataDir") || "./app/data";

        const server = new Server({
            dataDir: this.dataDir
        });

        const bimViewer = new BIMViewer(server, {
            enableMeasurements: true,
            canvasElement: this.shadowRoot.getElementById("myCanvas"), // WebGL canvas
            keyboardEventsElement: this.shadowRoot, // Optional, defaults to this.shadowRoot
            explorerElement: this.shadowRoot.getElementById("myExplorer"), // Left panel
            toolbarElement: this.shadowRoot.getElementById("myToolbar"), // Toolbar
            inspectorElement: this.shadowRoot.getElementById("myInspector"), // Right panel
            navCubeCanvasElement: this.shadowRoot.getElementById("myNavCubeCanvas"),
            busyModelBackdropElement: this.shadowRoot.getElementById("myViewer"),
            enableEditModels: this.enableEditModels,
            containerElement: this.shadowRoot
        });

        bimViewer.viewer.cameraControl.on("picked", (pickResult) => {
            if (pickResult && pickResult.entity) {
                const objectId = pickResult.entity.id;
                const metaObject = bimViewer.viewer.metaScene.metaObjects[objectId];
                if (metaObject) {
                    this.dispatchEvent(new CustomEvent('objectSelected', {
                        detail: metaObject,
                        bubbles: true,
                        composed: true
                    }));
                }
            }
        });

        bimViewer.setConfigs({
            "showSpaces": false, // Default
            "selectedGlowThrough": true,
            "highlightGlowThrough": true,
            "dtxEnabled": true // Enable data texture scene representation for models - may be slow on low-spec GPUs
        });

        bimViewer.on("openExplorer", () => {
            this.setExplorerOpen(true);
        });

        bimViewer.on("openInspector", () => {
            this.setInspectorOpen(true);
        });

        bimViewer.on("addModel", (event) => { // "Add" selected in Models tab's context menu
            console.log("addModel: " + JSON.stringify(event, null, "\t"));
        });

        bimViewer.on("editModel", (event) => { // "Edit" selected in Models tab's context menu
            console.log("editModel: " + JSON.stringify(event, null, "\t"));
        });

        bimViewer.on("deleteModel", (event) => { // "Delete" selected in Models tab's context menu
            console.log("deleteModel: " + JSON.stringify(event, null, "\t"));
        });

        this.bimViewer = bimViewer;
        this.configs = this.getAttribute("configs");
        if (this.configs) {
            const configNameVals = this.configs.split(/,(?![^\[\]]*\])/);
            for (let i = 0, len = configNameVals.length; i < len; i++) {
                const configNameValStr = configNameVals[i];
                const configNameVal = configNameValStr.split(":");
                const configName = configNameVal[0];
                const configVal = configNameVal[1];
                bimViewer.setConfig(configName, configVal);
            }
        }

        if (!this.projectId) {
            console.log("No projectId attribute found");
            return;
        }

        bimViewer.loadProject(this.projectId, () => {
            this.modelId = this.getAttribute("modelId");
            if (this.modelId) {
                bimViewer.loadModel(this.modelId);
            }
            this.tab = this.getAttribute("tab");
            if (this.tab) {
                bimViewer.openTab(this.tab);
            }
        },
            (errorMsg) => {
                console.error(errorMsg);
            });
    }

    attributeChangedCallback(name, oldValue, newValue) {
        if (name === "projectid") {
            console.log("projectid changed: " + newValue);
            this.projectId = newValue;
            if (!this.bimViewer || !this.projectId) {
                return;
            }
            this.bimViewer.loadProject(this.projectId, () => {
                this.modelId = this.getAttribute("modelId");
                if (this.modelId) {
                    this.bimViewer.loadModel(this.modelId);
                }
                this.tab = this.getAttribute("tab");
                if (this.tab) {
                    this.bimViewer.openTab(this.tab);
                }
            },
            (errorMsg) => {
                console.error(errorMsg);
            });
        } else if (name === "modelid") {
            this.modelId = newValue;
            if (!this.bimViewer || !this.modelId || !this.projectId) {
                return;
            }
            this.bimViewer.loadProject(this.projectId, () => {
                this.bimViewer.loadModel(this.modelId);
                this.tab = this.getAttribute("tab");
                if (this.tab) {
                    this.bimViewer.openTab(this.tab);
                }
            },
            (errorMsg) => {
                console.error(errorMsg);
            });
        }
    }

    setExplorerOpen(explorerOpen) {
        const toggle = this.shadowRoot.getElementById("explorer_toggle");
        if (toggle) {
            toggle.checked = explorerOpen;
        }
    }

    setInspectorOpen(inspectorOpen) {
        const toggle = this.shadowRoot.getElementById("inspector_toggle");
        if (toggle) {
            toggle.checked = inspectorOpen;
        }
    }
}

export { BIMViewer, BimViewerWebComponent, LocaleService, Server };
