Advanced
Plugins
Plugins are one of Threlte’s superpowers. Plugins allow you to globally extend
Threlte’s <T>
component functionality. This means
you can not only add arbitrary props and event handlers to your <T>
components
but also override the default behavior of the <T>
component entirely.
Plugins add code to every <T>
component instance. That code acts as if it were
part of the <T>
component itself. You have full access to props, event
listeners and the component instance itself.
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 are
also affected by the plugin 'my-plugin'
-->
<OtherComponent />
What it looks like
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
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
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 { T, useTask } from '@threlte/core'
import { DEG2RAD } from 'three/src/math/MathUtils.js'
import { injectLookAtPlugin } from './lookAtPlugin.svelte'
const cubePos = $state([0, 0.8, 0]) as [number, number, number]
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={DEG2RAD * -90}
>
<T.CircleGeometry args={[4, 60]} />
<T.MeshStandardMaterial />
</T.Mesh>
<T.Mesh
position={cubePos}
receiveShadow
castShadow
rotation.x={DEG2RAD * -90}
>
<T.BoxGeometry />
<T.MeshStandardMaterial color="#FE3D00" />
</T.Mesh>
<T.Group
lookAt={cubePos}
position={[0, 4, 0]}
>
<T.Mesh
receiveShadow
castShadow
rotation.x={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
A Plugin that implements BVH raycasting on all child meshes and geometries.
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:
<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
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
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
})
<T>
Component Props
Typing the 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 soon 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.