@threlte/studio

Authoring Extensions

All Studio functionality is provided by Studio Extensions. To make the Studio suit your workflow, you may add your own Studio Extensions which may add toolbar items, custom panes, have access to the scene and may access the state of other extensions and run their actions.

While it’s technically possible to access and extend the Studio from anywhere within your app, it’s highly recommended to follow this pattern to easily remove the Studio including all extensions from your app in production.

Example

Let’s create a simple extension that increments and decrements a counter.

Extension Directory Structure

In its simplest form, an extension consists of a single Svelte file. If you’re using TypeScript, you can define the types for the extension state and actions in a separate file. Additionally, you can create a hook to interact with the extension from outside and define the public API.

extensions/
  my-extension/
    MyExtension.svelte
    (types.ts)
    (useMyExtension.ts)

(Optional) Type Definitions

This file provides type definitions for the extension state and actions.

types.ts
export const extensionScope = 'my-extension'

export type ExtensionState = {
  enabled: boolean
  count: number
}

export type ExtensionActions = {
  setEnabled: (enabled: boolean) => void
  toggleEnabled: () => void
  increment: () => void
  decrement: () => void
}

Svelte Extension File

This file implements the extension. Furthermore, it has full access to the Threlte app.

MyExtension.svelte
<script lang="ts">
  import {
    useStudio,
    ToolbarItem,
    HorizontalButtonGroup,
    ToolbarButton
  } from '@threlte/studio/extend'
  import { extensionScope, type ExtensionState, type ExtensionActions } from './types'

  const { createExtension } = useStudio()

  createExtension<ExtensionState, ExtensionActions>({
    scope: extensionScope,
    state({ persist }) {
      return {
        enabled: persist(true),
        count: persist(0)
      }
    },
    actions: {
      toggleEnabled({ state }) {
        state.enabled = !state.enabled
      },
      setEnabled({ state }, enabled) {
        state.enabled = enabled
      },
      increment({ state }) {
        state.count++
      },
      decrement({ state }) {
        state.count--
      }
    },
    keyMap({ shift }) {
      return {
        increment: shift('+'),
        decrement: shift('-')
      }
    }
  })
</script>

<!-- Extension UI -->
<ToolbarItem position="left">
  <HorizontalButtonGroup>
    <ToolbarButton
      label="Decrement"
      icon="mdiMinus"
      on:click={extension.decrement}
      tooltip="Decrement (-)"
    />
    <ToolbarButton
      label="Increment"
      icon="mdiPlus"
      on:click={extension.increment}
      tooltip="Increment (+)"
    />
  </HorizontalButtonGroup>
</ToolbarItem>

<slot />
Extensions must include a slot element to render the scene.

Let’s look at creating the extension step by step:

  1. Every extension is created using the createExtension function. If you’re using TypeScript, you should provide the types for the extension state and actions.
createExtension<ExtensionState, ExtensionActions>({
  1. An extension is registered with a unique scope (i.e. a string). This scope is used to identify the extensions state and actions.
  scope: extensionScope,
  1. The state function is used to define the initial state of the extension. The persist function is used to automatically persist the state of the extension across sessions.
  state({ persist }) {
    return {
      enabled: persist(true),
      count: persist(0)
    }
  },
  1. To mutate the state of the extension, you must define actions. Actions are functions that receive the extension state and can mutate it.
  actions: {
    toggleEnabled({ state }) {
      state.enabled = !state.enabled
    },
    setEnabled({ state }, enabled) {
      state.enabled = enabled
    },
    increment({ state }) {
      state.count++
    },
    decrement({ state }) {
      state.count--
    }
  },
  1. You can define key mappings for the extension. Key mappings are used to trigger actions when a key combination is pressed. Key modifiers (shift, alt, ctrl, meta) are provided as arguments to the keyMap function, and they can be combined. The key mappings are defined as an object where the keys are the action names and the values are the key combinations.
  keyMap({ shift }) {
    return {
      increment: shift('+')
      decrement: shift('-')
    }
  }

(Optional) Hook

This file exposes a hook to interact with the extension. It should be the only way to interact with the extension and defines the public API that can be used by other extensions.

useMyExtension.ts
import { useStudio } from '@threlte/studio/extend'
import { extensionScope, type ExtensionState, type ExtensionActions } from './types'

export const useMyExtension = () => {
  const { useExtension } = useStudio()

  const extension = useExtension<ExtensionState, ExtensionActions>(extensionScope)

  return {
    get enabled() {
      return extension.state.enabled
    },
    get count() {
      return extension.state.count
    },
    setEnabled: extension.setEnabled,
    toggleEnabled: extension.toggleEnabled,
    increment: extension.increment,
    decrement: extension.decrement
  }
}

Usage

Adding the extension to the Studio is as simple as importing the extension file and passing it to the Studio component.

<script lang="ts">
  import { Canvas } from '@threlte/core'
  import { Studio } from '@threlte/studio'
  import MyExtension from './MyExtension.svelte'
</script>

<Canvas>
  <Studio extensions={[MyExtension]}>
    <Scene />
  </Studio>
</Canvas>