From lil-gui
import { GUI } from '@haxiomic/devui/compat'; // lil-gui API: add(obj, k, min, max, step), addColor, addFolder, .name(), .onChange()…
// haxiomic-engine/dev/DevUI.ts can become:
export { DevUI } from '@haxiomic/devui/three/devui';
Changing the import is usually all it takes, or alias lil-gui to @haxiomic/devui/compat in your bundler. What runs unchanged: add with min, max and step or options, addColor (hex strings and numbers, { r, g, b }, rgbScale), addFolder, name, onChange and onFinishChange (on a controller or a folder), listen (devui polls what's on screen anyway), disable, show/hide, open/close, reset, destroy, children, parent, root, and save()/load() in lil-gui's format. What's different:
- One panel. Every
new GUI()adds to devui's panel;new GUI({ title })makes a folder in it.width,containerandautoPlaceare accepted and ignored. Every element lil-gui hands out (a GUI's or a controller'sdomElement,$children,$name…) is a detached placeholder, so code that styles or moves one runs and changes nothing: an element appended to a folder's$childrendoesn't show, and the panel can't be drawn into a texture (three'sHTMLMesh). Move the panel with its own dock and float commands. - Values persist across reloads, as everywhere in devui. A restored value runs your
onChangeandonFinishChangeonce, after the code that attached them, so state built from it matches. - The same property twice is fine, as in lil-gui: three.js uniforms (
add(uniforms.a, 'value'),add(uniforms.b, 'value')) are two controls, saved asvalueandvalue 2, and twoaddFolder('Light')calls make two folders. - Ranges are optional: a number without min and max gets devui's inference rather than a bare box.
Moving to devui's own API
The compat layer runs lil-gui code as it is. Moving a panel to devui's own API is mostly mechanical, and TypeScript flags each call to change:
| lil-gui | devui |
|---|---|
folder.add(o, 'k', min, max, step).name('Label') | folder.add(o, 'k', { label: 'Label', min, max, step }) |
folder.add(o, 'k', { A: 1, B: 2 }), add(o, 'k', ['a', 'b']) | folder.add(o, 'k', { options: { A: 1, B: 2 } }) |
folder.addColor(o, 'k') | folder.add(o, 'k') (colours are inferred; a hex number needs color: true) |
folder.add(o, 'k').listen().disable() | folder.monitor(o, 'k') |
.listen(), .updateDisplay() | nothing: devui polls what's on screen |
folder.add({ go() {} }, 'go').name('Go') | folder.button('Go', () => …) |
gui.addFolder('X').close() | gui.folder('X', { collapsed: true }) |
controller.domElement.title = … | the description hint |
Two things behave differently:
- An unchanged value isn't written.
set(current)writes nothing and fires nothing, where lil-gui always assigned, so a setter with side effects doesn't run for "set it to what it is". - One label is one control. In devui's own API, a control's id defaults to its label, so two controls given the same label in a folder share one path, and the second rebinds the first. Give each its own
labelorid. (A control and a folder of one label both stay: the later one's path isGrid 2, and devui warns.)
demo/engine runs haxiomic-engine's PhysicallyBasedViewer unmodified, with its DevUI resolved to devui (run git submodule update --init first).