Adding a Click Handler to a Three.js Model
Tags: javascript, threejs, react
Published: 2020-12-11 15:59:01

---
title: Adding a Click Handler to a Three.js Model
published: true
description: I'm building a social media network and collaboration tool based on mind maps, documenting my work in this series of blog posts
tags: javascript,threejs,react
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/dybvdymmru80qxd8cgc4.gif
series: Nuffshell Coding Diary
---

*I'm building a social media network and collaboration tool based on mind maps, documenting my work in this series of blog posts. Follow me if you're interested in what I've learned along the way about building web apps with React, Tailwind CSS, Firebase, Apollo/GraphQL, three.js and TypeScript.*

# Today's Goal

After spent a good part of the week procrastinating by adding [Apollo State Management](https://dev.to/pahund/local-state-management-with-apollo-client-14lf) and [TypeScript](https://dev.to/pahund/typescript-all-the-things-2o4p) to my project, it's time to continue work on the actual mind map.

I want to start making the mind map interactive. As a first step, I want it, when a node is clicked, to zoom and pan to the clicked node, like this:

![Animation: clicking a mind map node](https://dev-to-uploads.s3.amazonaws.com/i/dybvdymmru80qxd8cgc4.gif)

# Picking a Library

Surprisingly, adding a click handler to an object in a three.js scene is not built in to the library. As was also the case with the [trackball controls](https://dev.to/pahund/adding-trackball-controls-to-a-three-js-scene-with-sprites-48hp) I've previously added, there are a couple of libraries available that add this functionality and work more or less the same.

After a bit of research, I settle for [three.interactive](https://github.com/markuslerner/THREE.Interactive#readme) by Markus Lerner, for these reasons:

* Available as npm package
* Has been updated recently
* Provides ES module imports
* Doesn't pollute the THREE namespace or define any global variables
* Supports handling clicks on overlapping objects

The only disadvantage is that it doesn't have TypeScript types. There was one other repo on GitHub for interactive three.js models that does provide types – [threejs-interactive-object](https://github.com/MasatoMakino/threejs-interactive-object). But that is not available as npm package, so I decided against using it.

# Logging on Click

To see if *threejs-interactive-object* does what it promises, I add the npm package to my library, then set it up to log a statement to the console when a mind map node is clicked.

For this, my [initializeScene](https://codesandbox.io/s/step-25-interactivity-6b43h?file=/src/features/mindMap/renderers/utils/initializeScene.ts) function now creates and returns an `InteractionManager`:

```typescript
function initializeScene(div: HTMLDivElement) {
  const canvas = createCanvas(window.innerWidth, window.innerHeight);
  const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
  div.appendChild(renderer.domElement);

  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0xffffff);

  const camera = new THREE.PerspectiveCamera(
    50,
    window.innerWidth / window.innerHeight,
    0.1,
    500000
  );
  camera.position.z = 1.5;

  const controls = new TrackballControls(camera, renderer.domElement);

  const interactionManager = new InteractionManager(renderer, camera, canvas);

  return { scene, renderer, camera, controls, interactionManager };
}
```

The interaction manager is then passed to the constructor of my `RenderCache`, which “pre-renders” the mind map nodes, i.e. prepare them before they are displayed in the 3D model:

*[renderMindMap.tsx](https://codesandbox.io/s/step-25-interactivity-6b43h?file=/src/features/mindMap/renderers/renderMindMap.tsx)*

```typescript
async function renderMindMap(
  div: HTMLDivElement,
  data: MindMapData
) {
  const {
    scene,
    renderer,
    camera,
    controls,
    interactionManager
  } = initializeScene(div);
  const renderCache = new RenderCache({ interactionManager });
  await renderCache.preRender(data);
  const graph = initializeGraph(renderCache, data);
  scene.add(graph);
  camera.lookAt(graph.position);
  animate(() => {
    graph.tickFrame();
    controls.update();
    interactionManager.update();
    renderer.render(scene, camera);
  });
}
```

Inside the `animation` function call at the end, I add `interactionManager.update()` to make sure the interaction manager updates with every animation loop iteration.

When the mind map node is rendered, a click event handler is added that, for now, just logs a statement so we know that it works:

*[RenderCache.tsx](https://codesandbox.io/s/step-25-interactivity-6b43h?file=/src/features/mindMap/renderers/RenderCache.tsx)*

```typescript
interface Constructor {
  interactionManager: typeof InteractionManager;
}

class RenderCache {
  private preRendered: Map<
    string | number | NodeObject | undefined,
    PreRendered
  > = new Map();

  private interationManager: typeof InteractionManager;

  constructor({ interactionManager }: Constructor) {
    this.interationManager = interactionManager;
  }

  preRender(data: MindMapData) {
    return Promise.all(
      data.nodes.map(async ({ name, val, id }) => {
        const sprite = await renderToSprite(
          <MindMapNode label={name} level={val} />,
          { width: 128, height: 64 }
        );
        sprite.addEventListener('click', (event) => {
          event.stopPropagation();
          return console.log(`Mind map node clicked: #${id} “${name}”`);
        });
        this.interationManager.add(sprite);
        const linkMaterial = new THREE.MeshBasicMaterial({
          color: colorsByLevel[val]
        });
        this.preRendered.set(id, { sprite, linkMaterial });
      })
    );
  }
}
```

Note the `event.stopPropagation()` – this makes sure, when mind maps nodes are overlapping, that a click only fires the event on the one closest to the camera.

Let's try it out:

{% youtube 2DJDTTJCxnk %}

👍🏻 Nice, it works!

Here's the code so far:

{% codesandbox 6b43h %}

# To Be Continued…

The next step will be to move the camera to put the mind map node that was clicked in the middle of the screen. I'll figure out how to do this in the next blog post.

I'm planning to turn my mind map into a social media network and collaboration tool and will continue to blog about my progress in follow-up articles. Stay tuned!