lil-gui

"Auto" GUI

The autoGUI() function traverses an object, adding controllers for all applicable properties. Nested objects become folders. It also returns a "proxy" for the object passed. Assignments to that object will update the GUI display, sort of like a lazy listen().

This page:
import { autoGUI, range, options, color } from './autoGUI.js'

const { object, gui } = autoGUI( {
	foo: 'bar',
	bar: false,
	baz: 0,
	Folders: {
		foo: 'bar',
		bar: true,
		nestedFolder1: {
			foo: 'bar',
			bar: false
		},
		nestedFolder2: {
			foo: 'bar',
			bar: 10
		}
	},
	Directives: {

		// we need "directives" when we want a special controller
		color: color( 0xff00ff ),

		// defaults to the first option
		options: options( 'a', 'b', 'c' ),

		// initial, min, max
		range: range( 2.5, 0, 10 )

	}
} );

window.object = object;

Try modifying the object variable in your console to see the GUI update.

./autoGUI.js
import GUI from '../../dist/lil-gui.esm.js';

export function autoGUI( object, gui = new GUI() ) {

	const controllers = {};

	for ( let prop in object ) {

		const val = object[ prop ];

		switch ( typeof val ) {

			case 'number':
			case 'string':
			case 'boolean':

				controllers[ prop ] = gui.add( object, prop );
				break;

			default:

				if ( val instanceof Directive ) {

					switch ( val.type ) {

						case 'range':
							object[ prop ] = val.initial;
							controllers[ prop ] = gui.add( object, prop, val.min, val.max );
							break;

						case 'options':
							object[ prop ] = val.options[ 0 ];
							controllers[ prop ] = gui.add( object, prop, val.options );
							break;

						case 'color':
							object[ prop ] = val.value;
							controllers[ prop ] = gui.addColor( object, prop );
							break;
					}

				} else if ( Object( val ) === val ) {

					const folder = gui.addFolder( prop ).close();
					object[ prop ] = autoGUI( val, folder ).object;

				}

				break;

		}

	}

	// Proxies allow us to intercept any assignment call to an object.
	// The default assignment behavior still applies, but if there's a controller
	// associated with a given property, it will update its display.
	const proxy = new Proxy( object, {
		set( _, prop, value ) {
			object[ prop ] = value;
			if ( prop in controllers ) {
				controllers[ prop ].updateDisplay();
			}
			return true;
		}
	} );

	return { object: proxy, gui };

}

export function options( ...options ) {
	return new Directive( { options, type: 'options' } );
}

export function color( value ) {
	return new Directive( { value, type: 'color' } );
}

export function range( initial, min, max ) {
	return new Directive( { initial, min, max, type: 'range' } );
}

// The Directive "class" allows us to distinguish between our controller
// directives and objects that should become folders. It just creates
// an object that will pass an `instanceof Directive` check.
function Directive( obj ) {
	Object.assign( this, obj );
}

In production, you could swap autoGUI.js for autoGUI.shim.js and the function would pass back the original object, untouched, along with an undefined GUI.

./autoGUI.shim.js
const pass = v => v;
const autoGUI = object => ( { object } );
export {
	autoGUI,
	pass as range,
	pass as color,
	pass as options
};