Plugins
Plugins allow you to extend Threlte’s <T>
component. They can be used to add props, event handlers, custom logic and
customize the component instance. You can think of a plugin as code that
is injected into every child <T> component.
Plugins can be overridden in child components.
The interactivity plugin in
@threlte/extras is an example of what a
plugins can do and there are a couple of other examples below.
When to use a plugin
Section titled “When to use a plugin”A plugin has access to all props and the lifecycle of the <T> component.
Use it to:
- add custom props to the
<T>component such aslookAt. - add custom logic to the
<T>component, such as automatically add helpers for certain objects in development mode. - collect object references from child components for app-wide systems such as an ECS.
- build custom integrations for external libraries.
When to use oncreate
<T>’s oncreate shares some
similarities to a plugin but only has access to the object referenced by the
<T> component. Also, it has to be defined on every <T> component
individually.
You can think of oncreate as a Svelte
Attachment for <T> components. Use it for
one-time setup logic that does not need access to the component’s props.
Injecting a plugin
Section titled “Injecting a plugin”Plugins are injected to a plugin context and are accessible to all child
<T> components.
<script> import { injectPlugin } from '@threlte/core' import myPlugin from './myPlugin' import OtherComponent from './OtherComponent.svelte'
injectPlugin('my-plugin', myPlugin)</script>
<!--This component is affected by the plugin 'my-plugin'--><T.Mesh />
<!--<T> components in this component arealso affected by the plugin 'my-plugin'--><OtherComponent />Plugin internals
Section titled “Plugin internals”Plugins open up the component <T> to external code that will be injected via
context into every child instance of a <T> component. The callback function
receives a reactive args object that contains the ref of the respective
<T> component, all base props (makeDefault, args, attach, manual,
makeDefault and dispose) and all props (anything else) passed to it.
import { injectPlugin } from '@threlte/core'
injectPlugin('plugin-name', (args) => { console.log(args.ref) // e.g. a Mesh console.log(args.props) // e.g. { position: [0, 10, 0] }})If a plugin decides via args.ref or args.props analysis that it doesn’t need
to act in the context of a certain <T> component, it can return early.
import { injectPlugin, isInstanceOf } from '@threlte/core'
injectPlugin('raycast-plugin', (args) => { if (!isInstanceOf(args.ref, 'Object3D') || !('raycast' in args.props)) return})The code of a plugin acts as if it would be part of the <T> component
itself and has access to all properties. A plugin can run arbitrary code in
lifecycle functions such as onMount, onDestroy and effects.
import { injectPlugin } from '@threlte/core'import { onMount } from 'svelte'
injectPlugin('plugin-name', (args) => { // Use lifecycle hooks as if it would run inside a <T> component. // This code runs when the `<T>` component this plugin is injected // into is mounted. onMount(() => { console.log('onMount') })
// Use any prop that is defined on the <T> component, in this // example `count`: <T.Mesh count={10} /> const count = $derived(args.props.count ?? 0)
$effect(() => { // This code runs whenever count changes. console.log(count) })
return { // Claiming the property "count" so that the <T> component // does not act on it. pluginProps: ['count'] }})A Plugin can also claim properties so that the component <T> does not act on it.
import { injectPlugin } from '@threlte/core'
injectPlugin('ecs', () => { return { // Without claiming the properties, <T> would apply the // property to the object. pluginProps: ['entity', 'health', 'velocity', 'position'] }})Plugins are passed down by context and can be overridden to prevent the effects of a plugin for a certain tree.
import { injectPlugin } from '@threlte/core'
// this overrides the plugin with the name "plugin-name" for all child components.injectPlugin('plugin-name', () => {})Creating a plugin
Section titled “Creating a plugin”Plugins can also be created for external consumption. This creates a named plugin. The name is used to identify the plugin and to override it.
import { createPlugin } from '@threlte/core'
export const layersPlugin = createPlugin('layers', () => { // ... Plugin Code})// somewhere else, e.g. in a component
import { injectPlugin } from '@threlte/core'import { layersPlugin } from '$plugins'
injectPlugin(layersPlugin)Examples
Section titled “Examples”lookAt
Section titled “lookAt”This is en example implementation that adds the property lookAt to all <T> components, so that <T.Mesh lookAt={[0, 10, 0]} /> is possible:
<script lang="ts"> import { Canvas } from '@threlte/core' import Scene from './Scene.svelte'</script>
<div> <Canvas> <Scene /> </Canvas></div>
<style> div { height: 100%; }</style><script lang="ts"> import { MathUtils } from 'three' import { T, useTask } from '@threlte/core' import { injectLookAtPlugin } from './lookAtPlugin.svelte'
const cubePos = $state<[number, number, number]>([0, 0.8, 0])
useTask(() => { cubePos[0] = Math.sin(Date.now() / 1000) * 2 cubePos[2] = Math.cos(Date.now() / 1000) * 2 })
injectLookAtPlugin()</script>
<T.OrthographicCamera zoom={80} position={[0, 5, 10]} makeDefault lookAt={[0, 2, 0]}/>
<T.Mesh receiveShadow rotation.x={MathUtils.DEG2RAD * -90}> <T.CircleGeometry args={[4, 60]} /> <T.MeshStandardMaterial /></T.Mesh>
<T.Mesh position={cubePos} receiveShadow castShadow rotation.x={MathUtils.DEG2RAD * -90}> <T.BoxGeometry /> <T.MeshStandardMaterial color="#FE3D00" /></T.Mesh>
<T.Group lookAt={cubePos} position={[0, 4, 0]}> <T.Mesh receiveShadow castShadow rotation.x={MathUtils.DEG2RAD * 90} > <T.ConeGeometry args={[1, 2]} /> <T.MeshStandardMaterial color="#FE3D00" flatShading /> </T.Mesh></T.Group>
<T.DirectionalLight position={[-3, 20, -10]} intensity={1} castShadow/><T.AmbientLight intensity={0.2} />import { injectPlugin, isInstanceOf, useThrelte } from '@threlte/core'
export const injectLookAtPlugin = () => { injectPlugin<{ lookAt: [number, number, number] }>('lookAt', (args) => { // skip injection if ref is not an Object3D if (!isInstanceOf(args.ref, 'Object3D')) return
// get the invalidate function from the useThrelte hook const { invalidate } = useThrelte()
$effect(() => { if (!args.props.lookAt) return args.ref.lookAt(args.props.lookAt[0], args.props.lookAt[1], args.props.lookAt[2]) invalidate() })
return { pluginProps: ['lookAt'] } })}BVH raycast plugin
Section titled “BVH raycast plugin”A Plugin that implements BVH raycasting on all child meshes and geometries.
This plugin outlines the basic setup of the extras bvh plugin.
import { injectPlugin, isInstanceOf } from '@threlte/core'import type { BufferGeometry, Mesh } from 'three'import { computeBoundsTree, disposeBoundsTree, acceleratedRaycast } from 'three-mesh-bvh'
const bvhRaycasting = () => { injectPlugin('bvh-raycast', (args) => { $effect(() => { if (isInstanceOf(args.ref, 'BufferGeometry')) { args.ref.computeBoundsTree = computeBoundsTree args.ref.disposeBoundsTree = disposeBoundsTree args.ref.computeBoundsTree() } if (isInstanceOf(args.ref, 'Mesh')) { args.ref.raycast = acceleratedRaycast } return () => { if (isInstanceOf(args.ref, 'BufferGeometry')) { args.ref.disposeBoundsTree() } } }) })}Implementing this plugin in your scene looks like this.
<script lang="ts"> import { T } from '@threlte/core' import bvhRaycasting from './plugins/bvhRaycasting.svelte'
bvhRaycasting()</script>
<T.Mesh> <T.MeshBasicMaterial /> <T.BoxGeometry /></T.Mesh>TypeScript
Section titled “TypeScript”Using TypeScript, we can achieve end-to-end type safety for plugins, from
the plugin implementation to the props of the <T> component. The example below
shows how to type the props of the lookAt plugin so that the prop
lookAt is strictly typed on the <T> component as well as in the plugin
implementation.
Typing a plugin
Section titled “Typing a plugin”The function injectPlugin accepts a type argument that you may use to type the
props passed to a plugin.
injectPlugin<{ lookAt?: [number, number, number] }>('lookAt', (args) => { // args.props.lookAt is now typed as [number, number, number] | undefined})Typing the <T> component props
Section titled “Typing the <T> component props”By default, the custom props of plugins are not present on the types of the
<T> component. You can however extend the types of the <T> component by
defining the Threlte.UserProps type in your ambient type definitions. In a
typical SvelteKit application, you can find these type definitions in
src/app.d.ts.
declare global { namespace App { // interface Error {} // interface Locals {} // interface PageData {} // interface PageState {} // interface Platform {} }
namespace Threlte { interface UserProps { lookAt?: [number, number, number] } }}
export {}The prop lookAt is now available on the <T> component and is typed as
[number, number, number] | undefined.
<script lang="ts"> import { T } from '@threlte/core'</script>
<!-- This is now type safe --><T.Mesh lookAt={[0, 10, 0]} />
<!-- This will throw an error --><T.Mesh lookAt="this object please" />As your app grows in size, you should consider moving these type these type definitions to a
separate file and merge all available props to a single type definition. This type may then be
used by injectPlugin as well as your ambient type defintions.