Prodigy Tree Viewer

View gameobject trees within Prodigy Game.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Prodigy Tree Viewer
// @namespace    https://hipposgrumm.dev/
// @version      1.2.0
// @description  View gameobject trees within Prodigy Game.
// @author       Hipposgrumm
// @license      gpl-3.0
// @match        *://*.prodigygame.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=prodigygame.com
// @grant        none
// @run-at       document-end
// ==/UserScript==

/*jshint esversion: 11 */
(async function() {
    let gameaccess;
    for (let timeout in [0, 100, 500, 1000, 5000]) {
        if (timeout > 0) await new Promise(resolve => setTimeout(resolve, timeout));
        try {
            gameaccess = window.Boot?.prototype.game; // Math
            if (!gameaccess) gameaccess = window.GameFramework?.FrameworkApp?.instance?.game; // English
            if (gameaccess) break;
        } catch (e) {}
    }
    if (!gameaccess) return; // Can't do anything since there's no game world.

    let css = `
<link rel="stylesheet" href="https://code.prodigygame.com/assets/Font-Awesome/css/font-awesome-bb53ad7bff.min.css">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,300,400,500,600,700" rel="stylesheet" type="text/css" async>
<style>
/* https://www.w3schools.com/howto/howto_js_treeview.asp */
.tree ul {
    font-size: 14px;
    list-style-type: none;
    padding-inline-start: 20px;
    user-select: none; /* Prevent text selection */
}
.treename {
    cursor: pointer;
    display: inline-block;
    min-width: 10em;
    padding: 3px;
    border-radius: 5px;
}
.treename.spinebone {
    color: #39E;
}
.treename:hover {
    background-color: #8884;
}
.treename:active {
    background-color: #8882;
}
.selected > .treename {
    background-color: #8888;
}
.caret {
    cursor: pointer;
}
.caret::before {
    content: "\u25B6";
    display: inline-block;
    margin-right: 6px;
}
.makeinvisible::before {
    color: transparent;
    cursor: auto;
}
.caret-down::before {
    transform: rotate(90deg);
}
</style>
<style>
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
::-webkit-scrollbar-thumb {
  background: #5F606A;
}
::-webkit-scrollbar-thumb:hover {
  background: #4F505A;
  cursor: pointer;
}
body {
    display: flex;
    font-family: "Open Sans";
    color: #DDD;
    background-color: #1A1A1E;
}
input,textarea,select,button {
    font-family: "Open Sans";
    color: #E5E5E5;
    background-color: #363638;
    border-color: #444;
    border-radius: 5px;
}
.hidden {
  display: none;
}
div.tree {
    height: calc(100vh - 40px);
    background-color: #242429;
    overflow: auto;
    white-space: nowrap;
    padding: 10px;
    border-radius: 10px;
}
div.splitter {
    width: 10px;
    cursor: col-resize;
}
div.dataview {
    height: calc(100vh - 40px);
    background-color: #202023;
    overflow: auto;
    white-space: nowrap;
    flex: 1;
    padding: 10px;
    border-radius: 10px;
}
div.dataitem {
    display: grid;
    grid-template-columns: minmax(150px, 210px) 1fr;
    padding: 2px 10px;
    height: 25px;
}
.dataitem span {
    vertical-align: bottom;
    user-select: none;
}
.dataitem > span {
    vertical-align: middle;
    width: 150px;
    min-width: 50px;
}
.dataview textarea {
    resize: vertical;
}
.dataitem > input,textarea,select {
    min-width: 0px;
}
.dataview div.sub {
    background-color: #242427;
    padding: 10px;
    border-radius: 10px;
}
button.degtoggle {
    background-color: #363638;
    border-color: #444;
    aspect-ratio: 1 / 1;
    height: 25px;
}
button.degtoggle.rad {
    background-image: url("data:image/svg+xml;base64,`+btoa(`<svg xmlns="http://www.w3.org/2000/svg" fill="#DDD" viewBox="0 0 500 500">
	<path d="M 71.235 164.76 L 55.596 164.695 C 55.581 164.696 68.346 113.835 89.175 88.947 C 106.336 68.108 131.312 61.924 149.8 61.7 L 444.154 61.724 L 444.2 114.6 L 354.67 114.631 C 354.669 120.296 343.483 203.929 339.545 264.172 C 336.436 311.736 334.283 367.164 380.158 371.445 C 418.882 373.364 427.109 335.185 427.683 322.619 L 442.8 322.6 C 439.3 365.492 419.567 433.28 357.153 432.362 C 308.314 429.455 293.292 398.763 287.689 363.431 C 278.473 329.546 306.818 121.136 304.62 114.733 L 206.6 114.7 C 207.422 115.05 193.43 261.761 187.564 293.505 C 183.956 324.61 174.05 370.806 161.744 395.17 C 145.189 429.984 121.953 435.333 105.175 428.222 C 77.642 414.754 81.181 388.386 90.763 374.381 C 96.317 367.136 123.771 328.393 131.818 309.928 C 142.793 288.981 151.542 249.678 153.494 236.773 C 159.835 216 167.064 115.489 166.297 115.387 L 130.092 115.44 C 111.64 115.885 97.442 126.385 88.196 139.247 C 87.33 138.9 71.158 164.683 71.2 164.7 L 71.235 164.76 Z" />
</svg>`)+`")
}
button.degtoggle.deg {
    background-image: url("data:image/svg+xml;base64,`+btoa(`<svg xmlns="http://www.w3.org/2000/svg" fill="#DDD" viewBox="0 0 500 500">
	<path d="M 159.139 277.589 C 210.158 294.834 247.443 340.673 243.385 399.989 L 288.465 398.242 C 291.281 358.175 261.569 271.201 184.684 242.627 L 159.139 277.589 Z" />
	<path d="M 250.213 80.675 L 60.809 408.222 L 60.829 419.278 L 439.19 419.325 L 439.1 377.9 L 127.222 377.894 L 290.819 106.63 L 250.213 80.675 Z" />
</svg>`)+`")
}
div.imageview {
    max-height: 200px;
    border-style: inset;
}
.imageview img {
    width: 100%;
    height: 100%;
    object-fit: contain;
}
.imageview p.errmsg {
    position: relative;
    text-align: center;
    margin: 0;
    display: none;
    color: #F00;
    /* https://www.w3schools.com/howto/howto_css_center-vertical.asp */
    top: 50%;
    left: 50%;
    -ms-transform: translate(-50%, -50%);
    transform: translate(-50%, -50%);
}
div.colorview {
    border-style: inset;
}
</style>
`;
    let EXPORT_MIMETYPES = {
        "html":  "text/html",
        "css":   "text/css",
        "js":    "application/javascript",
        "woff":  "application/font-woff",
        "woff2": "application/font-woff2",
        "png":   "image/png",
        "jpg":   "image/jpeg",
        "jpeg":  "image/jpeg",
        "gif":   "image/gif",
        "txt":   "text/plain",
        "json":  "application/json",
        "mp4":   "video/mp4",
        "ogg":   "application/ogg",
        "zip":   "application/zip"
    };

    let DEG2RAD = Math.PI / 180;
    let RAD2DEG = 180 / Math.PI;

    let colordetectorcanvas = document.createElement('canvas');
    colordetectorcanvas.width = 1;
    colordetectorcanvas.height = 1;
    function intFromColor(color) {
        if (typeof(color) == 'number') return color;
        let ctx = colordetectorcanvas.getContext('2d');
        ctx.fillStyle = color;
        ctx.fillRect(0,0,1,1);
        let pixel = ctx.getImageData(0,0,1,1).data;
        return (pixel[0] << 16) | (pixel[1] << 8) | pixel[2];
    }

    let PIXI_CLASSES = [];
    let PIXI_CLASSES_SPINE = [];
    for (let key in window.PIXI) {
        let val = window.PIXI[key];
        if (typeof(val) == 'function' && val.hasOwnProperty("prototype")) PIXI_CLASSES[key] = val;
    }
    for (let key in window.PIXI.spine) {
        let val = window.PIXI.spine[key];
        if (typeof(val) == 'function' && val.hasOwnProperty("prototype")) PIXI_CLASSES_SPINE[key] = val;
    }

    {
        let fun;
        fun = (e) => {
            window.EXTRACTED_CORS_POLICY = {};
            e.originalPolicy.split(new RegExp('; ?')).forEach(v => {
                if (!v) return;
                let vs = [];
                let name;
                v.split(' ').every((e,i) => {
                    if (i == 0) name = e;
                    else {
                        let val;
                        if (e.charAt(0) != "'") val = e;
                        else if (e == "'self'") {
                            val = document.location.origin;
                        } else if (e == "'none'") {
                            vs = [];
                            return false;
                        }
                        if (val) vs.push(new RegExp(val.replaceAll(':', '\\:').replaceAll('/', '\\/').replaceAll('*', '(.*)')));
                    }
                    return true;
                });
                window.EXTRACTED_CORS_POLICY[name] = vs;
            });
            document.removeEventListener("securitypolicyviolation", fun);
        };
        document.addEventListener("securitypolicyviolation", fun);
    }

    // PROBABLY EXPENSIVE, USE SPARINGLY
    async function checkCORSAllow(url, type) {
        if (!window.EXTRACTED_CORS_POLICY) {
            let img = new Image();
            img.crossOrigin = "anonymous";
            img.src = "//cors-test-please-ignore"; // triggers the function above

            // Source - https://stackoverflow.com/a/52652681
            // Posted by Lightbeard
            // Retrieved 2026-04-21, License - CC BY-SA 4.0
            await new Promise(resolve => {
                let loop;
                loop = setInterval(() => {
                    if (window.EXTRACTED_CORS_POLICY) {
                        clearInterval(loop);
                        resolve();
                    }
                }, 10);
            });
        }
        let origin = url.split(new RegExp('(?<![\:\/])\/'))[0]; // Doesn't match ':/' properly but we really don't care.
        return Boolean((window.EXTRACTED_CORS_POLICY[type] ?? []).find(e => origin.match(e)));
    }

    class TextureManaged {
        constructor(texture) {
            if (!texture.castToBaseTexture) { // checks if function exists
                console.log(texture);
                throw new Error("object is not a texture.");
            }
            this._id = -67;
            this.texture = texture;
        }

        hasChanged() {
            return this._id != this.texture.castToBaseTexture().dirtyId;
        }

        getDataUrl() {
            if (this.hasChanged()) this._url = renderPIXITexture(this);
            return this._url;
        }
    }
    let PIXITextureShader = new window.PIXI.Filter(null, `
// nocache: ${Date.now() /* Cached shaders sometimes simply refuse to work at all. */}
precision mediump float;

varying vec2 vTextureCoord;

uniform sampler2D uSampler;
uniform bool doMatte;

void main(void) {
    vec4 fg = texture2D(uSampler, vTextureCoord);
    if (doMatte) fg.r = fg.a;
    fg.a = 1.0;
    gl_FragColor = fg;
}
`, {doMatte: false});
    let PIXIBufferApp = new window.PIXI.Application({ backgroundAlpha: 0 });
    function renderPIXITexture(textures) {
        if (!gameaccess) throw new Error("Attempted to render PIXI texture before viewer window was created.");
        let single = !Array.isArray(textures);
        if (single) textures = [textures];
        let outs = [];
        let outcanvas = document.createElement('canvas');
        let outctx = outcanvas.getContext('2d');
        let gl = gameaccess.renderer.gl;
        textures.forEach(mantex => {
            if (!(mantex instanceof TextureManaged)) {
                console.log("Skipping texture that isn't managed.");
                return;
            }
            let tex = mantex.texture;
            outctx.clearRect(0, 0, outcanvas.width, outcanvas.height);
            outcanvas.width = tex.width;
            outcanvas.height = tex.height;
            let sprite = new window.PIXI.Sprite(tex);
            let texsrc = tex.castToBaseTexture().resource;
            if (texsrc && (texsrc.bitmap || texsrc.source)) {
                PIXIBufferApp.renderer.resize(tex.width, tex.height);
                PIXIBufferApp.renderer.render(sprite);
                outctx.drawImage(PIXIBufferApp.renderer.view, 0, 0);
            } else {
                sprite.filters = [PIXITextureShader];
                for (;sprite.x>-tex.width;sprite.x-=gameaccess.width) {
                    for (;sprite.y>-tex.height;sprite.y-=gameaccess.height) {
                        let width = Math.min(tex.width+sprite.x, gameaccess.width);
                        let height = Math.min(tex.height+sprite.y, gameaccess.height);
                        let vOff = gameaccess.height - height;
                        let pixels = new Uint8Array(width * height * 4);
                        PIXITextureShader.uniforms.doMatte = false;
                        gameaccess.renderer.render(sprite);
                        gl.readPixels(0, vOff, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
                        {
                            let red = new Uint8Array(pixels.length);
                            PIXITextureShader.uniforms.doMatte = true;
                            gameaccess.renderer.render(sprite);
                            gl.readPixels(0, vOff, width, height, gl.RGBA, gl.UNSIGNED_BYTE, red);
                            for (let i=0;i<pixels.length;i+=4) {
                                pixels[i+3] = red[i];
                            }
                        }
                        pixels = new Uint8ClampedArray(pixels, width, height);
                        let wid = width*4;
                        for (let r=0;r<(((pixels.length/wid)/2)*wid);r+=wid) {
                            let ir = pixels.length-r-wid;
                            for (let i=0;i<wid;i++) {
                                let iri=ir+i, ri=r+i;
                                let t = pixels[iri];
                                pixels[iri] = pixels[ri];
                                pixels[ri] = t;
                            }
                        }
                        outctx.putImageData(new ImageData(pixels, width, height), -sprite.x, -sprite.y);
                    }
                }
            }
            outs.push(outcanvas.toDataURL());
            mantex._id = tex.castToBaseTexture().dirtyId;
        });
        if (single) return outs[0];
        else return outs;
    }

    /// An abstract container that can be used to modify objects that ignores the descrepancies between GameObject types
    class ManipulatableObject {
        /// Creates inner classes for this class
        static createInnerClasses() {
            /// Contains all data for this object's management in the DOM.
            ManipulatableObject.ElementData = class {
                constructor(obj, list, parent) {
                    this._obj = obj;
                    this._list = list;
                    this._item = document.createElement('li');

                    this._caret = document.createElement('span');
                    this._caret.classList.add("caret");
                    this._caret.addEventListener("click", () => this.doToggle());
                    this._caret.classList.add("makeinvisible");
                    this._caretHidden = true;
                    this._item.appendChild(this._caret);

                    if (obj.active != null) {
                        this._checkbox = document.createElement('input');
                        this._checkbox.type = "checkbox";
                        this._checkbox.addEventListener("change", () => {
                            obj.active = this._checkbox.checked;
                        });
                        this._checkbox.checked = obj.active;
                        this._item.appendChild(this._checkbox);
                    }

                    this._nametext = " ";
                    this._txt = document.createElement('span');
                    this._txt.classList.add("treename");
                    if (obj.skeleton || parent?.skeleton) {
                        this._txt.classList.add("spinebone");
                    }
                    this._txt.addEventListener("click", () => openObject(obj));
                    this._item.appendChild(this._txt);

                    this._childList = document.createElement('ul');
                    this._childList.classList.add("hidden");
                    this._item.appendChild(this._childList);
                    this._hideChildren = true;
                }

                /// Expands/collapses this item's child list.
                doToggle() {
                    if (!this._childList) return;
                    this._hideChildren = !this._hideChildren;
                    if (this._hideChildren) {
                        this._childList.classList.add("hidden");
                        this._caret.classList.remove("caret-down");
                        this.updateChildren();
                    } else {
                        this._childList.classList.remove("hidden");
                        this._caret.classList.add("caret-down");
                    }
                }

                updateChildren(addition=null) {
                    if (this._caretHidden) {
                        if (this._obj.node.children.length > 0) {
                            this._caret.classList.remove("makeinvisible");
                            this._caretHidden = false;
                            return;
                        }
                    } else if (this._obj.node.children.length == 0) {
                        this._caret.classList.add("makeinvisible");
                        this._caretHidden = true;
                    }
                    if (!this.isChildrenVisible()) return; // if the children are hidden we don't need to worry about them right now
                    this._obj.node.sortChildren(addition);
                    if (!this._obj.node.sortingDirtyAccept()) return;
                    // https://stackoverflow.com/q/34685316
                    var elements = document.createDocumentFragment();
                    this._obj.node.children.forEach(child => {
                        elements.appendChild(child.object.element._item);
                    });
                    this._childList.innerHTML = null;
                    this._childList.appendChild(elements);
                }

                /// @returns {bool} Whether this item's list of children is collapsed or not
                get childrenHidden() { return this._hideChildren; }

                /// @returns {Object} List Element containing this object
                get list() { return this._list; }

                /// @returns {Object} ListItem Element representing this object
                get item() { return this._item; }

                /// @returns {Object} List Element for this object's children
                get childList() { return this._childList; }

                /// @returns {Object} Checkbox that determines if the GameObject is visible/active
                get checkbox() { return this._checkbox; }

                get txt() { return this._txt; }
                get txt_inner() { return this._nametext; }
                set txt_inner(val) {
                    this._nametext = val;
                    if (!val?.trim()) this._txt.innerHTML = "\u00A0"; // This prevents it from becoming flat.
                    else this._txt.innerHTML = val;
                }

                isChildrenVisible() {
                    let par = this._obj.node;
                    while (par) {
                        if (par.object.element._hideChildren) return false;
                        par = par.parent;
                    }
                    return true;
                }
            };
            delete ManipulatableObject.createInnerClasses;
        }

        constructor(data, list, parent) {
            if (!data) throw new Error("Value passed to 'data' parameter of ManipulatableObject.new() is null or empty.");
            this._inTree = true;
            if (data instanceof ManipulatableObject) {
                this._treeobject = data._treeobject;
                this._pixiobject = data._pixiobject;
                this._data = data._data;
                this._typename = data._typename;
                this._pixiTransform = data._pixiTransform;
            } else {
                if (data.hasOwnProperty("_legacyTransform")) { // PIXI GameObject
                    this._treeobject = data.legacyTransform; // potential reference to Tree GameObject
                    this._pixiobject = data;
                } else if (data.transform) { // Tree GameObject
                    this._treeobject = data;
                    this._pixiobject = data.gameObjectRef; // potential reference to PIXI GameObject
                } else {
                    throw new Error("Object passed to 'data' parameter of ManipulatableObject.new() doesn't appear to be a GameObject.");
                }
                this._data = data; // Useful for referencing names common to both GameObject types
                this._typename = null;
                for (let name in PIXI_CLASSES) {
                    let clazz = PIXI_CLASSES[name];
                    if (clazz == data.constructor) {
                        this._typename = "pixi/"+name;
                        break;
                    }
                }
                for (let name in PIXI_CLASSES_SPINE) {
                    let clazz = PIXI_CLASSES_SPINE[name];
                    if (clazz == data.constructor) {
                        this._typename = "pixi/spine/"+name;
                        break;
                    }
                }
                this._pixiTransform = this._treeobject ? this._treeobject.transform : this._pixiobject.transform?.pixiTransform;
            }
            this._element = new ManipulatableObject.ElementData(this, list, parent);
        }

        compare(other) {
            return this._treeobject == other._treeobject &&
                this._pixiobject == other._pixiobject;
        }

        get inTree() { return this._inTree; }
        get treeObject() { return this._treeobject; }
        get pixiObject() { return this._pixiobject; }
        get typename() { return this._typename; }
        get name() {
            if (this._data.name != null) return this._data.name;
            if (!this._pixiobject && !this._data.hasOwnProperty("name")) return null;
            else return "";
        }
        get active() {
            if (this._pixiobject) return this._pixiobject.active;
            else if (this._treeobject?.hasOwnProperty("visible")) {
                return this._treeobject.visible;
            } else return null;
        }
        set active(val) {
            if (this._pixiobject) this._pixiobject.active = val;
            else if (this._treeobject?.hasOwnProperty("visible")) {
                this._treeobject.visible = val;
            }
        }
        get pos() { return this._pixiTransform.position; }
        get rot() { // Rotation in radians.
            return this._pixiTransform ? this._pixiTransform.rotation : null;
        }
        set rot(val) {
            if (this._pixiTransform) this._pixiTransform.rotation = val;
        }
        get scl() { return this._pixiTransform.scale; }
        get skw() { return this._pixiTransform.skew; } // Skew in radians.
        get piv() { return this._pixiTransform.pivot; }
        get transLoc() { return this._pixiTransform.localTransform; }
        get transWld() { return this._pixiTransform.worldTransform; }
        get alpha() {
            if (!this._treeobject) return null;
            return this._treeobject.alpha;
        }
        set alpha(val) {
            if (this._treeobject) this._treeobject.alpha = val;
        }

        get text() {
            if (this._data.text != null) return this._data.text;
            if (!this._data.hasOwnProperty("_text")) return null;
            return this._data.text ?? "";
        }
        get textStyle() {
            return this._data.style;
        }
        textStylePrepare() {
            if (this._textStyleModified) return;
            this._data.style = this._data.style.clone();
            this._textStyleModified = true;
        }
        get texture() {
            if (this._texture?.texture != this._data.texture) {
                this._texture = this._data.texture ? new TextureManaged(this._data.texture) : null;
            }
            return this._texture;
        }
        set texture(val) {
            if (!(val instanceof TextureManaged)) val = new TextureManaged(val);
            this._texture = val;
            this._data.texture = val.texture;
        }
        get skeleton() {
            return this._data.skeleton;
        }

        get data() { return this._data; }
        get element() { return this._element; }
        get node() { return this._node; }
    } ManipulatableObject.createInnerClasses();
    class ObjectHeirarchy {
        static createInnerClasses() {
            ObjectHeirarchy.Node = class {
                constructor(obj, heirarchy, parent) {
                    obj._node = this;
                    this._obj = obj;
                    this._heirarchy = heirarchy;
                    this._parent = parent;
                    this._siblingUp = null;
                    this._siblingDown = null;
                    this._children = [];
                    this._sortingDirty = true;
                    this._listTreeObjsLast = [];
                    this._listPixiObjsLast = [];
                }

                get object() { return this._obj; }
                get parent() { return this._parent; }
                get siblingUp() { return this._siblingUp; }
                get siblingDown() { return this._siblingDown; }
                get children() { return this._children; }
                sortingDirtyAccept() {
                    if (!this._sortingDirty) return false;
                    this._sortingDirty = false;
                    return true;
                }

                sortChildren(addition=null) {
                    let listTreeObjs = this._obj.treeObject?.children ?? [];
                    let listPixiObjs = this._obj.pixiObject?.children ?? [];
                    if (addition) {
                        if (addition._obj.treeObject) this._listTreeObjsLast.push(addition._obj.treeObject);
                        if (addition._obj.pixiObject) this._listPixiObjsLast.push(addition._obj.pixiObject);
                        this._obj.element.childList.appendChild(addition._obj.element.item);
                    }
                    if (
                        this._listTreeObjsLast.length == listTreeObjs.length &&
                        this._listTreeObjsLast.every((e,i) => e == listTreeObjs[i]) &&
                        this._listPixiObjsLast.length == listPixiObjs.length &&
                        this._listPixiObjsLast.every((e,i) => e == listPixiObjs[i])
                    ) return;
                    this._listTreeObjsLast = Array.from(listTreeObjs);
                    this._listPixiObjsLast = Array.from(listPixiObjs);

                    let items = [], extras = [];
                    this._children.forEach(e => {
                        let added = false;
                        e._tmp_treeInd = e._obj.treeObject ? listTreeObjs.indexOf(e._obj.treeObject) : -1;
                        if (e._tmp_treeInd >= 0) {
                            items.push(e);
                            added = true;
                        }
                        e._tmp_pixiInd = e._obj.pixiObject ? listPixiObjs.indexOf(e._obj.pixiObject) : -1;
                        if (!added) {
                            if (e._tmp_pixiInd >= 0) {
                                items.push(e);
                            } else extras.push(e);
                        }
                    });

                    items.sort((a, b) => {
                        let ind1 = a._tmp_pixiInd;
                        let ind2 = b._tmp_pixiInd;
                        if (ind1 < 0 || ind2 < 0) return 0;
                        else return ind1-ind2;
                    });
                    items.sort((a, b) => {
                        let ind1 = a._tmp_treeInd;
                        let ind2 = b._tmp_treeInd;
                        if (ind1 < 0 || ind2 < 0) return 0;
                        else return ind1-ind2;
                    });
                    let newChildren = items.concat(extras);
                    newChildren.forEach((e, i) => {
                        e._siblingUp = (i > 0) ? newChildren[i-1] : null;
                        e._siblingDown = (i < (newChildren.length-1)) ? newChildren[i+1] : null;
                        delete e._tmp_treeInd; delete e._tmp_pixiInd;
                    });
                    let sorted = this._children.length != newChildren.length ||
                        !this._children.every((e,i) => e == newChildren[i]);

                    this._children = newChildren;
                    if (sorted) this._sortingDirty = true;
                }

                add(obj) {
                    let node = new ObjectHeirarchy.Node(obj, this._heirarchy, this);
                    if (obj.treeObject) {
                        this._heirarchy._treeObjectsKeys.push(obj.treeObject);
                        this._heirarchy._treeObjectsVals.push(node);
                    }
                    if (obj.pixiObject) {
                        this._heirarchy._pixiObjectsKeys.push(obj.pixiObject);
                        this._heirarchy._pixiObjectsVals.push(node);
                    }
                    this._heirarchy._allnodes.push(node);
                    if (this._children.length > 0) {
                        let lastChild = this._children[this._children.length-1];
                        lastChild._siblingDown = node;
                        node._siblingUp = lastChild;
                    }
                    this._children.push(node);
                    this._obj.element.updateChildren(node);
                }

                removeSelf(fromRecursion=false) {
                    this._children.forEach(c=>c.removeSelf(true));
                    let ind = this._heirarchy._allnodes.indexOf(this);
                    if (ind < 0) return;

                    if (this._siblingUp) this._siblingUp._siblingDown = this._siblingDown;
                    if (this._siblingDown) this._siblingDown._siblingUp = this._siblingUp;

                    this._heirarchy._allnodes.splice(ind, 1);
                    ind = this._heirarchy._treeObjectsVals.indexOf(this);
                    if (ind >= 0) {
                        this._heirarchy._treeObjectsKeys.splice(ind, 1);
                        this._heirarchy._treeObjectsVals.splice(ind, 1);
                    }
                    ind = this._heirarchy._pixiObjectsVals.indexOf(this);
                    if (ind >= 0) {
                        this._heirarchy._pixiObjectsKeys.splice(ind, 1);
                        this._heirarchy._pixiObjectsVals.splice(ind, 1);
                    }
                    this._obj._inTree = false;
                    if (!fromRecursion) {
                        this._obj.element.item.remove();
                        this._parent._children.splice(this._parent._children.indexOf(this), 1);
                    }
                }
            };
            delete ObjectHeirarchy.createInnerClasses;
        }

        constructor(obj) {
            this.add(obj, null);
        }

        add(obj, parent) {
            if (parent != null && !(parent instanceof ObjectHeirarchy.Node)) {
                if (!(parent instanceof ManipulatableObject)) throw new Error("Unusable 'parent' for adding object to heirarchy.");
                if (parent.treeObject) parent = this._treeObjectsVals[this._treeObjectsKeys.indexOf(parent.treeObject)];
                else if (parent.pixiObject) parent = this._pixiObjectsVals[this._pixiObjectsKeys.indexOf(parent.pixiObject)];
                else throw new Error("This ManipulatableObject is incapable of being a 'parent'.");
            }
            if (parent) parent.add(obj);
            else {
                this._root = new ObjectHeirarchy.Node(obj, this, null);
                if (obj.treeObject) {
                    this._treeObjectsKeys = [obj.treeObject];
                    this._treeObjectsVals = [this._root];
                } else {
                    this._treeObjectsKeys = [];
                    this._treeObjectsVals = [];
                }
                if (obj.pixiObject) {
                    this._pixiObjectsKeys = [obj.pixiObject];
                    this._pixiObjectsVals = [this._root];
                } else {
                    this._pixiObjectsKeys = [];
                    this._pixiObjectsVals = [];
                }
                this._allnodes = [this._root];
                obj.element.list.appendChild(obj.element.item);
            }
        }

        remove(obj) {
            if (!(obj instanceof ObjectHeirarchy.Node)) {
                if (obj instanceof ManipulatableObject) {
                    obj = this._allnodes.find(o => o.object == obj);
                } else throw new Error("Value of 'obj' is not a node, cannot remove it.");
            }
            obj.removeSelf();
        }
    } ObjectHeirarchy.createInnerClasses();

    let createDataitem = function(nameHTML, elementType, elementSetup, elementLayout="auto") {
        let div = document.createElement('div');
        let label = document.createElement('span');
        let vals = document.createElement('div');
        div.classList.add("dataitem");
        div.appendChild(label);
        div.appendChild(vals);
        label.innerHTML = nameHTML;
        vals.style.display = "inline-grid";
        vals.style["grid-template-columns"] = elementLayout;
        if (!Array.isArray(elementType)) {
            let val = document.createElement(elementType);
            if (!Array.isArray(elementSetup)) {
                if (elementSetup) elementSetup(val, [val]);
            } else if (elementSetup.length > 0) {
                if (elementSetup[0]) elementSetup[0](val, [val]);
            }
            vals.appendChild(val);
            return div;
        } else if (elementType.length == 0) return div;
        let elements = [];
        elementType.forEach(e => {
            let val = document.createElement(e);
            elements.push(val);
            vals.appendChild(val);
        });
        if (!Array.isArray(elementSetup)) {
            if (elementSetup) elementSetup(elements[0], Array.from(elements));
            return div;
        }
        elements.every((e, i) => {
            if (i >= elementSetup.length) return false;
            if (elementSetup[i]) elementSetup[i](e, Array.from(elements));
            return true;
        });
        return div;
    };
    let dataItemDegRad = function(dataviewData, modename, onSetDegrees, onSetRadians) {
        let modenameLoc = dataviewData;
        while (true) {
            let dotpos = modename.indexOf('.');
            if (dotpos < 0) break;
            modenameLoc = modenameLoc[modename.slice(0, dotpos)];
            modename = modename.substring(dotpos+1);
        }
        return (radDeg, elements) => {
            radDeg.classList.add("degtoggle");
            radDeg.addEventListener("click", () => {
                var mode = !modenameLoc[modename];
                modenameLoc[modename] = mode;
                if (mode) {
                    radDeg.title = "Mode: Degrees";
                    radDeg.classList.remove("rad");
                    radDeg.classList.add("deg");
                    onSetDegrees(dataviewData, elements);
                } else {
                    radDeg.title = "Mode: Radians";
                    radDeg.classList.add("rad");
                    radDeg.classList.remove("deg");
                    onSetRadians(dataviewData, elements);
                }
            });
            radDeg.title = "Mode: Degrees";
            radDeg.classList.add("deg");
            modenameLoc[modename] = true;
        };
    };

    let treeDiv, dataview;
    let treeData = null, dataviewData = null, dataviewUpdaters = null;

    let selected = null;
    function openObject(obj) {
        if (obj instanceof ObjectHeirarchy.Node) obj = obj.object;

        if (selected) selected.element.item.classList.remove("selected");
        selected = obj;
        while (dataview.firstChild) dataview.firstChild.remove();
        dataviewData = {"extra":{},"current":{"extra":{}}};
        dataviewUpdaters = [];
        if (!selected) return;
        selected.element.item.classList.add("selected");

        {
            let par = obj.node.parent;
            while (par) {
                if (par.object.element.childrenHidden) par.object.element.doToggle();
                par = par.parent;
            }
        }

        {
            let treeRect = treeDiv.getBoundingClientRect();
            let itemRect = selected.element.txt.getBoundingClientRect();
            let scrollX = treeDiv.scrollLeft, scrollY = treeDiv.scrollTop;
            if (itemRect.left < treeRect.left) scrollX -= treeRect.left-itemRect.left;
            else if (itemRect.right > treeRect.right) scrollX += itemRect.right-treeRect.right;
            if (itemRect.top < treeRect.top) scrollY -= treeRect.top-itemRect.top;
            else if (itemRect.bottom > treeRect.bottom) scrollY += itemRect.bottom-treeRect.bottom;
            treeDiv.scroll(scrollX, scrollY);
        }

        dataview.appendChild(createDataitem("Class ID", 'input', text => {
            text.type = "text";
            text.readOnly = true;
            let clazz = Object.getPrototypeOf(obj.data);
            let path = clazz.constructor.name;
            while (true) {
                let par = Object.getPrototypeOf(clazz);
                if (!par) break;
                path = par.constructor.name+' '+path;
                clazz = par;
            }
            text.value = path;
        }));
        if (obj.name != null) {
            dataview.appendChild(createDataitem("Name", 'input', text => {
                text.type = "text";
                text.value = obj.name;
                text.readOnly = true;
            }));
        }
        if (obj.active != null) {
            dataview.appendChild(createDataitem("Is Active", 'input', check => {
                check.type = "checkbox";
                check.addEventListener("change", () => {
                    obj.element.checkbox.checked = obj.active = check.checked;
                });
                check.checked = obj.active;
                dataviewData.active = check;
            }));
            dataviewUpdaters.push(() => {
                dataviewData.active.checked = selected.active;
            });
        }
        if (obj._pixiTransform) {
            let transformDiv = document.createElement('div');
            transformDiv.classList.add("sub");
            transformDiv.appendChild(createDataitem("Position", ['span', 'input', 'span', 'input'], [
                label => { label.innerHTML = "X:"; },
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        dataviewData.current.posX = obj.pos.x = input.valueAsNumber;
                    });
                    input.value = dataviewData.current.posX = obj.pos.x;
                    dataviewData.posX = input;
                },
                label => { label.innerHTML = ", Y:"; },
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        dataviewData.current.posY = obj.pos.y = input.valueAsNumber;
                    });
                    input.value = dataviewData.current.posY = obj.pos.y;
                    dataviewData.posY = input;
                }
            ], "auto minmax(0, 1fr) auto minmax(0, 1fr)"));
            dataviewUpdaters.push(() => {
                if (dataviewData.posX.value == dataviewData.current.posX) {
                    dataviewData.posX.value = dataviewData.current.posX = selected.pos.x;
                }
                if (dataviewData.posY.value == dataviewData.current.posY) {
                    dataviewData.posY.value = dataviewData.current.posY = selected.pos.y;
                }
            });
            transformDiv.appendChild(createDataitem("Rotation", ['input', 'button'], [
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        let val = input.valueAsNumber;
                        if (dataviewData.rot_degmode) val *= DEG2RAD;
                        dataviewData.current.rot = obj.rot = val;
                    });
                    input.value = dataviewData.current.rot = obj.rot * RAD2DEG;
                    dataviewData.rot = input;
                }, dataItemDegRad(dataviewData, "rot_degmode", (dataviewData, elements) => {
                    elements[0].value = dataviewData.current.rot = obj.rot * RAD2DEG;
                    dataviewData.rot.step = 1;
                }, (dataviewData, elements) => {
                    elements[0].value = dataviewData.current.rot = obj.rot;
                    dataviewData.rot.step = DEG2RAD;
                })
            ], "minmax(0, 1fr) auto"));
            dataviewUpdaters.push(() => {
                if (dataviewData.rot.value != dataviewData.current.rot) return;
                let val = selected.rot;
                if (dataviewData.rot_degmode) val *= RAD2DEG;
                dataviewData.rot.value = dataviewData.current.rot = val;
            });
            transformDiv.appendChild(createDataitem("Scale", ['span', 'input', 'span', 'input'], [
                label => { label.innerHTML = "X:"; },
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        dataviewData.current.sclX = obj.scl.x = input.valueAsNumber;
                    });
                    input.value = dataviewData.current.sclX = obj.scl.x;
                    dataviewData.sclX = input;
                },
                label => { label.innerHTML = ", Y:"; },
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        dataviewData.current.sclY = obj.scl.y = input.valueAsNumber;
                    });
                    input.value = dataviewData.current.sclY = obj.scl.y;
                    dataviewData.sclY = input;
                }
            ], "auto minmax(0, 1fr) auto minmax(0, 1fr)"));
            dataviewUpdaters.push(() => {
                if (dataviewData.sclX.value == dataviewData.current.sclX) {
                    dataviewData.sclX.value = dataviewData.current.sclX = selected.scl.x;
                }
                if (dataviewData.sclY.value == dataviewData.current.sclY) {
                    dataviewData.sclY.value = dataviewData.current.sclY = selected.scl.y;
                }
            });
            transformDiv.appendChild(createDataitem("Skew", ['span', 'input', 'span', 'input', 'button'], [
                label => { label.innerHTML = "X:"; },
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        let val = input.valueAsNumber;
                        if (dataviewData.skw_degmode) val *= DEG2RAD;
                        dataviewData.current.skwX = obj.skw.x = val;
                    });
                    input.step = 1;
                    input.value = dataviewData.current.skwX = obj.skw.x * RAD2DEG;
                    dataviewData.skwX = input;
                },
                label => { label.innerHTML = ", Y:"; },
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        let val = input.valueAsNumber;
                        if (dataviewData.skw_degmode) val *= DEG2RAD;
                        dataviewData.current.skwY = obj.skw.y = val;
                    });
                    input.step = 1;
                    input.value = dataviewData.current.skwY = obj.skw.y * RAD2DEG;
                    dataviewData.skwY = input;
                }, dataItemDegRad(dataviewData, "skw_degmode", (dataviewData, elements) => {
                    elements[1].value = dataviewData.current.skwX = obj.skw.x * RAD2DEG;
                    elements[3].value = dataviewData.current.skwY = obj.skw.y * RAD2DEG;
                    dataviewData.skwX.step = dataviewData.skwY.step = 1;
                }, (dataviewData, elements) => {
                    elements[1].value = dataviewData.current.skwX = obj.skw.x;
                    elements[3].value = dataviewData.current.skwY = obj.skw.y;
                    dataviewData.skwX.step = dataviewData.skwY.step = DEG2RAD;
                })
            ], "auto minmax(0, 1fr) auto minmax(0, 1fr) auto"));
            dataviewUpdaters.push(() => {
                if (dataviewData.skwX.value == dataviewData.current.skwX) {
                    let val = selected.skw.x;
                    if (dataviewData.skw_degmode) val *= RAD2DEG;
                    dataviewData.skwX.value = dataviewData.current.skwX = val;
                }
                if (dataviewData.skwY.value == dataviewData.current.skwY) {
                    let val = selected.skw.y;
                    if (dataviewData.skw_degmode) val *= RAD2DEG;
                    dataviewData.skwY.value = dataviewData.current.skwY = val;
                }
            });
            transformDiv.appendChild(createDataitem("Pivot", ['span', 'input', 'span', 'input'], [
                label => { label.innerHTML = "X:"; },
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        dataviewData.current.pivX = obj.piv.x = input.valueAsNumber;
                    });
                    input.value = dataviewData.current.pivX = obj.piv.x;
                    dataviewData.pivX = input;
                },
                label => { label.innerHTML = ", Y:"; },
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        dataviewData.current.pivY = obj.piv.y = input.valueAsNumber;
                    });
                    input.value = dataviewData.current.pivY = obj.piv.y;
                    dataviewData.pivY = input;
                }
            ], "auto minmax(0, 1fr) auto minmax(0, 1fr)"));
            dataviewUpdaters.push(() => {
                if (dataviewData.pivX.value == dataviewData.current.pivX) {
                    dataviewData.pivX.value = dataviewData.current.pivX = selected.piv.x;
                }
                if (dataviewData.pivY.value == dataviewData.current.pivY) {
                    dataviewData.pivY.value = dataviewData.current.pivY = selected.piv.y;
                }
            });
            if (obj.text != null) {
                let transLoc = createDataitem("Local Transform", ['span', 'input', 'span', 'input', 'span', 'input', 'span', 'input', 'span', 'input', 'span', 'input'], [
                    label => { label.innerHTML = "PosX:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            dataviewData.current.transLocTX = obj.transLoc.tx = input.valueAsNumber;
                            obj.data.dirty = true;
                        });
                        input.value = dataviewData.current.transLocTX = obj.transLoc.tx;
                        dataviewData.transLocTX = input;
                    },
                    label => { label.innerHTML = ", PosY:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            dataviewData.current.transLocTY = obj.transLoc.ty = input.valueAsNumber;
                            obj.data.dirty = true;
                        });
                        input.value = dataviewData.current.transLocTY = obj.transLoc.ty;
                        dataviewData.transLocTY = input;
                    },
                    label => { label.innerHTML = "SclX:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            dataviewData.current.transLocA = obj.transLoc.a = input.valueAsNumber;
                            obj.data.dirty = true;
                        });
                        input.value = dataviewData.current.transLocA = obj.transLoc.a;
                        dataviewData.transLocA = input;
                    },
                    label => { label.innerHTML = ", SclY:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            dataviewData.current.transLocD = obj.transLoc.d = input.valueAsNumber;
                            obj.data.dirty = true;
                        });
                        input.value = dataviewData.current.transLocD = obj.transLoc.d;
                        dataviewData.transLocD = input;
                    },
                    label => { label.innerHTML = "SkwX:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            dataviewData.current.transLocC = obj.transLoc.c = input.valueAsNumber;
                            obj.data.dirty = true;
                        });
                        input.value = dataviewData.current.transLocC = obj.transLoc.c;
                        dataviewData.transLocC = input;
                    },
                    label => { label.innerHTML = ", SkwY:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            dataviewData.current.transLocB = obj.transLoc.b = input.valueAsNumber;
                            obj.data.dirty = true;
                        });
                        input.value = dataviewData.current.transLocB = obj.transLoc.b;
                        dataviewData.transLocB = input;
                    }
                ], "auto minmax(0, 1fr) auto minmax(0, 1fr)");
                transLoc.style.height = "75px";
                transformDiv.appendChild(transLoc);
                dataviewUpdaters.push(() => {
                    if (dataviewData.transLocA.value == dataviewData.current.transLocA) {
                        dataviewData.transLocA.value = dataviewData.current.transLocA = selected.transLoc.a;
                    }
                    if (dataviewData.transLocB.value == dataviewData.current.transLocB) {
                        dataviewData.transLocB.value = dataviewData.current.transLocB = selected.transLoc.b;
                    }
                    if (dataviewData.transLocC.value == dataviewData.current.transLocC) {
                        dataviewData.transLocC.value = dataviewData.current.transLocC = selected.transLoc.c;
                    }
                    if (dataviewData.transLocD.value == dataviewData.current.transLocD) {
                        dataviewData.transLocD.value = dataviewData.current.transLocD = selected.transLoc.d;
                    }
                    if (dataviewData.transLocTX.value == dataviewData.current.transLocTX) {
                        dataviewData.transLocTX.value = dataviewData.current.transLocTX = selected.transLoc.tx;
                    }
                    if (dataviewData.transLocTY.value == dataviewData.current.transLocTY) {
                        dataviewData.transLocTY.value = dataviewData.current.transLocTY = selected.transLoc.ty;
                    }
                });
                let transWld = createDataitem("World Transform", ['span', 'input', 'span', 'input', 'span', 'input', 'span', 'input', 'span', 'input', 'span', 'input'], [
                    label => { label.innerHTML = "PosX:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            dataviewData.current.transWldTX = obj.transWld.tx = input.valueAsNumber;
                            obj.data.dirty = true;
                        });
                        input.value = dataviewData.current.transWldTX = obj.transWld.tx;
                        dataviewData.transWldTX = input;
                    },
                    label => { label.innerHTML = ", PosY:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            dataviewData.current.transWldTY = obj.transWld.ty = input.valueAsNumber;
                            obj.data.dirty = true;
                        });
                        input.value = dataviewData.current.transWldTY = obj.transWld.ty;
                        dataviewData.transWldTY = input;
                    },
                    label => { label.innerHTML = "SclX:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            dataviewData.current.transWldA = obj.transWld.a = input.valueAsNumber;
                            obj.data.dirty = true;
                        });
                        input.value = dataviewData.current.transWldA = obj.transWld.a;
                        dataviewData.transWldA = input;
                    },
                    label => { label.innerHTML = ", SclY:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            dataviewData.current.transWldD = obj.transWld.d = input.valueAsNumber;
                            obj.data.dirty = true;
                        });
                        input.value = dataviewData.current.transWldD = obj.transWld.d;
                        dataviewData.transWldD = input;
                    },
                    label => { label.innerHTML = "SkwX:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            dataviewData.current.transWldC = obj.transWld.c = input.valueAsNumber;
                            obj.data.dirty = true;
                        });
                        input.value = dataviewData.current.transWldC = obj.transWld.c;
                        dataviewData.transWldC = input;
                    },
                    label => { label.innerHTML = ", SkwY:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            dataviewData.current.transWldB = obj.transWld.b = input.valueAsNumber;
                            obj.data.dirty = true;
                        });
                        input.value = dataviewData.current.transWldB = obj.transWld.b;
                        dataviewData.transWldB = input;
                    }
                ], "auto minmax(0, 1fr) auto minmax(0, 1fr)");
                transWld.style.height = "75px";
                transformDiv.appendChild(transWld);
                dataviewUpdaters.push(() => {
                    if (dataviewData.transWldA.value == dataviewData.current.transWldA) {
                        dataviewData.transWldA.value = dataviewData.current.transWldA = selected.transWld.a;
                    }
                    if (dataviewData.transWldB.value == dataviewData.current.transWldb) {
                        dataviewData.transWldB.value = dataviewData.current.transWldB = selected.transWld.b;
                    }
                    if (dataviewData.transWldC.value == dataviewData.current.transLocC) {
                        dataviewData.transWldC.value = dataviewData.current.transLocC = selected.transWld.c;
                    }
                    if (dataviewData.transWldD.value == dataviewData.current.transWldD) {
                        dataviewData.transWldD.value = dataviewData.current.transWldD = selected.transWld.d;
                    }
                    if (dataviewData.transWldTX.value == dataviewData.current.transWldTX) {
                        dataviewData.transWldTX.value = dataviewData.current.transWldTX = selected.transWld.tx;
                    }
                    if (dataviewData.transWldTY.value == dataviewData.current.transWldTY) {
                        dataviewData.transWldTY.value = dataviewData.current.transWldTY = selected.transWld.ty;
                    }
                });
            }
            dataview.appendChild(transformDiv);
        }
        if (obj.alpha != null) {
            dataview.appendChild(createDataitem("Opacity", ['input', 'input'], [
                (slider, elements) => {
                    slider.type = "range";
                    slider.min = 0;
                    slider.max = 1;
                    slider.step = 0.001;
                    slider.value = obj.alpha;
                    slider.addEventListener("input", () => {
                        let val = slider.valueAsNumber;
                        obj.alpha = val;
                        elements[1].value = dataviewData.current.alphaslider = dataviewData.current.alpha = val;
                    });
                    dataviewData.current.alphaslider = Math.min(Math.max(obj.alpha, 0), 1);
                    slider.value = dataviewData.current.alphaslider;
                    dataviewData.alphaslider = slider;
                    slider.style["min-width"] = "50px";
                },
                (input, elements) => {
                    input.type = "number";
                    input.step = 0.1;
                    input.addEventListener("change", () => {
                        let val = dataviewData.current.alpha = input.valueAsNumber;
                        obj.alpha = val;
                        elements[0].value = Math.min(Math.max(val, 0), 1);
                    });
                    input.value = dataviewData.current.alpha = obj.alpha;
                    dataviewData.alpha = input;
                    input.style["min-width"] = 0;
                }
            ], "auto 75px"));
            dataviewUpdaters.push(() => {
                if (dataviewData.alpha.value != dataviewData.current.alpha) return;
                let val = dataviewData.current.alpha = selected.alpha;
                dataviewData.alphaslider.value = val;
                dataviewData.alpha.value = val;
            });
        }
        if (obj.text != null) {
            let item = createDataitem("Text", 'textarea', [
                (text, elements) => {
                    text.addEventListener("change", () => {
                        obj.data.text = dataviewData.current.extra.text = text.value;
                    });
                    text.value = dataviewData.current.extra.text = obj.data.text;
                    dataviewData.extra.text = text;
                    text.style["min-width"] = 0;
                }
            ]);
            item.style["min-height"] = item.style.height;
            item.style.height = "auto";
            dataview.appendChild(item);
            dataviewUpdaters.push(() => {
                if (dataviewData.extra.text.value != dataviewData.current.extra.text) return;
                dataviewData.extra.text.value = dataviewData.current.extra.text = selected.data.text;
            });
            dataviewData.textStyle = {};
            dataviewData.current.textStyle = {};
            let styleDiv = document.createElement('div');
            styleDiv.classList.add("sub");
            styleDiv.appendChild(createDataitem("Font", 'input', input => {
                input.type = "text";
                input.addEventListener("change", () => {
                    obj.textStylePrepare();
                    dataviewData.current.textStyle.font = obj.textStyle.fontFamily = input.value;
                });
                input.value = dataviewData.current.textStyle.font = obj.textStyle.fontFamily;
                dataviewData.textStyle.font = input;
            }, "minmax(0, 1fr) auto"));
            dataviewUpdaters.push(() => {
                if (dataviewData.textStyle.font.value != dataviewData.current.textStyle.font) return;
                dataviewData.textStyle.font.value = dataviewData.current.textStyle.font = selected.textStyle.fontFamily;
            });
            styleDiv.appendChild(createDataitem("Font Size", 'input', input => {
                input.type = "text";
                let pxReg = new RegExp("\\dpx$");
                input.addEventListener("change", () => {
                    obj.textStylePrepare();
                    let val = input.value;
                    dataviewData.current.textStyle.fontSize = val;
                    if (val.match(pxReg)) {
                        obj.textStyle.fontSize = parseInt(val.slice(0, -2));
                    } else {
                        let valnum = parseInt(val);
                        if (!isNaN(valnum)) {
                            obj.textStyle.fontSize = valnum+"px";
                        } else {
                            obj.textStyle.fontSize = val;
                        }
                    }
                    obj.textStyle.fontSize = val;
                });
                let fontval = obj.textStyle.fontSize;
                if (typeof(fontval) == 'number') fontval = fontval+"px";
                input.value = dataviewData.current.textStyle.fontSize = fontval;
                dataviewData.textStyle.fontSize = input;
            }, "minmax(0, 1fr) auto"));
            dataviewUpdaters.push(() => {
                if (dataviewData.textStyle.fontSize.value != dataviewData.current.textStyle.fontSize) return;
                let fontval = selected.textStyle.fontSize;
                if (typeof(fontval) == 'number') fontval = fontval+"px";
                dataviewData.textStyle.fontSize.value = dataviewData.current.textStyle.fontSize = fontval;
            });
            styleDiv.appendChild(createDataitem("Font Style", 'select', dropdown => {
                [['normal', "Normal"], ['italic', "Italic"], ['oblique', "Oblique"]].forEach(vals => {
                    let opt = document.createElement('option');
                    opt.innerHTML = vals[1];
                    opt.value = vals[0];
                    dropdown.appendChild(opt);
                });
                dropdown.addEventListener("change", () => {
                    obj.textStylePrepare();
                    obj.textStyle.fontStyle = dropdown.value;
                });
                dropdown.value = obj.textStyle.fontStyle;
                dataviewData.textStyle.fontStyle = dropdown;
            }));
            dataviewUpdaters.push(() => {
                dataviewData.textStyle.fontStyle.value = selected.textStyle.fontStyle;
            });
            styleDiv.appendChild(createDataitem("Font Variant", 'select', dropdown => {
                [['normal', "Normal"], ['small-caps', "Small Caps"]].forEach(vals => {
                    let opt = document.createElement('option');
                    opt.innerHTML = vals[1];
                    opt.value = vals[0];
                    dropdown.appendChild(opt);
                });
                dropdown.addEventListener("change", () => {
                    obj.textStylePrepare();
                    obj.textStyle.fontVariant = dropdown.value;
                });
                dropdown.value = obj.textStyle.fontVariant;
                dataviewData.textStyle.fontVariant = dropdown;
            }));
            dataviewUpdaters.push(() => {
                dataviewData.textStyle.fontVariant.value = selected.textStyle.fontVariant;
            });
            styleDiv.appendChild(createDataitem("Font Weight", 'select', dropdown => {
                [['normal', "Normal"], ['bold', "Bold"], ['bolder', "Bolder (+100)"], ['lighter', "Lighter (-100)"], ['100', "100"], ['200', "200"], ['300', "400"], ['400', "400 (Normal)"], ['500', "500"], ['600', "600"], ['700', "700 (Bold)"], ['800', "800"], ['900', "900"]].forEach(vals => {
                    let opt = document.createElement('option');
                    opt.innerHTML = vals[1];
                    opt.value = vals[0];
                    dropdown.appendChild(opt);
                });
                dropdown.addEventListener("change", () => {
                    obj.textStylePrepare();
                    obj.textStyle.fontWeight = dropdown.value;
                });
                dropdown.value = obj.textStyle.fontWeight;
                dataviewData.textStyle.fontWeight = dropdown;
            }));
            dataviewUpdaters.push(() => {
                dataviewData.textStyle.fontWeight.value = selected.textStyle.fontWeight;
            });
            {
                let color = intFromColor(obj.textStyle.stroke);
                dataviewData.textStyle.strokeColor = {};
                dataviewData.current.textStyle.strokeColor = {};
                styleDiv.appendChild(createDataitem("Stroke Color", ['div', 'span', 'input', 'span', 'input', 'span', 'input'], [
                    colorview => {
                        colorview.classList.add("colorview");
                        colorview.style.width = colorview.style.height = "19px";
                        colorview.style["background-color"] = obj.textStyle.stroke;
                        dataviewData.textStyle.strokeColor.view = colorview;
                    },
                    label => { label.innerHTML = "R:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            obj.textStylePrepare();
                            let val = Math.min(Math.max(Math.round(input.valueAsNumber), 0), 255);
                            input.value = dataviewData.current.textStyle.strokeColor.r = val;
                            color = (color & 0x00FFFF) | (val << 16);
                            obj.textStyle.stroke = color;
                        });
                        input.value = dataviewData.current.textStyle.strokeColor.r = color >> 16;
                        dataviewData.textStyle.strokeColor.r = input;
                    },
                    label => { label.innerHTML = ", G:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            obj.textStylePrepare();
                            let val = Math.min(Math.max(Math.round(input.valueAsNumber), 0), 255);
                            input.value = dataviewData.current.textStyle.strokeColor.g = val;
                            color = (color & 0xFF00FF) | (val << 8);
                            obj.textStyle.stroke = color;
                        });
                        input.value = dataviewData.current.textStyle.strokeColor.g = (color >> 8) & 0xFF;
                        dataviewData.textStyle.strokeColor.g = input;
                    },
                    label => { label.innerHTML = ", B:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            obj.textStylePrepare();
                            let val = Math.min(Math.max(Math.round(input.valueAsNumber), 0), 255);
                            input.value = dataviewData.current.textStyle.strokeColor.b = val;
                            color = (color & 0xFFFF00) | val;
                            obj.textStyle.stroke = color;
                        });
                        input.value = dataviewData.current.textStyle.strokeColor.b = color & 0xFF;
                        dataviewData.textStyle.strokeColor.b = input;
                    }
                ], "auto auto minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr)"));
            }
            dataviewUpdaters.push(() => {
                let color = intFromColor(selected.textStyle.stroke);
                dataviewData.textStyle.strokeColor.view.style["background-color"] = selected.textStyle.stroke;
                if (dataviewData.textStyle.strokeColor.r.value == dataviewData.current.textStyle.strokeColor.r) {
                    dataviewData.textStyle.strokeColor.r.value = dataviewData.current.textStyle.strokeColor.r = color >> 16;
                }
                if (dataviewData.textStyle.strokeColor.g.value == dataviewData.current.textStyle.strokeColor.g) {
                    dataviewData.textStyle.strokeColor.g.value = dataviewData.current.textStyle.strokeColor.g = (color >> 8) & 0xFF;
                }
                if (dataviewData.textStyle.strokeColor.b.value == dataviewData.current.textStyle.strokeColor.b) {
                    dataviewData.textStyle.strokeColor.b.value = dataviewData.current.textStyle.strokeColor.b = color & 0xFF;
                }
            });
            styleDiv.appendChild(createDataitem("Stroke Thickness", 'input', input => {
                input.type = "number";
                input.addEventListener("change", () => {
                    obj.textStylePrepare();
                    dataviewData.current.textStyle.strokeThickness = obj.textStyle.strokeThickness = input.valueAsNumber;
                });
                input.value = dataviewData.current.textStyle.strokeThickness = obj.textStyle.strokeThickness;
                dataviewData.textStyle.strokeThickness = input;
            }, "minmax(0, 1fr) auto"));
            dataviewUpdaters.push(() => {
                if (dataviewData.textStyle.strokeThickness.value != dataviewData.current.textStyle.strokeThickness) return;
                dataviewData.textStyle.strokeThickness.value = dataviewData.current.textStyle.strokeThickness = selected.textStyle.strokeThickness;
            });
            {
                let color = intFromColor(obj.textStyle.fill);
                dataviewData.textStyle.fillColor = {};
                dataviewData.current.textStyle.fillColor = {};
                styleDiv.appendChild(createDataitem("Fill Color", ['div', 'span', 'input', 'span', 'input', 'span', 'input'], [
                    colorview => {
                        colorview.classList.add("colorview");
                        colorview.style.width = colorview.style.height = "19px";
                        colorview.style["background-color"] = obj.textStyle.fill;
                        dataviewData.textStyle.fillColor.view = colorview;
                    },
                    label => { label.innerHTML = "R:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            obj.textStylePrepare();
                            let val = Math.min(Math.max(Math.round(input.valueAsNumber), 0), 255);
                            input.value = dataviewData.current.textStyle.fillColor.r = val;
                            color = (color & 0x00FFFF) | (val << 16);
                            obj.textStyle.fill = color;
                        });
                        input.value = dataviewData.current.textStyle.fillColor.r = color >> 16;
                        dataviewData.textStyle.fillColor.r = input;
                    },
                    label => { label.innerHTML = ", G:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            obj.textStylePrepare();
                            let val = Math.min(Math.max(Math.round(input.valueAsNumber), 0), 255);
                            input.value = dataviewData.current.textStyle.fillColor.g = val;
                            color = (color & 0xFF00FF) | (val << 8);
                            obj.textStyle.fill = color;
                        });
                        input.value = dataviewData.current.textStyle.fillColor.g = (color >> 8) & 0xFF;
                        dataviewData.textStyle.fillColor.g = input;
                    },
                    label => { label.innerHTML = ", B:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            obj.textStylePrepare();
                            let val = Math.min(Math.max(Math.round(input.valueAsNumber), 0), 255);
                            input.value = dataviewData.current.textStyle.fillColor.b = val;
                            color = (color & 0xFFFF00) | val;
                            obj.textStyle.fill = color;
                        });
                        input.value = dataviewData.current.textStyle.fillColor.b = color & 0xFF;
                        dataviewData.textStyle.fillColor.b = input;
                    }
                ], "auto auto minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr)"));
            }
            dataviewUpdaters.push(() => {
                let color = intFromColor(selected.textStyle.fill);
                dataviewData.textStyle.fillColor.view.style["background-color"] = selected.textStyle.fill;
                if (dataviewData.textStyle.fillColor.r.value == dataviewData.current.textStyle.fillColor.r) {
                    dataviewData.textStyle.fillColor.r.value = dataviewData.current.textStyle.fillColor.r = color >> 16;
                }
                if (dataviewData.textStyle.fillColor.g.value == dataviewData.current.textStyle.fillColor.g) {
                    dataviewData.textStyle.fillColor.g.value = dataviewData.current.textStyle.fillColor.g = (color >> 8) & 0xFF;
                }
                if (dataviewData.textStyle.fillColor.b.value == dataviewData.current.textStyle.fillColor.b) {
                    dataviewData.textStyle.fillColor.b.value = dataviewData.current.textStyle.fillColor.b = color & 0xFF;
                }
            });
            styleDiv.appendChild(createDataitem("Align", 'select', dropdown => {
                [['left', "Left"], ['center', "Center"], ['right', "Right"], ['justify', "Justify"]].forEach(vals => {
                    let opt = document.createElement('option');
                    opt.innerHTML = vals[1];
                    opt.value = vals[0];
                    dropdown.appendChild(opt);
                });
                dropdown.addEventListener("change", () => {
                    obj.textStylePrepare();
                    obj.textStyle.align = dropdown.value;
                });
                dropdown.value = obj.textStyle.align;
                dataviewData.textStyle.align = dropdown;
            }));
            dataviewUpdaters.push(() => {
                dataviewData.textStyle.align.value = selected.textStyle.align;
            });
            styleDiv.appendChild(createDataitem("Drop Shadow", 'input', check => {
                check.type = "checkbox";
                check.addEventListener("change", () => {
                    obj.textStylePrepare();
                    obj.textStyle.dropShadow = check.checked;
                });
                check.checked = obj.textStyle.dropShadow;
                dataviewData.textStyle.hasDropShadow = check;
            }));
            dataviewUpdaters.push(() => {
                dataviewData.textStyle.hasDropShadow.checked = selected.textStyle.dropShadow;
            });
            {
                let color = intFromColor(obj.textStyle.dropShadowColor);
                dataviewData.textStyle.dropShadowColor = {};
                dataviewData.current.textStyle.dropShadowColor = {};
                styleDiv.appendChild(createDataitem("Drop Shadow Color", ['div', 'span', 'input', 'span', 'input', 'span', 'input', 'span', 'input'], [
                    colorview => {
                        colorview.classList.add("colorview");
                        colorview.style.width = colorview.style.height = "19px";
                        colorview.style["background-color"] = obj.textStyle.dropShadowColor;
                        dataviewData.textStyle.dropShadowColor.view = colorview;
                    },
                    label => { label.innerHTML = "R:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            obj.textStylePrepare();
                            let val = Math.min(Math.max(Math.round(input.valueAsNumber), 0), 255);
                            input.value = dataviewData.current.textStyle.dropShadowColor.r = val;
                            color = (color & 0x00FFFF) | (val << 16);
                            obj.textStyle.dropShadowColor = color;
                        });
                        input.value = dataviewData.current.textStyle.dropShadowColor.r = color >> 16;
                        dataviewData.textStyle.dropShadowColor.r = input;
                    },
                    label => { label.innerHTML = ", G:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            obj.textStylePrepare();
                            let val = Math.min(Math.max(Math.round(input.valueAsNumber), 0), 255);
                            input.value = dataviewData.current.textStyle.dropShadowColor.g = val;
                            color = (color & 0xFF00FF) | (val << 8);
                            obj.textStyle.dropShadowColor = color;
                        });
                        input.value = dataviewData.current.textStyle.dropShadowColor.g = (color >> 8) & 0xFF;
                        dataviewData.textStyle.dropShadowColor.g = input;
                    },
                    label => { label.innerHTML = ", B:"; },
                    input => {
                        input.type = "number";
                        input.addEventListener("change", () => {
                            obj.textStylePrepare();
                            let val = Math.min(Math.max(Math.round(input.valueAsNumber), 0), 255);
                            input.value = dataviewData.current.textStyle.dropShadowColor.b = val;
                            color = (color & 0xFFFF00) | val;
                            obj.textStyle.dropShadowColor = color;
                        });
                        input.value = dataviewData.current.textStyle.dropShadowColor.b = color & 0xFF;
                        dataviewData.textStyle.dropShadowColor.b = input;
                    },
                    label => { label.innerHTML = ", A:"; },
                    input => {
                        input.type = "number";
                        input.step = 0.1;
                        input.addEventListener("change", () => {
                            obj.textStylePrepare();
                            let val = Math.min(Math.max(input.valueAsNumber, 0), 1);
                            input.value = dataviewData.current.textStyle.dropShadowColor.a = val;
                            obj.textStyle.dropShadowAlpha = val;
                        });
                        input.value = dataviewData.current.textStyle.dropShadowColor.a = obj.textStyle.dropShadowAlpha;
                        dataviewData.textStyle.dropShadowColor.a = input;
                    }
                ], "auto auto minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr)"));
            }
            dataviewUpdaters.push(() => {
                let color = intFromColor(selected.textStyle.dropShadowColor);
                dataviewData.textStyle.dropShadowColor.view.style["background-color"] = selected.textStyle.dropShadowColor;
                if (dataviewData.textStyle.dropShadowColor.r.value == dataviewData.current.textStyle.dropShadowColor.r) {
                    dataviewData.textStyle.dropShadowColor.r.value = dataviewData.current.textStyle.dropShadowColor.r = color >> 16;
                }
                if (dataviewData.textStyle.dropShadowColor.g.value == dataviewData.current.textStyle.dropShadowColor.g) {
                    dataviewData.textStyle.dropShadowColor.g.value = dataviewData.current.textStyle.dropShadowColor.g = (color >> 8) & 0xFF;
                }
                if (dataviewData.textStyle.dropShadowColor.b.value == dataviewData.current.textStyle.dropShadowColor.b) {
                    dataviewData.textStyle.dropShadowColor.b.value = dataviewData.current.textStyle.dropShadowColor.b = color & 0xFF;
                }
                if (dataviewData.textStyle.dropShadowColor.a.value == dataviewData.current.textStyle.dropShadowColor.a) {
                    dataviewData.textStyle.dropShadowColor.a.value = dataviewData.current.textStyle.dropShadowColor.a = selected.textStyle.dropShadowAlpha;
                }
            });
            styleDiv.appendChild(createDataitem("Drop Shadow Angle", ['input', 'button'], [
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        obj.textStylePrepare();
                        let val = input.valueAsNumber;
                        if (dataviewData.textStyle.dropShadowRot_degmode) val *= DEG2RAD;
                        dataviewData.current.textStyle.dropShadowAngle = obj.textStyle.dropShadowAngle = val;
                    });
                    input.value = dataviewData.current.textStyle.dropShadowAngle = obj.textStyle.dropShadowAngle * RAD2DEG;
                    dataviewData.textStyle.dropShadowAngle = input;
                }, dataItemDegRad(dataviewData, "textStyle.dropShadowRot_degmode", (dataviewData, elements) => {
                    elements[0].value = dataviewData.current.textStyle.dropShadowAngle = obj.textStyle.dropShadowAngle * RAD2DEG;
                    dataviewData.textStyle.dropShadowAngle.step = 1;
                }, (dataviewData, elements) => {
                    elements[0].value = dataviewData.current.textStyle.dropShadowAngle = obj.textStyle.dropShadowAngle;
                    dataviewData.textStyle.dropShadowAngle.step = DEG2RAD;
                })
            ], "minmax(0, 1fr) auto"));
            dataviewUpdaters.push(() => {
                if (dataviewData.textStyle.dropShadowAngle.value != dataviewData.current.textStyle.dropShadowAngle) return;
                let val = selected.textStyle.dropShadowAngle;
                if (dataviewData.textStyle.dropShadowRot_degmode) val *= RAD2DEG;
                dataviewData.textStyle.dropShadowAngle.value = dataviewData.current.textStyle.dropShadowAngle = val;
            });
            styleDiv.appendChild(createDataitem("Drop Shadow Blur", 'input', input => {
                input.type = "number";
                input.addEventListener("change", () => {
                    obj.textStylePrepare();
                    dataviewData.current.textStyle.dropShadowBlur = obj.textStyle.dropShadowBlur = input.valueAsNumber;
                });
                input.value = dataviewData.current.textStyle.dropShadowBlur = obj.textStyle.dropShadowBlur;
                dataviewData.textStyle.dropShadowBlur = input;
            }, "minmax(0, 1fr) auto"));
            dataviewUpdaters.push(() => {
                if (dataviewData.textStyle.dropShadowBlur.value != dataviewData.current.textStyle.dropShadowBlur) return;
                dataviewData.textStyle.dropShadowBlur.value = dataviewData.current.textStyle.dropShadowBlur = selected.textStyle.dropShadowBlur;
            });
            styleDiv.appendChild(createDataitem("Drop Shadow Distance", 'input', input => {
                input.type = "number";
                input.addEventListener("change", () => {
                    obj.textStylePrepare();
                    dataviewData.current.textStyle.dropShadowDistance = obj.textStyle.dropShadowDistance = input.valueAsNumber;
                });
                input.value = dataviewData.current.textStyle.dropShadowDistance = obj.textStyle.dropShadowDistance;
                dataviewData.textStyle.dropShadowDistance = input;
            }, "minmax(0, 1fr) auto"));
            dataviewUpdaters.push(() => {
                if (dataviewData.textStyle.dropShadowDistance.value != dataviewData.current.textStyle.dropShadowDistance) return;
                dataviewData.textStyle.dropShadowDistance.value = dataviewData.current.textStyle.dropShadowDistance = selected.textStyle.dropShadowDistance;
            });
            styleDiv.appendChild(createDataitem("Letter Spacing", 'input', input => {
                input.type = "number";
                input.addEventListener("change", () => {
                    obj.textStylePrepare();
                    dataviewData.current.textStyle.letterSpacing = obj.textStyle.letterSpacing = input.valueAsNumber;
                });
                input.value = dataviewData.current.textStyle.letterSpacing = obj.textStyle.letterSpacing;
                dataviewData.textStyle.letterSpacing = input;
            }, "minmax(0, 1fr) auto"));
            dataviewUpdaters.push(() => {
                if (dataviewData.textStyle.letterSpacing.value != dataviewData.current.textStyle.letterSpacing) return;
                dataviewData.textStyle.letterSpacing.value = dataviewData.current.textStyle.letterSpacing = selected.textStyle.letterSpacing;
            });
            styleDiv.appendChild(createDataitem("Line Spacing", 'input', input => {
                input.type = "number";
                input.addEventListener("change", () => {
                    obj.textStylePrepare();
                    dataviewData.current.textStyle.lineSpacing = obj.textStyle.leading = input.valueAsNumber;
                });
                input.value = dataviewData.current.textStyle.lineSpacing = obj.textStyle.leading;
                dataviewData.textStyle.lineSpacing = input;
            }, "minmax(0, 1fr) auto"));
            dataviewUpdaters.push(() => {
                if (dataviewData.textStyle.lineSpacing.value != dataviewData.current.textStyle.lineSpacing) return;
                dataviewData.textStyle.lineSpacing.value = dataviewData.current.textStyle.lineSpacing = selected.textStyle.leading;
            });
            styleDiv.appendChild(createDataitem("Line Spacing 2", 'input', input => {
                input.type = "number";
                input.addEventListener("change", () => {
                    obj.textStylePrepare();
                    dataviewData.current.textStyle.letterHeight = obj.textStyle.lineHeight = input.valueAsNumber;
                });
                input.value = dataviewData.current.textStyle.letterHeight = obj.textStyle.lineHeight;
                dataviewData.textStyle.letterHeight = input;
            }, "minmax(0, 1fr) auto"));
            dataviewUpdaters.push(() => {
                if (dataviewData.textStyle.letterHeight.value != dataviewData.current.textStyle.letterHeight) return;
                dataviewData.textStyle.letterHeight.value = dataviewData.current.textStyle.letterHeight = selected.textStyle.lineHeight;
            });
            styleDiv.appendChild(createDataitem("Baseline", 'select', dropdown => {
                [['alphabetic', "Alphabetic"], ['top', "Top"], ['hanging', "Hanging"], ['middle', "Middle"], ['ideographic', "Ideographic"], ['bottom', "Bottom"]].forEach(vals => {
                    let opt = document.createElement('option');
                    opt.innerHTML = vals[1];
                    opt.value = vals[0];
                    dropdown.appendChild(opt);
                });
                dropdown.addEventListener("change", () => {
                    obj.textStylePrepare();
                    obj.textStyle.textBaseline = dropdown.value;
                });
                dropdown.value = obj.textStyle.textBaseline;
                dataviewData.textStyle.baseline = dropdown;
            }));
            dataviewUpdaters.push(() => {
                dataviewData.textStyle.baseline.value = selected.textStyle.textBaseline;
            });
            styleDiv.appendChild(createDataitem("Trim Transparent Borders", 'input', check => {
                check.type = "checkbox";
                check.addEventListener("change", () => {
                    obj.textStylePrepare();
                    obj.textStyle.trim = check.checked;
                });
                check.checked = obj.textStyle.trim;
                dataviewData.textStyle.trim = check;
            }));
            dataviewUpdaters.push(() => {
                dataviewData.textStyle.trim.checked = selected.textStyle.trim;
            });
            styleDiv.appendChild(createDataitem("Padding", 'input', input => {
                input.type = "number";
                input.addEventListener("change", () => {
                    obj.textStylePrepare();
                    dataviewData.current.textStyle.padding = obj.textStyle.padding = input.valueAsNumber;
                });
                input.value = dataviewData.current.textStyle.padding = obj.textStyle.padding;
                dataviewData.textStyle.padding = input;
            }, "minmax(0, 1fr) auto"));
            dataviewUpdaters.push(() => {
                if (dataviewData.textStyle.padding.value != dataviewData.current.textStyle.padding) return;
                dataviewData.textStyle.padding.value = dataviewData.current.textStyle.padding = selected.textStyle.padding;
            });
            styleDiv.appendChild(createDataitem("Whitespace Handling", 'select', dropdown => {
                [['normal', "Collapse Space and Newline"], ['pre', "Preserve Space and Newline"], ['pre-wrap', "Preserve Space, Collapse Newline"], ['pre-line', "Collapse Space, Preserve Newline"]].forEach(vals => {
                    let opt = document.createElement('option');
                    opt.innerHTML = vals[1];
                    opt.value = vals[0];
                    dropdown.appendChild(opt);
                });
                dropdown.addEventListener("change", () => {
                    obj.textStylePrepare();
                    obj.textStyle.whiteSpace = dropdown.value;
                });
                dropdown.value = obj.textStyle.whiteSpace;
                dataviewData.textStyle.baseline = dropdown;
            }));
            dataviewUpdaters.push(() => {
                dataviewData.textStyle.baseline.value = selected.textStyle.whiteSpace;
            });
            styleDiv.appendChild(createDataitem("Word Wrap", 'input', check => {
                check.type = "checkbox";
                check.addEventListener("change", () => {
                    obj.textStylePrepare();
                    obj.textStyle.wordWrap = check.checked;
                });
                check.checked = obj.textStyle.wordWrap;
                dataviewData.textStyle.wordWrap = check;
            }));
            dataviewUpdaters.push(() => {
                dataviewData.textStyle.wordWrap.checked = selected.textStyle.wordWrap;
            });
            styleDiv.appendChild(createDataitem("Word Wrap Width", 'input', input => {
                input.type = "number";
                input.addEventListener("change", () => {
                    obj.textStylePrepare();
                    dataviewData.current.textStyle.wordWrapWidth = obj.textStyle.wordWrapWidth = input.valueAsNumber;
                });
                input.value = dataviewData.current.textStyle.wordWrapWidth = obj.textStyle.wordWrapWidth;
                dataviewData.textStyle.wordWrapWidth = input;
            }, "minmax(0, 1fr) auto"));
            dataviewUpdaters.push(() => {
                if (dataviewData.textStyle.wordWrapWidth.value != dataviewData.current.textStyle.wordWrapWidth) return;
                dataviewData.textStyle.wordWrapWidth.value = dataviewData.current.textStyle.wordWrapWidth = selected.textStyle.wordWrapWidth;
            });
            styleDiv.appendChild(createDataitem("Break Words", 'input', check => {
                check.type = "checkbox";
                check.addEventListener("change", () => {
                    obj.textStylePrepare();
                    obj.textStyle.breakWords = check.checked;
                });
                check.checked = obj.textStyle.breakWords;
                dataviewData.textStyle.breakWords = check;
            }));
            dataviewUpdaters.push(() => {
                dataviewData.textStyle.breakWords.checked = selected.textStyle.breakWords;
            });
            styleDiv.appendChild(createDataitem("Line Join", 'select', dropdown => {
                [['miter', "Miter"], ['round', "Round"], ['bevel', "Bevel"]].forEach(vals => {
                    let opt = document.createElement('option');
                    opt.innerHTML = vals[1];
                    opt.value = vals[0];
                    dropdown.appendChild(opt);
                });
                dropdown.addEventListener("change", () => {
                    obj.textStylePrepare();
                    obj.textStyle.lineJoin = dropdown.value;
                });
                dropdown.value = obj.textStyle.lineJoin;
                dataviewData.textStyle.lineJoin = dropdown;
            }));
            dataviewUpdaters.push(() => {
                dataviewData.textStyle.lineJoin.value = selected.textStyle.lineJoin;
            });
            styleDiv.appendChild(createDataitem("(LineJoin) Miter Limit", 'input', input => {
                input.type = "number";
                input.addEventListener("change", () => {
                    obj.textStylePrepare();
                    dataviewData.current.textStyle.lineJoinMiterLimit = obj.textStyle.miterLimit = input.valueAsNumber;
                });
                input.value = dataviewData.current.textStyle.lineJoinMiterLimit = obj.textStyle.miterLimit;
                dataviewData.textStyle.lineJoinMiterLimit = input;
            }, "minmax(0, 1fr) auto"));
            dataviewUpdaters.push(() => {
                if (dataviewData.textStyle.lineJoinMiterLimit.value != dataviewData.current.textStyle.lineJoinMiterLimit) return;
                dataviewData.textStyle.lineJoinMiterLimit.value = dataviewData.current.textStyle.lineJoinMiterLimit = selected.textStyle.miterLimit;
            });
            dataview.appendChild(styleDiv);
        }
        if (obj.data.hasOwnProperty("_texture")) {
            let item = createDataitem("Texture", ['div', 'a', 'a', 'button'], [
                container => {
                    container.classList.add("imageview");
                    container.style["grid-column"] = "span 3";
                    let img = document.createElement('img');
                    let errMsg = document.createElement('p');
                    errMsg.classList.add("errmsg");
                    container.appendChild(img);
                    container.appendChild(errMsg);
                    dataviewData.extra.texture = img;
                    dataviewData.extra.texturefailure = errMsg;
                },
                btncont => {
                    btncont.target = "_blank";
                    btncont.rel = "noopener noreferrer";
                    let btn = document.createElement('button');
                    btn.innerHTML = "<i class=\"fa fa-external-link\"></i> Open";
                    btn.style.width = "100%";
                    btncont.appendChild(btn);
                    dataviewData.extra.textureOpenBtn = btn;
                    btncont.style["min-width"] = btn.style["min-width"] = 0;
                    btn.style.overflow = "clip";
                },
                btncont => {
                    let btn = document.createElement('button');
                    btn.innerHTML = "<i class=\"fa fa-download\"></i> Export";
                    btn.style.width = "100%";
                    btncont.appendChild(btn);
                    dataviewData.extra.textureExportBtn = btn;
                    btncont.style["min-width"] = btn.style["min-width"] = 0;
                    btn.style.overflow = "clip";
                },
                btn => {
                    let filecont = document.createElement('label');
                    filecont.innerHTML = "<i class=\"fa fa-files-o\"></i> Change";
                    // Source - https://stackoverflow.com/a/27165977
                    // Posted by nkron, modified by community. See post 'Timeline' for change history
                    // Retrieved 2026-04-23, License - CC BY-SA 4.0
                    if (selected.text != null) btn.disabled = true;
                    else {
                        let fileInp = document.createElement('input');
                        fileInp.style.display = "none";
                        fileInp.type = "file";
                        fileInp.accept = "image/*";
                        let reader = new FileReader();
                        reader.onload = () => {
                            selected.texture = window.PIXI.Texture.from(reader.result);
                        };
                        fileInp.addEventListener("change", evt => {
                            reader.readAsDataURL(evt.target.files[0]);
                        });
                        fileInp.style["min-width"] = 0;
                        filecont.appendChild(fileInp);
                    }
                    btn.appendChild(filecont);
                    btn.style["min-width"] = 0;
                    btn.style.overflow = "clip";
                }
            ], "1fr 1fr 1fr");
            item.style["min-height"] = item.style.height;
            item.style.height = "auto";
            dataview.appendChild(item);
            dataviewUpdaters.push(() => {
                if (dataviewData.current.extra.texture) {
                    if (selected.data.texture != dataviewData.current.extra.texture) selected.texture = selected.data.texture;
                    else if (!selected.texture?.hasChanged()) return;
                }
                dataviewData.extra.texture.style.display = "block";
                dataviewData.extra.texturefailure.style.display = "none";
                let url = selected.texture.texture.baseTexture.resource?.url;
                dataviewData.extra.textureOpenBtn.disabled = !Boolean(url);
                dataviewData.extra.textureOpenBtn.parentElement.href = url;
                try {
                    let srcImgData = selected.texture.getDataUrl();
                    dataviewData.extra.textureExportBtn.parentElement.href = srcImgData;
                    let filename = selected.texture.texture.baseTexture.cacheId;
                    if (!filename) {
                        if (url) {
                            filename = url.substring(url.lastIndexOf('/')+1); // If there is no slash, it will be -1, which when 1 is added makes 0. This works out perfectly.
                            let lastDot = filename.lastIndexOf('.');
                            filename = filename.substring(0, lastDot);
                        } else filename = "texture";
                    }
                    dataviewData.extra.textureExportBtn.parentElement.download = filename+".png";
                    dataviewData.extra.texture.src = srcImgData;
                } catch (e) {
                    console.error(e);
                    if (!dataviewData.extra.texture) return;
                    dataviewData.extra.texture.style.display = "none";
                    dataviewData.extra.texturefailure.style.display = "block";
                    dataviewData.extra.texturefailure.innerHTML = "Something went wrong.";
                }
                dataviewData.current.extra.texture = selected.data.texture;
            });
        }
        dataview.appendChild(createDataitem("Create Child Sprite", 'button', btn => {
            btn.innerHTML = "this.addChild(new PIXI.Sprite())";
            btn.addEventListener("click", () => {
                obj.data.addChild(new window.PIXI.Sprite());
            });
        }));
        {
            let desBtn;
            dataview.appendChild(createDataitem("Destroy Object", 'button', btn => {
                desBtn = btn;
                btn.innerHTML = "this.destroy()";
            }));
            dataviewUpdaters.push(menu => {
                if (!desBtn) return;
                desBtn.addEventListener("click", () => {
                    if (!menu.confirm("Are you sure you want to destroy this object and its descendants?")) return;
                    selected.data.destroy();
                    openObject(selected.node.parent.object);
                });
                desBtn = null;
            });
        }
    }

    function updateTreeObject(obj) {
        if (obj.name != null && obj.name.trim() != "") {
            if (obj.element.txt_inner != obj.name) {
                obj.element.txt_inner = obj.name;
            }
        } else if (obj.element.txt_inner != null) {
            obj.element.txt_inner = null;
            obj.element.txt.innerHTML = `<i> ${obj.typename ? obj.typename : "Unknown"}</i>`;
        }
        let activeVal = obj.active;
        if (obj.element.checkbox && obj.element.checkbox.checked != activeVal) {
            obj.element.checkbox.checked = activeVal;
        }

        obj.element.updateChildren();

        let updatedChildren = [];
        ["treeObject", "pixiObject"].forEach(type => { // Probably a horrendous way of going about iterating this, but it lets me so it makes my brain happy.
            if (obj[type]?.children != null) {
                obj[type].children.forEach(childData => { // Finds and adds new children.
                    let child = obj.node.children.find(o => o.object[type] == childData)?.object;
                    if (!child) {
                        child = new ManipulatableObject(childData, obj.element.childList, obj);
                        obj.node.add(child);
                    }
                    updateTreeObject(child);
                    updatedChildren.push(child);
                });
            }
        });
        obj.node.children.forEach(child => { // Removes old children.
            if (!updatedChildren.includes(child.object)) {
                child.removeSelf();
            }
        });
    }

    // needed because can't open popup until there is an interaction
    let createprodigytreeviewermodbutton = document.createElement('button');
    document.body.appendChild(createprodigytreeviewermodbutton);
    createprodigytreeviewermodbutton.innerText = "Open Tree View";
    createprodigytreeviewermodbutton.style = "position: fixed; top: 10px; left: 10px;";
    createprodigytreeviewermodbutton.onclick = () => {
        if (!gameaccess) {
            createprodigytreeviewermodbutton.remove();
            console.log(`No game detected.

  ____________________
< How did we get here? >
  --------------------
         \\   ^__^
          \\  (oo)\\_______
             (__)\\       )\\/\\\\
                 ||----w |
                 ||     ||
`); // https://github.com/cowsay-org/cowsay
            return;
        }

        // Source - https://stackoverflow.com/a/16992521
        // Posted by Vadim
        // Retrieved 2026-03-18, License - CC BY-SA 3.0
        var menu = window.open("", "_blank", "height=500,width=900,status=yes,toolbar=0,menubar=0,location=0");
        if (!menu.document.body || !menu.document.body.innerHTML) {
            menu.document.write(css);
            menu.document.write(`
<!-- https://www.w3schools.com/howto/howto_js_treeview.asp -->
<span style="display:none">load-bearing element; the script crashes without this</span>
`);
            treeDiv = document.createElement('div');
            treeDiv.classList.add("tree");
            menu.document.body.appendChild(treeDiv);
            menu.window.addEventListener("keydown", function (evt) {
                if (!selected) return;
                if (['INPUT', 'TEXTAREA'].includes(menu.document.activeElement.tagName.toUpperCase())) return;
                switch (evt.key) {
                    case ' ': {
                        evt.preventDefault();
                        if (!selected.element.checkbox) return;
                        let val = !selected.active;
                        selected.active = val;
                        selected.element.checkbox.checked = val;
                        if (dataviewData.active) dataviewData.active.checked = val;
                    } break;
                    case "ArrowUp":
                        evt.preventDefault();
                        if (selected.node.siblingUp) {
                            let up = selected.node.siblingUp;
                            while (!up.object.element.childrenHidden && up.children.length > 0) up = up.children[up.children.length-1];
                            openObject(up);
                        } else if (selected.node.parent) openObject(selected.node.parent);
                        else return;
                        break;
                    case "ArrowLeft":
                        evt.preventDefault();
                        if (!selected.element.childrenHidden) selected.element.doToggle();
                        else if (selected.node.parent) openObject(selected.node.parent);
                        else return;
                        break;
                    case "ArrowDown":
                        evt.preventDefault();
                        if (!selected.element.childrenHidden && selected.node.children.length > 0) openObject(selected.node.children[0]);
                        else if (selected.node.siblingDown) openObject(selected.node.siblingDown);
                        else {
                            let par = selected.node.parent;
                            while (par && !par.siblingDown) par = par.parent;
                            if (par && par.siblingDown) openObject(par.siblingDown);
                            else return;
                        }
                        break;
                    case "ArrowRight":
                        evt.preventDefault();
                        if (selected.node.children.length == 0) return;
                        if (selected.element.childrenHidden) selected.element.doToggle();
                        else openObject(selected.node.children[0]);
                        break;
                }
            });

            let drag = false; let moveX;
            let splitter = document.createElement('div');
            splitter.classList.add("splitter");
            menu.document.body.appendChild(splitter);

            dataview = document.createElement('div');
            dataview.classList.add("dataview");
            menu.document.body.appendChild(dataview);

            treeDiv.style.width = "300px";
            let treeDivResize = x => {
                let bodyStyle = menu.window.getComputedStyle(menu.document.body);
                let treeStyle = menu.window.getComputedStyle(treeDiv);
                treeDiv.style.width = (x-32)+"px";
            };
            splitter.addEventListener("mousedown", function (evt) {
                drag = true;
                treeDivResize(evt.x);
            });
            menu.window.addEventListener("mouseup", function(evt) {
                drag = false;
            });
            menu.window.addEventListener("mousemove", function (evt) {
                if (drag) treeDivResize(evt.x);
            });

            menu.updateTree = setInterval(() => {
                if (!gameaccess) {
                    menu.document.write(`
<p style="color: #F00; font-size: 20px;">Prodigy instance lost.</p>
`);
                    clearInterval(menu.updateTree);
                    return;
                }
                if (menu.closed) {
                    clearInterval(menu.updateTree);
                    treeData = null;
                    return;
                }
                let root = gameaccess.stage;
                let pathUp = [];
                /*while (root.parent != null) {
                    pathUp.push(root.parent.children.indexOf(root));
                    root = root.parent; // The stage might not be the root object.
                }*/ // but tbf it's not much of interest anyway
                if (treeData != null && treeData.data != root) {
                    openObject(null);
                    treeData.element.list.remove();
                    treeData = null;
                }
                let newTree = !treeData;
                if (newTree) {
                    let treeBase = document.createElement('ul');
                    treeBase.style = "margin: 0; padding: 0;";
                    treeDiv.appendChild(treeBase);
                    treeData = new ManipulatableObject(root, treeBase, null);
                    new ObjectHeirarchy(treeData);
                }
                updateTreeObject(treeData);
                if (newTree) {
                    treeData.element.doToggle();
                    pathUp.forEach(num => treeData.node.children[num].element.doToggle());
                }
            }, 100);

            menu.updateData = setInterval(() => {
                if (!gameaccess || menu.closed) {
                    clearInterval(menu.updateData);
                    return;
                }
                if (!selected?.inTree) return;

                dataviewUpdaters.forEach(u => u(menu));
            }, 5);
        }
    };
})();