Debugfunction

bindDebugSchema

Describe values, controls, groups, and actions once, then bind them to Tweakpane.

Demo

{
  "amount": 0.42,
  "speed": 1.2,
  "enabled": true,
  "visible": false,
  "mode": "balanced",
  "cssColor": "#7c5cff",
  "hex": "#ffb454",
  "actions": 0,
  "tint": {
    "r": 0.3,
    "g": 0.7,
    "b": 1
  }
}
'use client'

import { bindDebugSchema } from '@joycostudio/xyz/debug'
import { useEffect, useRef, useState } from 'react'
import { Pane } from 'tweakpane'

import { DemoPanel } from '@/components/demos/demo-ui'
import { JsonOutput } from './json-output'
import { definitionFor, makeState } from './schema'

export default function PaneDemo() {
  const host = useRef<HTMLDivElement>(null)
  const [, render] = useState(0)
  const [state] = useState(() => makeState(() => render((value) => value + 1)))

  useEffect(() => {
    if (!host.current) return
    const pane = new Pane({ container: host.current, title: 'bindDebugSchema' })
    const bound = bindDebugSchema(
      pane,
      definitionFor(state, () => render((value) => value + 1)),
      {
        sectionName: 'bindDebugSchema',
        copyText: async () => undefined,
      }
    )
    return () => {
      bound.dispose()
      pane.dispose()
    }
  }, [state])

  const { tint, ...snapshot } = state
  return (
    <DemoPanel>
      <div className="grid min-w-0 items-start gap-gap sm:grid-cols-2">
        <div ref={host} className="debug-host min-w-0" />
        <JsonOutput value={{ ...snapshot, tint: { r: tint.r, g: tint.g, b: tint.b } }} />
      </div>
    </DemoPanel>
  )
}

Usage

import { bindDebugSchema, bool, property, slider } from '@joycostudio/xyz/debug'
import { Pane } from 'tweakpane'

const state = { speed: 1, enabled: true }
const pane = new Pane()
const controls = bindDebugSchema(pane, {
  schema: () => ({
    speed: slider(property(state, 'speed'), { min: 0, max: 3 }),
    enabled: bool(property(state, 'enabled')),
  }),
})

// Controls write to state. Refresh after changing state from code.
state.speed = 2
controls.refresh()
controls.values() // { speed: 2, enabled: true }

// When the owner is removed:
controls.dispose()
pane.dispose()

Binding

bindDebugSchema

bindDebugSchema(host: FolderApi, definition: DebugDefinition, options?: BindDebugOptions): BoundDebug

Creates controls in an existing Tweakpane pane or folder. Use this directly when you own the pane's lifecycle. DebugManager.bind() calls it internally when mounting a registered definition, so you do not need to call both.

ArgumentDescription
hostThe pane or folder that receives the controls. The caller owns and disposes this host.
definitionAn object containing the schema factory and optional behavior described below.
definition.schemaRequired factory returning a record of control fields.
definition.copy?Adds a copy-values button when true. Off when omitted.
definition.dispose?Cleanup function, disposable object, or array of either, run when the binding is disposed.
definition.extend?Receives the Tweakpane folder and { fields } (see fields() below) after the schema is bound, to add custom controls; may return cleanup.
definition.sources?Source descriptors with label: string and value: () => object, collected in controls.sources. The built-in copy button reads schema values instead.
options.sectionName?Wraps copied values under this name.
options.registerFrame?Receives a refresh callback to register with your frame loop and returns its teardown. Required for automatic updates of controls with live: true.
options.copyText?Receives serialized values and returns void or a promise. Defaults to navigator.clipboard.writeText.
options.onError?Receives (error, context) when a control, action, or copy operation fails. Defaults to logging to the console.

Returns a BoundDebug controller:

  • apply(): Writes the controls' current values to their sources, then refreshes.
  • refresh(): Pulls current source values into the controls, including nested folders.
  • values(): Reads the current nested value object, excluding buttons.
  • fields(): Lists every bound schema field, nested folders' fields included, in schema order. Each entry has path (schema keys from this definition's root, never labels), field (the bound DebugField), and blade (the BindingApi, FolderApi, or ButtonApi created for it). The list is fixed at bind time and excludes the copy button and anything added by extend.
  • sources: Collects declared DebugSource entries from this definition and its children.
  • dispose(): Removes created controls, nested folders, frame callbacks, and extension teardowns, and runs the definition's cleanup. The host remains owned by the caller.

References

ref

ref<T>(read: () => T, write: (value: T) => void): ValueRef<T>

Adapts getter and setter functions into a mutable reference.

ArgumentDescription
readReturns the current source value whenever the reference is read.
writeUpdates the source whenever the reference is assigned a value.
const state = { speed: 1 }
const speed = ref(
  () => state.speed,
  (value) => {
    state.speed = value
  }
)

property

property<TObject, TKey extends keyof TObject>(
  object: TObject,
  key: TKey,
  changed?: () => void
): ValueRef<TObject[TKey]>

Creates a typed reference to one object property.

ArgumentDescription
objectThe object to read from and write to.
keyA key of that object. The reference keeps the property's value type.
changed?A callback with no arguments, called after the reference writes to the property. Direct changes to the object do not trigger it.
const state = { speed: 1 }
const speed = property(state, 'speed', () => console.log(state.speed))

Number and boolean controls

Control helpers return a DebugField to place in a schema. Their optional label overrides the schema key displayed beside the control. live defaults to off; set it to true and provide registerFrame in the binding options to reflect source changes automatically.

slider

slider(store: ValueRef<number>, options: SliderOptions): DebugField

Creates one numeric slider.

ArgumentDescription
storeA numeric reference, usually from property or ref.
options.minRequired lower bound.
options.maxRequired upper bound.
options.step?Numeric increment. Omitted by default.
options.label?Display label.
options.live?Refresh from the source on registered frames.
const state = { speed: 1 }
slider(property(state, 'speed'), { min: 0, max: 3, step: 0.1, label: 'Speed' })

sliders

sliders(object, specs, changed?): DebugSchema

Creates several sliders for numeric properties of one object. Spread the result into your schema.

ArgumentDescription
objectThe object whose numeric properties will be edited.
specsA map of numeric property keys to SliderSpec tuples: [min, max, step?, label?]. Only listed keys get controls.
changed?A callback with no arguments, called after any generated control writes to the object.
const state = { speed: 1, intensity: 0.5 }
const schema = () => ({
  ...sliders(state, {
    speed: [0, 3, 0.1, 'Speed'],
    intensity: [0, 1],
  }),
})

bool

bool(store: ValueRef<boolean>, options?: { label?: string; live?: boolean }): DebugField

Creates one boolean toggle.

ArgumentDescription
storeA boolean reference.
options.label?Display label.
options.live?Refresh from the source on registered frames.
const state = { enabled: true }
bool(property(state, 'enabled'), { label: 'Enabled' })

toggles

toggles(object, specs, changed?): DebugSchema

Creates several toggles for boolean properties of one object. Spread the result into your schema.

ArgumentDescription
objectThe object whose boolean properties will be edited.
specsA map of boolean property keys to a label string or true. Use true to keep the property key as the label. Only listed keys get controls.
changed?A callback with no arguments, called after any generated control writes to the object.
const state = { enabled: true, wireframe: false }
const schema = () => ({
  ...toggles(state, { enabled: true, wireframe: 'Show wireframe' }),
})

Choices and colors

select

select<T extends string | number>(
  store: ValueRef<T>,
  options: Record<string, T>,
  config?: { label?: string; live?: boolean }
): DebugField

Creates a dropdown for string or number values.

ArgumentDescription
storeA reference to the selected value.
optionsA map from displayed option labels to stored values.
config.label?Display label for the control.
config.live?Refresh from the source on registered frames.
const state = { quality: 'high' }
select(property(state, 'quality'), { Low: 'low', High: 'high' }, { label: 'Quality' })

enumField

enumField(store, options, config?): DebugField

Alias of select, with the same arguments and behavior. Pass the selected-value reference, the label-to-value map, and optional label / live configuration.

const state = { quality: 'high' }
enumField(property(state, 'quality'), { Low: 'low', High: 'high' })

color

color<T extends RgbColor>(store: ValueRef<T>, options?: { label?: string; live?: boolean }): DebugField

Creates an RGB color control configured for floating-point channels. Edits call the existing object's setRGB method, preserving its identity.

ArgumentDescription
storeA reference to an object with r, g, and b numeric channels and a setRGB(r, g, b) method.
options.label?Display label.
options.live?Refresh from the source on registered frames.
const state = {
  tint: {
    r: 1,
    g: 0.5,
    b: 0,
    setRGB(r: number, g: number, b: number) {
      Object.assign(this, { r, g, b })
    },
  },
}
color(property(state, 'tint'), { label: 'Tint' })

colorString

colorString(store: ValueRef<string>, options?: { label?: string; live?: boolean }): DebugField

Creates a color picker backed by a CSS color string.

ArgumentDescription
storeA string reference containing a color value supported by Tweakpane, such as '#ff8800'.
options.label?Display label.
options.live?Refresh from the source on registered frames.
const state = { tint: '#ff8800' }
colorString(property(state, 'tint'), { label: 'Tint' })

hexColor

hexColor(store: ValueRef<string>, options?: { label?: string; live?: boolean }): DebugField

Alias of colorString, with the same string reference and optional label / live arguments. It does not add hex-only validation.

const state = { tint: '#ff8800' }
hexColor(property(state, 'tint'))

Structure and actions

folder

folder(definition: DebugDefinition & { title?: string; expanded?: boolean }): DebugField

Nests a debug definition inside the current schema. Also accepts an existing DebugFolder, such as a single definition returned by an object's debug() method or passed to DebugManager.bind().

ArgumentDescription
definitionThe nested schema factory and optional copy, extension, source, and cleanup behavior.
definition.title?Folder title. Defaults to the schema key.
definition.expanded?Initial expanded state. When omitted, uses Tweakpane's default.
const state = { speed: 1 }
const schema = () => ({
  motion: folder({
    title: 'Motion',
    expanded: true,
    schema: () => ({ speed: slider(property(state, 'speed'), { min: 0, max: 3 }) }),
  }),
})

section

section(schema: () => DebugSchema, options?: SectionOptions): DebugField

Creates a nested folder that is collapsed and includes a copy-values button by default.

ArgumentDescription
schemaFactory returning the nested fields.
options.label?Folder title. Defaults to the schema key.
options.expanded?Initial expanded state. Defaults to false.
options.copy?Show the copy-values button. Defaults to true.
options.dispose?Teardown to run when the section is disposed.
options.extend?Receives the section's Tweakpane folder and { fields } with paths relative to the section; may return a teardown.
options.sources?Source descriptors included in the bound controller's sources array.
const state = { speed: 1 }
const schema = () => ({
  motion: section(() => ({ speed: slider(property(state, 'speed'), { min: 0, max: 3 }) }), { label: 'Motion' }),
})

button

button(title: string, run: () => void | Promise<void>, feedback?: string): DebugField

Creates an action button. After the action completes successfully, the bound controls refresh from their sources.

ArgumentDescription
titleButton text.
runCallback invoked on click. May be synchronous or return a promise.
feedback?Temporary button text shown for 1.2 seconds after success.
const state = { speed: 1 }
button(
  'Reset speed',
  () => {
    state.speed = 1
  },
  'Reset ✓'
)

schemaValues

schemaValues(schema: DebugSchema): Record<string, unknown>

Reads current values without creating any Tweakpane controls.

ArgumentDescription
schemaThe field record to read. Nested folder factories are evaluated recursively, and buttons are omitted from the result.
const state = { speed: 1 }
schemaValues({ speed: slider(property(state, 'speed'), { min: 0, max: 3 }) })
// { speed: 1 }