@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.
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.
<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 />
Let’s look at creating the extension step by step:
- 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>({
- 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,
- The
state
function is used to define the initial state of the extension. Thepersist
function is used to automatically persist the state of the extension across sessions.
state({ persist }) {
return {
enabled: persist(true),
count: persist(0)
}
},
- 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--
}
},
- 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 thekeyMap
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.
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>