threlte logo
@threlte/extras

<Resize>

webgl <Resize> works with WebGL and is available from the @threlte/extras export. webgpu <Resize> works with WebGPU and is available from the @threlte/extras/webgpu export.

Scales up or down a group of objects by the maximum dimension of their bounding box. Object proportions are preserved. This is particularly useful if you want to “normalize” the dimensions of an object to be in the range 0 to 1.

<script lang="ts">
  import { Checkbox, Pane } from 'svelte-tweakpane-ui'
  import Scene from './Scene.svelte'
  import { Canvas } from '@threlte/core'
  import { NoToneMapping } from 'three'

  let showCylinder = $state(true)
  let auto = $state(true)
</script>

<Pane
  title="Resize"
  position="fixed"
>
  <Checkbox
    label="Show Cylinder"
    bind:value={showCylinder}
  />
  <Checkbox
    label="Auto"
    bind:value={auto}
  />
</Pane>

<div>
  <Canvas toneMapping={NoToneMapping}>
    <Scene
      {showCylinder}
      {auto}
    />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { T, useStage, useThrelte } from '@threlte/core'
  import { Align, Edges, Grid, MeshDiscardMaterial, OrbitControls, Resize } from '@threlte/extras'

  let {
    showCylinder,
    auto
  }: {
    showCylinder: boolean
    auto: boolean
  } = $props()

  // Create the stages for resizing and aligning
  const { renderStage, mainStage } = useThrelte()
  // Resizing must happen *before* aligning, so we need to create a new stage to orchestrate this
  const resizeStage = useStage(Symbol('resize'), { after: mainStage, before: renderStage })
  // Aligning must happen *after* resizing, to take the new size into account
  const alignStage = useStage(Symbol('align'), { after: resizeStage, before: renderStage })
</script>

<T.PerspectiveCamera
  makeDefault
  position={5}
>
  <OrbitControls />
</T.PerspectiveCamera>

<T.DirectionalLight
  position={[5, 10, 4]}
  intensity={Math.PI}
/>
<T.AmbientLight intensity={0.2} />

<Grid
  cellColor="#1F3153"
  cellSize={0.5}
  sectionColor="#1F3153"
  sectionThickness={3}
/>

<T.Mesh position.y={0.5}>
  <MeshDiscardMaterial />
  <T.BoxGeometry />
  <Edges color="white" />
</T.Mesh>

<Align
  stage={alignStage}
  y={1}
  auto
>
  <Resize
    stage={resizeStage}
    {auto}
  >
    <T.Mesh>
      <T.MeshStandardMaterial color="hotpink" />
      <T.BoxGeometry />
    </T.Mesh>
    <T.Mesh position.y={1.5}>
      <T.MeshStandardMaterial color="cyan" />
      <T.SphereGeometry />
    </T.Mesh>
    {#if showCylinder}
      <T.Mesh position.y={3}>
        <T.MeshStandardMaterial color="yellow" />
        <T.CylinderGeometry args={[1, 1.5, 1]} />
      </T.Mesh>
    {/if}
  </Resize>
</Align>

Choosing the Axis

You can choose the axis by providing the axis prop, otherwise the maximum axis is used.

<Resize axis="x">
  <T.Mesh />
</Resize>

If you use the axis prop, the dimensions will not be normalized to the range 0 to 1 unless the maximum axis is chosen.

Bring Your Own Box

Use the box prop to provide your own Box3 instance. Doing so allows you to capture the bounding box of the objects prior to any scaling

<script>
  import { Box3 } from 'three'
  const box = new Box3()
</script>

<Resize {box}>
  <T.Mesh />
</Resize>

<T.Box3Helper args={[box]} />

Normalizing Multiple Objects

If you have a bunch of source objects that are all different sizes, a useful technique is to wrap each one in a <Resize>. Once they’ve all been normalized, it may be easier to reason about their relative sizes.

<script lang="ts">
  import Scene from './Scene.svelte'
  import { Canvas } from '@threlte/core'
  import { Checkbox, Pane } from 'svelte-tweakpane-ui'

  let resize = $state(true)
</script>

<Pane
  position="fixed"
  title="objects"
>
  <Checkbox
    bind:value={resize}
    label="resize"
  />
</Pane>

<div>
  <Canvas>
    <Scene {resize} />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { OrbitControls, Resize, useGltf } from '@threlte/extras'
  import { T } from '@threlte/core'

  type SceneProps = {
    resize?: boolean
  }

  let { resize = true }: SceneProps = $props()

  const names = ['Duck', 'Flower', 'Fox']

  const promises = Promise.all(names.map((name) => useGltf(`/models/${name}.glb`)))

  const increment = (2 * Math.PI) / names.length
</script>

<T.PerspectiveCamera
  makeDefault
  position={[5, 5, 5]}
>
  <OrbitControls />
</T.PerspectiveCamera>

<T.AmbientLight intensity={0.2} />
<T.DirectionalLight position={[1, 5, 3]} />

{#await promises then objects}
  {#each objects as { scene }, i}
    {@const r = increment * i}
    <T.Group
      position.x={Math.cos(r)}
      position.z={Math.sin(r)}
    >
      {#if resize}
        <Resize>
          <T is={scene} />
        </Resize>
      {:else}
        <T is={scene} />
      {/if}
    </T.Group>
  {/each}
{/await}

In the example above, the fox model is much larger than the duck and flower models. It is so much larger that you may have to pull the camera back in order to see it. Using <Resize> scales each model so that each one fits inside a unit cube. From there, their relative sizes and positions may be easier to work with.

Manual Resizing

Using the Export

You can manually trigger a resize by calling the resize export.

Scene.svelte
<script>
  import { Resize } from '@threlte/extras'

  let ref = $state<Resize>()

  // ... later on
  ref?.resize()
</script>

<Resize bind:this={ref}>
  <!-- ... -->
</Resize>

Using the Snippet Argument

You can also use the snippet argument to trigger a resize.

Scene.svelte
<script>
  import { Resize } from '@threlte/extras'
</script>

<Resize>
  {#snippet children({ resize })}
    <T.Mesh oncreate={resize} />
  {/snippet}
</Resize>

Component Signature

<Resize> extends < T . Group > and supports all its props, snippets, bindings and events.

Props

name
type
required
default
description

auto
boolean
no
false
Resizes again whenever a child <T> component mounts or unmounts.

axis
'x' | 'y' | 'z'
no
the axis the children are largest along
The axis scaled to 1 unit. The children are scaled uniformly, so the other axes keep their proportions.

box
Box3
no
a Box3 shared by every <Resize>
The Box3 the children's bounding box is written to. Pass your own to read the measured box.

precise
boolean
no
false
Computes the bounding box from every vertex instead of each geometry's bounding box. More accurate for rotated objects, but slower.

stage
Stage
no
useStage('<Resize>', { before: renderStage })
The stage the resizing runs in. Resizing happens at most once per frame.

Events

name
payload
description

onresize
void
Called after every resize.

Exports

name
type
description

resize
() => void
Manually trigger a resize.