Advanced
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
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 DEV 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
Action for <T> components. Use it for
one-time setup logic that does not need access to the component’s props.
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>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
})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 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.