threlte logo
Advanced

Custom Abstractions

A lot of the components you will find in the package @threlte/extras are abstractions on top of the <T> component. These abstractions provide extra functionality like automatically invalidating the frame or providing default values or extra props.

A common use case for custom abstractions is to create a component that is a fixed entity in your Threlte app which you want to reuse in multiple places. As an example, let’s create a component that is made up from multiple <T> components resembling a floor and a cube on top:

Tile.svelte
<script>
  import { T } from '@threlte/threlte'
  import { DEG2RAD } from 'three/src/math/MathUtils.js'
</script>

<T.Group>
  <!-- 1x1x1 Cube -->
  <T.Mesh
    position.y={0.5}
  >
    <T.BoxGeometry />
    <T.MeshStandardMaterial />
  </T.Mesh>

  <!-- 2x2 Floor -->
  <T.Mesh
    rotation.x={-90 * DEG2RAD}
  >
    <T.PlaneGeometry args={[2, 2]} />
    <T.MeshStandardMaterial />
  </T.Mesh>

  <!-- A slot to nest objects in -->
  <slot />
</T.Group>

Let’s see what implementing that component looks like:

Scene.svelte
<script>
  import Tile from './Tile.svelte'
</script>

<Tile />

Props

The <Tile> component is now available in the scene and can be reused as many times as you want. Now we’d like to assign a different position to the <Tile> component in order to move it around. We can do that by passing a position prop to the <Tile> component:

Scene.svelte
<script>
  import Tile from './Tile.svelte'
</script>

<Tile position={[0, 0, 0]} />
<Tile position={[2, 0, 0]} />
<Tile position={[4, 0, 0]} />

The component <Tile> internally needs to use the position prop in order to set the position of the <T> components. We can do that by spreading $$restProps on the <T.Group> component at the root hierarchy of <Tile>:

Tile.svelte
<script>
  import { T } from '@threlte/threlte'
  import { DEG2RAD } from 'three/src/math/MathUtils.js'
</script>

<T.Group {...$$restProps}>
  <!-- 1x1x1 Cube -->
  <T.Mesh
    position.y={0.5}
  >
    <T.BoxGeometry />
    <T.MeshStandardMaterial />
  </T.Mesh>

  <!-- 2x2 Floor -->
  <T.Mesh rotation.x={-90 * DEG2RAD}>
    <T.PlaneGeometry args={[2, 2]} />
    <T.MeshStandardMaterial />
  </T.Mesh>

  <!-- A slot to nest objects in -->
  <slot />
</T.Group>

Events

The following section assumes you use the plugin interactivity to listen to pointer events.

We successfully forwarded all props and are able to move the <Tile> component around in the scene. However, we may also need to listen to events on objects inside <Tile>. Threlte provides the utility function forwardEventHandlers to forward event handlers defined on <Tile> internally to our cubes <T.Mesh> component.

Tile.svelte
<script>
  import { T, forwardEventHandlers } from '@threlte/threlte'
  import { DEG2RAD } from 'three/src/math/MathUtils.js'

  const component = forwardEventHandlers()
</script>

<T.Group {...$$restProps}>
  <!-- 1x1x1 Cube -->
  <T.Mesh
    position.y={0.5}
    bind:this={$component}
  >
    <T.BoxGeometry />
    <T.MeshStandardMaterial />
  </T.Mesh>

  <!-- 2x2 Floor -->
  <T.Mesh rotation.x={-90 * DEG2RAD}>
    <T.PlaneGeometry args={[2, 2]} />
    <T.MeshStandardMaterial />
  </T.Mesh>

  <!-- A slot to nest objects in -->
  <slot />
</T.Group>

When we now add an on:click event handler to the <Tile> component, we can see that the event handler is called when we click on the floor:

Scene.svelte
<script>
  import Tile from './Tile.svelte'
</script>

<Tile
  on:click={() => {
    console.log('clicked')
  }}
/>

Types

The following section assumes you use TypeScript.

The last thing we need to do is to add types to our custom abstraction so that editors like VSCode can provide us with autocompletion and type checking. We will create a Tile.d.ts file next to the Tile.svelte file and add the following content:

Tile.d.ts
import type { Events, Props, Slots } from '@threlte/core'
import { SvelteComponent } from 'svelte'
import type { Group } from 'three'

export type TileProps = Props<Group> & {
  // Define extra props here.
}

export type TileEvents = Events<Mesh> & {
  // Define extra events here.
}

export type TileSlots = Slots<Mesh> & {
  // Define extra slots here.
}

export default class Tile extends SvelteComponent<TileProps, TileEvents, TileSlots> {}

Now we can use the <Tile> component in our scene and get autocompletion and type checking:

Scene.svelte
<script>
  import Tile from './Tile.svelte'
</script>

<!-- Autocompletion and type checking works here. -->
<Tile position={[0, 0, 0]} />

Let’s cross check the types inside our Tile.svelte file by implementing $$Props, $$Events and $$Slots. We immediately see that we forgot to pass the slot prop ref to our <slot />, let’s add that:

Tile.svelte
<script lang="ts">
  import { T, forwardEventHandlers } from '@threlte/threlte'
  import { DEG2RAD } from 'three/src/math/MathUtils.js'
  import type { TileProps, TileEvents, TileSlots } from './Tile.svelte'

  type $$Props = TileProps
  type $$Events = TileEvents
  type $$Slots = TileSlots

  const component = forwardEventHandlers()
</script>

<T.Group {...$$restProps} let:ref>
  <!-- 1x1x1 Cube -->
  <T.Mesh
    position.y={0.5}
    bind:this={$component}
  >
    <T.BoxGeometry />
    <T.MeshStandardMaterial />
  </T.Mesh>

  <!-- 2x2 Floor -->
  <T.Mesh rotation.x={-90 * DEG2RAD}>
    <T.PlaneGeometry args={[2, 2]} />
    <T.MeshStandardMaterial />
  </T.Mesh>

  <!-- A slot to nest objects in -->
  <slot {ref} />
</T.Group>