Repository

js/Mobilizing/renderer/3D/three/ui/Panel.js

import Clickable from "./Clickable";
import Color from "../types/Color";
import Component from "../../../../core/Component";
import Mesh from "../shape/Mesh";
import Node from "../shape/3D/primitive/Node";
import Plane from "../shape/2D/Plane";
import Texture from "../texture/Texture";
import Vector2 from "../types/Vector2";
import { isObject } from "../../../../core/util/Misc";
import RichText from "../../../../text/RichText";

const OPACITY_ENABLED = 1;
const OPACITY_DISABLED = 0.3;

export default class Panel extends Component {
    /**
     * Panel UI element is a kind of aggregation between a button, a textured plane and a text label.
     * It has a similar interactive behavior as a button.
     * @param {Object} params Parameters object, given by the constructor.
     * @param {String} [params.title] the title of this panel, a text that will be displayed as a title.
     * @param {Camera} [params.camera] the camera to be used for picking.
     * @param {String} [params.material="basic"] this panel material type (String) to use for its creation.
     * @param {Pointer} [params.pointer] the pointer Object to be used for picking.
     * @param {Number} [params.width=50] the panel's width.
     * @param {Number} [params.height=50] the panel's height.
     * @param {Number} [params.cutOff="auto"] the bevel angle for corners.
     * @param {Number} [params.strokeWidth="auto"] the colored stroke around the panel.
     * @param {Number} [params.titleHeight="auto"] the title text's height (in pixel)
     * @param {Color} [params.fillColor=Color.black] the text's color
     * @param {Color} [params.strokeColor=Color.mobilizing] the stroke's color
     * @param {Font} [params.font] the panels regular font
     * @param {Font} [params.fontItalic] the panels italic font
     * @param {Font} [params.fontBold] the panels bold font
     * @param {Font} [params.fontBoldItalic] the panels bold-italic font
     * @param {Texture} [params.texture] the texture to map on the panel
    */
    constructor({
        title = null,
        camera = null,
        material = "basic",
        pointer = null,
        width = 50,
        height = 50,
        cutOff = "auto",
        strokeWidth = "auto",
        titleHeight = "auto",
        fillColor = Color.black.clone(),
        strokeColor = Color.mobilizing.clone(),
        font = null,
        fontItalic = null,
        fontBold = null,
        fontBoldItalic = null,
        texture = null
    } = {}) {

        super(...arguments);

        this.title = title;

        this.camera = camera;
        this.pointer = pointer;
        this.material = material;
        this.width = width;
        this.height = height;
        this.cutOff = cutOff;
        this.strokeWidth = strokeWidth;
        this.titleHeight = titleHeight;
        this.fillColor = fillColor;
        this.strokeColor = strokeColor;
        this.titleHeight = titleHeight;
        this.font = font;
        this.fontItalic = fontItalic;
        this.fontBold = fontBold;
        this.fontBoldItalic = fontBoldItalic;
        this.texture = texture;

        if (this.cutOff === "auto") {
            this.cutOff = Math.min(this.width, this.height) / 10;
        }
        else if (this.cutOff === 0) {
            //singularity with cutOff == 0 : titleHeight will be 0 and no text will be displayed
            if (this.titleHeight === "auto") {
                this.titleHeight = 3;
            }
        }

        if (this.strokeWidth === "auto") {
            this.strokeWidth = Math.min(this.width, this.height) / 50;
        }

        this.titleWidth = this.height - (2 * this.cutOff);

        if (this.titleHeight === "auto") {
            this.titleHeight = (typeof this.cutOff === "number") ? this.cutOff : (this.width / 10);
        }

        // Node & Materials
        this.root = new Node();
        this.transform = this.root.transform;
        this.materials = [];

        // Vertices
        const w = this.width / 2;
        const h = this.height / 2;

        this.vertices = [
            // Top
            new Vector2(-w + this.cutOff, h),
            new Vector2(w - this.cutOff, h),
            // Right
            new Vector2(w, h - this.cutOff),
            new Vector2(w, -h + this.cutOff),
            // Bottom
            new Vector2(w - this.cutOff, -h),
            new Vector2(-w + this.cutOff, -h),
            // Left
            new Vector2(-w, -h + this.cutOff),
            new Vector2(-w, h - this.cutOff)
        ];

        // Base mesh
        this.mesh = new Mesh({ "material": this.material });
        this.mesh.generateFillMesh(this.vertices);
        //!!trick for panelStack
        this.mesh.uiObject = this;
        this.root.transform.addChild(this.mesh.transform);

        if (title === null && isObject(texture)) {
            this.mesh.material.setTexture(texture);
        }
        else {
            this.mesh.material.setColor(this.fillColor);
        }

        this.mesh.material.setTransparent(true);
        this.materials.push(this.mesh.material);

        // Stroke
        this.strokeMesh = Mesh.generateStrokeMesh(this.mesh, this.strokeWidth);
        this.root.transform.addChild(this.strokeMesh.transform);

        this.strokeMesh.material.setColor(this.strokeColor);
        this.strokeMesh.material.setTransparent(true);

        this.materials.push(this.strokeMesh.material);

        if (title !== null) {
            // Title
            const canvasWidth = Math.max(this.titleWidth, 512);
            const canvasHeight = canvasWidth * (this.titleHeight / this.titleWidth);

            //default font are managed by RichText class
            this.titleText = new RichText({
                "width": canvasWidth,
                "height": canvasHeight,
                "marginTop": 0,
                "text": `<b>${this.title}</b>`,
                "textSize": canvasHeight * 0.75,
                "textAlign": "right",
                "textColor": this.strokeColor.getStyle(),
                "backgroundColor": Color.transparent.makeRGBAStringWithAlpha(0),
                "font": this.font,
                "fontItalic": this.fontItalic,
                "fontBold": this.fontBold,
                "fontBoldItalic": this.fontBoldItalic
            });
            //console.log("this.titleText", this.titleText);

            this.titleTexture = new Texture({ canvas: this.titleText.getCanvas() });
            //console.log("this.titleTexture", this.titleTexture);

            this.texturedMesh = new Plane({
                "material": this.material,
                "width": this.titleWidth,
                "height": this.titleHeight
            });

            this.texturedMesh.transform.setLocalPosition((this.titleHeight / 2 - this.width / 2), 0, 1);
            this.texturedMesh.transform.setLocalRotation(0, 0, 90);
            //console.log("this.texturedMesh", this.texturedMesh);
            this.root.transform.addChild(this.texturedMesh.transform);

            this.texturedMesh.material.setTexture(this.titleTexture);
            this.texturedMesh.material.setTransparent(true);

            this.materials.push(this.texturedMesh.material);

            // Image
            if (isObject(this.texture)) {

                this.imgMesh = new Plane({
                    "material": this.material,
                    "width": this.width - (2 * this.titleHeight),
                    "height": this.height - (2 * this.titleHeight)
                });
                this.imgMesh.transform.setLocalPositionZ(0.5);
                this.imgMesh.material.setTransparent(true);
                this.imgMesh.material.setTexture(texture);

                this.root.transform.addChild(this.imgMesh.transform);

                this.materials.push(this.imgMesh.material);
            }
        }
    }

    setup() {
        super.setup();

        if (this.context === null) {
            throw new Error("Add Panel to a context before calling setup()");
        }
        // Interactive element
        this.clickable = new Clickable({
            "camera": this.camera,
            "mesh": this.mesh,
            "pointer": this.pointer,

            "onPress": (pick) => {
                if (this.active) {
                    this.events.trigger("down", pick);
                }
            },
            "onRelease": (pick) => {
                if (this.active) {
                    this.events.trigger("up", pick);
                    this.events.trigger("click", pick);
                }
            },
            "onEnter": (pick) => {
                if (this.active) {
                    this.events.trigger("over", pick);
                }
            },
            "onLeave": () => {
                if (this.active) {
                    this.events.trigger("out");
                }
            }
        });

        this.context.addComponent(this.clickable);

        this.clickable.setup();
        this.on();
    }

    /* update() {
    } */

    setStrokeColor(color) {
        this.strokeColor = color;
        this.strokeMesh.material.setColor(this.strokeColor);

        if (this.titleText) {
            this.titleText.textColor = color;
            // TODO: Fix text shifting (margin increasing) on each render
            this.titleText.lines = [];
            this.titleText.render();
        }
    }

    on() {
        super.on();
        this.materials.forEach((material) => material.setOpacity(OPACITY_ENABLED));
    }

    off() {
        super.off();
        this.materials.forEach((material) => material.setOpacity(OPACITY_DISABLED));
    }
}