tweakpane icon indicating copy to clipboard operation
tweakpane copied to clipboard

[feature request] reset to default button

Open braebo opened this issue 2 years ago • 2 comments

Hey! I've often found myself wishing there was a reset button. I've implemented one myself, but perhaps it could be a handy feature?

ezgif com-video-to-gif

braebo avatar Mar 06 '23 18:03 braebo

Hi @braebo I would love to see an example how you manage to add a reset button!

whitespacecode avatar Jun 06 '24 10:06 whitespacecode

Hey @whitespacecode — here is the function that adds the reset to default button.

createResetButton
import type { InputBindingApi, ListApi } from 'tweakpane'

import { Color, Vector3 } from 'three'
import { ButtonApi } from 'tweakpane'

/**
 * Adds a button that resets the value of an input to its default value when clicked.
 */
export function createResetButton(
	input: InputBindingApi<unknown, unknown> | ListApi<any> | ButtonApi,
	key: string,
	sourceObj: Record<string, any>,
	el?: HTMLElement,
	callback?: () => void,
) {
	if (!input) return

	const defaultButton = createDefaultButton()

	const targetValue = sourceObj[key] ?? sourceObj['defaults'][key]

	if (!exists(targetValue)) throw new Error(`Key "${String(key)}" not found in source object.`)

	//* We can simply overwrite numbers and booleans.
	if (typeof targetValue === 'number' || typeof targetValue === 'boolean') {
		const defaultValue = JSON.parse(JSON.stringify(targetValue))
		addReset(defaultValue)
	}

	//* Color instances will need to go through the setter.
	if (isColor(targetValue)) {
		const defaultColor = new Color().fromArray(targetValue.toArray())
		addReset(defaultColor)
	}

	//* Vectors are a bit more finicky.
	if (isVector3(targetValue)) {
		const defaultVector = new Vector3().copy(targetValue)

		if (typeof defaultVector === 'undefined') {
			console.error({ key, sourceObj })
			throw new Error(
				`createResetToDefaultButton() - Key "${String(key)}" not found in source object.`,
			)
		}

		addReset(defaultVector)
	}

	//* Check for lists.
	if ('options' in input) {
		const options = input.options

		if (options.length) {
			const defaultOption = options.find((option) => option.value === targetValue)

			if (defaultOption) {
				defaultButton.addEventListener('click', () => {
					sourceObj[key] = defaultOption.value

					// Update the select text - https://github.com/cocopon/tweakpane/issues/547
					const select = input.element.getElementsByTagName('select')?.[0]
					if (select) {
						const index = options.findIndex(
							(option) => option.value === defaultOption.value,
						)
						select.selectedIndex = index
					}

					reset(defaultOption)
					resetGui()
				})
			}
		}
	}

	//* Mount the button.
	const inputEl = el ?? input.element
	inputEl.appendChild(defaultButton)

	function addReset(value: any) {
		defaultButton.addEventListener('click', () => {
			reset(value)
			resetGui()
		})
	}

	function reset(value: any) {
		if (callback) {
			callback()
			resetGui()
		} else {
			sourceObj[key] = value
		}
	}

	function resetGui() {
		setTimeout(() => {
			defaultButton.style.color = '#333'
			if ('refresh' in input) input.refresh()
		}, 10)
	}

	function createDefaultButton() {
		const btn = document.createElement('div')

		btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="12px" height="12px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M3 2v6h6" /><path d="M21 12A9 9 0 0 0 6 5.3L3 8" /><path d="M21 22v-6h-6" /><path d="M3 12a9 9 0 0 0 15 6.7l3-2.7" /></svg>`
		btn.style.display = 'flex'
		btn.style.alignItems = 'center'
		btn.style.justifyContent = 'center'

		btn.style.width = '1rem'
		btn.style.height = '1rem'
		btn.style.margin = 'auto'

		btn.style.color = '#333'
		btn.style.cursor = 'pointer'

		btn.style.transform = 'translateX(0.1rem)'
		btn.style.userSelect = 'none'
		btn.title = 'Reset to default'

		if (input instanceof ButtonApi) {
			input.on('click', () => {
				btn.style.color = '#aaa'
			})
		} else {
			input.on('change', () => {
				btn.style.color = '#aaa'
			})
		}

		return btn
	}
}

function exists<T>(value: T | undefined): value is T {
	return typeof value !== 'undefined'
}

function isVector3(v: any): v is Vector3 {
	return typeof v === 'object' && 'isVector3' in v
}

function isColor(v: any): v is Color {
	return v instanceof Color
}

braebo avatar Jun 16 '24 18:06 braebo