GUI.js

import * as util from '../src/utils.js'
import dat from '../vendor/dat.gui.js'

/** @class */
class GUI {
    /**
     * @param {Object} template A set of name/object pairs, one per UI element
     *
     * @example
     * const gui = new GUI ({
     *     opacity: { // slider
     *         val: [canvas.opacity, [0, 1, 0.1]],
     *         cmd: val => canvas.setOpacity(val),
     *     },
     *     download: { // button
     *         cmd: () => util.downloadBlob(data, 'data.json', false),
     *     },
     *     ...
     * })
     */
    constructor(template) {
        this.template = template

        this.controllers = {}
        this.values = {} // the key/val's from each template

        this.gui = new dat.GUI()
        const guis = [this.gui]

        // this.folders['default'] = this.baseGui

        let newFolder = obj => !obj.val && !obj.cmd
        const parseGuis = obj => {
            util.forLoop(obj, (obj, key) => {
                if (newFolder(obj)) {
                    console.log('new follder', key)

                    this.gui = this.gui.addFolder(key)
                    guis.push(this.gui)
                    parseGuis(obj)
                    guis.pop()
                    this.gui = guis.at(-1)
                } else {
                    this.controllers[key] = this.addUI(obj, key)
                }
            })
        }
        parseGuis(template)

        console.log('controllers, values', this.controllers, this.values, guis)
    }

    type(obj) {
        const { val, cmd } = obj
        const valType = util.typeOf(val)
        const cmdType = util.typeOf(cmd)

        if (this.isDatColor(val)) return 'color'
        if (valType === 'undefined') return 'button'
        // if (val === 'listen') return 'monitor'
        if (valType === 'boolean') return 'toggle'
        if (valType === 'string') return 'input'
        if (valType === 'array' && val.length === 2) {
            if (util.typeOf(val[0]) === 'number') return 'slider'
            if (util.typeOf(val[0]) === 'string') return 'chooser'
            if (util.typeOf(val[0]) === 'object') return 'monitor'
            if (util.typeOf(val[0]) === 'array') return 'monitor'
        }

        throw Error('GUI type error, val: ' + val + ' cmd: ' + cmd)
    }

    /**
     *
     * @param {Object} obj A gui object with two optional objects: 'val' and 'cmd'
     * @param {string} key The name of the gui
     * @returns A dat.gui control object
     */
    addUI(obj, key) {
        let { val, cmd } = obj
        const type = this.type(obj)

        let control, extent

        if (type === 'monitor') cmd = () => val[0][val[1]]
        if (['slider', 'chooser'].includes(type)) [val, extent] = val
        if (type === 'button') val = cmd

        console.log('addUI:', type, key, val, cmd)

        this.values[key] = val

        switch (type) {
            case 'slider':
                const [min, max, step = 1] = extent
                control = this.gui.add(this.values, key, min, max).step(step)
                break

            case 'chooser':
                control = this.gui.add(this.values, key, extent)
                break

            case 'color':
                control = this.gui.addColor(this.values, key)
                break

            case 'button':
            case 'toggle':
            case 'input':
                control = this.gui.add(this.values, key)
                break

            case 'monitor':
                control = this.gui.add(val[0], val[1])
                break

            default:
                throw Error(`Controller.addUI: bad type: ${type}`)
        }

        // initialize: set model etc initial values to this value
        if (!['monitor', 'button', 'toggle'].includes(type)) cmd(val)
        // if (cmd && val && type !== 'monitor') cmd(val)
        // if (val === 'listen') this.setListener(key, cmd)

        if (cmd) {
            // if (val === 'listen') control.listen().onChange(cmd)
            if (type === 'monitor') control.listen() //.onChange(cmd)
            else control.onChange(cmd)
        }

        return control
    }
    updateGui(name, value) {
        console.log('updateGui name, value', name, value)
    }

    isDatColor(val) {
        if (util.typeOf(val) === 'string') {
            if (val[0] === '#') return val.length === 7 || val.length === 4
            if (val.startsWith('rgb(') || val.startsWith('rgba('))
                return val.endsWith(')')
            if (val.startsWith('hsl(') || val.startsWith('hsla('))
                return val.endsWith(')')
            if (val.startsWith('hsv(') || val.startsWith('hsva('))
                return val.endsWith(')')
        }

        if (util.typeOf(val) === 'object')
            return val.h != null && val.s != null && val.v != null
        // if (util.typeOf(val) === 'array') {
        //     if (val.length === 3 || val.length === 4)
        //         return val.every(i => util.typeOf(i) === 'number')
        // }

        return false
    }
    // setListener(key, cmd) {
    //     this.values[key] = cmd()
    // }
    // update() {
    //     util.forLoop(this.template, (obj, key) => {
    //         if (obj.val === 'listen') this.setListener(key, obj.cmd)
    //     })
    // }
}

export default GUI