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
text/typescript
/**
* 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
}