UNPKG

agentscape

Version:

Agentscape is a library for creating agent-based simulations. It provides a simple API for defining agents and their behavior, and for defining the environment in which the agents interact. Agentscape is designed to be flexible and extensible, allowing

227 lines (184 loc) 7.37 kB
/** * This is responsible for creating the controls pane in the UI. */ import LocalStoreContext from '../runtime/ui/LocalStoreContext' export interface ControlVariableConfig { label: string name: string default: LocalStoreValue tooltip?: string validation?: (value: LocalStoreValue) => {isValid: boolean, message: string} options?: { label: string value: LocalStoreValue }[] } export interface ControlVariable extends ControlVariableConfig { type: 'number'|'string'|'boolean' } type LocalStoreValue = string | number | boolean export interface ControlsConstructor { root: HTMLDivElement settings: ControlVariableConfig[] id?: string title?: string } export default class Controls { private context: LocalStoreContext private settings: ControlVariable[] = [] constructor(opts: ControlsConstructor) { const { root, settings, title = 'Controls', id = '_controls_' } = opts this.context = new LocalStoreContext(id) // infer a type for each setting settings.forEach( setting => { if (typeof setting.default === 'string') { // setting.type = 'string' this.settings.push({type: 'string', ...setting}) } else if (typeof setting.default === 'number') { // setting.type = 'number' this.settings.push({type: 'number', ...setting}) } else if (typeof setting.default === 'boolean') { // setting.type = 'boolean' this.settings.push({type: 'boolean', ...setting}) } else { throw new Error(`Unknown type for setting ${setting.name}`) } }) if (this.context.length() === 0) { this.settings.forEach( setting => { this.context.set(setting.name, setting.default, setting.type) }) } const controlsPane = document.createElement('drag-pane') controlsPane.setAttribute('heading', title) controlsPane.setAttribute('key', 'controls') const controlsContainer = document.createElement('div') controlsContainer.style.display = 'flex' controlsContainer.style.flexDirection = 'column' controlsContainer.style.alignItems = 'flex-start' controlsContainer.style.padding = '5px' controlsContainer.style.width = 300 + 'px' const resetButton = document.createElement('button') resetButton.innerText = 'Reset Values' resetButton.style.margin = '5px' resetButton.addEventListener('click', () => { this.reset() window.location.reload() }) this.settings.forEach(setting => { const control = setting?.options && setting.options.length > 0 ? generateSelection(setting, this.context) : generateInput(setting, this.context) controlsContainer.appendChild(control) }) controlsContainer.appendChild(resetButton) controlsPane.appendChild(controlsContainer) root.appendChild(controlsPane) } public getSetting(name: string) { return this.context.get(name) } public setSetting(name: string, value: LocalStoreValue, type: ControlVariable['type']): void { this.context.set(name, value, type) } public reset(): void { this.context.clear() this.settings.forEach( setting => { this.context.set(setting.name, setting.default, setting.type) }) } } // generates select & option elements based on the options of the setting const generateSelection = (setting: ControlVariable, context: LocalStoreContext): HTMLDivElement => { // create a div to hold the input and label const wrapper = document.createElement('div') const select = document.createElement('select') select.style.margin = '1px 3px' select.style.width = '200px' setting.options?.forEach( option => { const optionElement = document.createElement('option') optionElement.value = option.value as string optionElement.innerText = option.label as string select.appendChild(optionElement) }) select.value = context.get(setting.name) as string select.id = setting.name const label = document.createElement('label') label.htmlFor = setting.name label.innerText = setting.label label.title = setting.tooltip || '' // underline the label if it has a tooltip if (setting.tooltip) { label.style.cursor = 'help' } wrapper.appendChild(label) wrapper.appendChild(select) wrapper.querySelector('select')!.addEventListener('input', (event) => { context.set(setting.name, (event.target as HTMLSelectElement).value, setting.type) }) return wrapper } // generates an input element based on the type of the setting const generateInput = (setting: ControlVariable, context: LocalStoreContext): HTMLDivElement => { // create a div to hold the input and label const wrapper = document.createElement('div') const input = document.createElement('input') input.style.margin = '1px 3px' input.style.width = '50px' // counts the number of decimal places in a float const numericalPrecision = typeof context.get(setting.name) === 'number' ? ((context.get(setting.name) as number).toString().split('.')[1] ?? []).length : null switch(setting.type) { case 'number': input.type = 'number' input.value = context.get(setting.name) as string if (numericalPrecision) { input.step = '0.' + '0'.repeat(numericalPrecision - 1) + '1' } else { input.step = '1' } break case 'string': input.type = 'text' input.value = context.get(setting.name) as string break case 'boolean': input.type = 'checkbox' input.checked = context.get(setting.name) as boolean break } input.id = setting.name const label = document.createElement('label') label.htmlFor = setting.name label.innerText = setting.label label.title = setting.tooltip || '' // underline the label if it has a tooltip if (setting.tooltip) { label.style.cursor = 'help' } wrapper.appendChild(label) wrapper.appendChild(input) wrapper.querySelector('input')!.addEventListener('input', (event) => { let newValue: LocalStoreValue let oldValue = context.get(setting.name) if ( setting.type === 'boolean' ) { newValue = (event.target as HTMLInputElement).checked } else { newValue = (event.target as HTMLInputElement).value } if (setting.validation) { const {isValid = true, message = ''} = setting.validation(newValue) ?? {} if (!isValid) { console.error(`Invalid value for ${setting.name}: ${message}`) context.set(setting.name, oldValue, setting.type) ;(event.target as HTMLInputElement).value = oldValue as string } else { context.set(setting.name, newValue, setting.type) } } else { context.set(setting.name, newValue, setting.type) } }) return wrapper }