How-to: Communicator ESM with Next.js and TypeScript

Updated guide for HOOPS Communicator 2025.1.0 and Next.js 15

Initial Setup

  1. Download HOOPS Communicator from https://manage.techsoft3d.com/ or our Developer zone if you are an existing partner.

  2. Create an empty directory for your project and open Visual Studio Code or your preferred IDE. (The rest of this tutorial will be based on Visual Studio Code.)

  3. Open a terminal and run npx create-next-app@latest.

Apply your desired configuration as shown above. Note that it is not recommended to use Turbopack with HOOPS Communicator. More info on configuration related to Next.js can be found in the Next.js documentation.

cd to the new <Next_App> directory created (hc_2025_next in this case) and run npm run dev to start the development server to view the app and verify successful installation.

  1. Create a directory under <Next_App>/app called hoops.

  2. Copy the contents of <Communicator_Install_Directory>/web_viewer/@hoops to this directory.

Note: we are using the monolith version of the web viewer here for simplicity. The non-monolithic version will require additional configuration at this time, which we will not cover in this guide.

Communicator Integration

Create a viewer component

  1. Copy microengine.scs from <Communicator>/quick_start/converted_models/standard/scs to a new directory called <Next_App>/public/models.

  2. Create a directory in <Next_App>/app called components.

  3. Create Viewer.tsx here and create an empty basic component:

export default function Viewer() {
   return (
     <>
     </>
    )
}
  1. At the top of the file, add the 'use client' directive to support the web viewer and React functionalities. Also, include the module imports for this basic demo:
import * as Communicator from '../hoops/web-viewer'
import setupMonolith from '../hoops/web-viewer-monolith/setupMonolith';
import { useState, useEffect } from 'react';
  1. Next, set up the monolith as follows:
const engine = setupMonolith();
Communicator.WebViewer.defaultEngineBinary = engine.binary;
  1. Within the Viewer function, create a variable hwv for the viewer and a container (div element) for the viewer:
let hwv : Communicator.WebViewer;

return (
    <>
	<div id="viewer"></div>              
    </>    
 )

(If you’d like to confirm your setup, scroll to the bottom for the completed files.)

  1. Above the return in that function, add a startViewer function:
    function startViewer(container: string, model: string) : Communicator.WebViewer {
        hwv = new Communicator.WebViewer({
            containerId: container,
            endpointUri: model
        });

        window.onresize = () => {
            hwv.resizeCanvas();
        }

        hwv.start();

        return hwv;
    }
  1. We’ll want this function executed once the page has rendered, so we can use useEffect to call it. Add this before everything else you’ve defined in the Viewer function:
useEffect(() => {
    let hwv = startViewer("viewer", "./models/microengine.scs");
       
    return () => {
        hwv.shutdown();
    }
}, [])

Note the use of the cleanup function (see https://react.dev/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development) for info).

Render the component

  1. Delete all contents in <Next_App>/app/page.js and replace with the following so just the Viewer component is rendered:
'use client'
import dynamic from "next/dynamic";

const Viewer = dynamic(()=> import('./components/Viewer'), {
  ssr: false
});

export default function Home() {
  return (
     <Viewer />
  )
}

Note that we dynamically import the Communicator “Viewer” component and disable server side rendering for it because Communicator depends on the browser API.

  1. Check your app by running npm run dev:

nextjs_ts1

Using State

Next, we will create a simple demo of using State to change what’s on the web page. Specifically, we will display the node ID of the most recently clicked node in the lower right corner of the screen:

  1. In Viewer.tsx in the Viewer function, add the following before the useEffect:
const [nodeId, setNodeId] = useState<number | null>(null);
  1. In the startViewer function, add a callback to set the nodeId state to the most recently clicked node. Add the following immediately before hwv.start():
        hwv.setCallbacks({
            selectionArray: (selEvents : NodeSelectionEvent[])=>{
                if (selEvents.length > 0){
                    const lastSelectedNode = selEvents[0].getSelection().getNodeId();
                    setNodeId(lastSelectedNode);
                }
                else {
                    setNodeId(null);
                }
            }
        });
  1. Finally, add the HTML elements to display the nodeId by adding the following in the return statement, just after the viewer div:
            <div id="nodeDisplay" style={{position: "absolute", bottom: "10px", right: "10px", fontSize: "18px", padding: "10px"}}>
                <p id="nodeDisplayText">{"Node ID: " + nodeId}</p>
            </div>
  1. Run npm run dev and notice the “Node ID” display changes based on the last node that’s clicked.

nextjs_ts2

Summary

Directory structure:

Viewer.tsx

'use client'
import * as Communicator from '../hoops/web-viewer'
import setupMonolith from '../hoops/web-viewer-monolith/setupMonolith';
import { useState, useEffect } from 'react';
import { NodeSelectionEvent } from '../hoops/web-viewer/lib/event';

const engine = setupMonolith();
Communicator.WebViewer.defaultEngineBinary = engine.binary;

export default function Viewer() {

    const [nodeId, setNodeId] = useState<number | null>(null);

    useEffect(() => {
        let hwv = startViewer("viewer", "./models/microengine.scs");
           
        return () => {
            hwv.shutdown();
        }
    }, [])

    let hwv : Communicator.WebViewer;

    function startViewer(container: string, model: string) : Communicator.WebViewer {
        hwv = new Communicator.WebViewer({
            containerId: container,
            endpointUri: model
        });

        window.onresize = ()=> {
            hwv.resizeCanvas();
        }

        hwv.setCallbacks({
            selectionArray: (selEvents : NodeSelectionEvent[])=>{
                if (selEvents.length > 0){
                    const lastSelectedNode = selEvents[0].getSelection().getNodeId();
                    setNodeId(lastSelectedNode);
                }
                else {
                    setNodeId(null);
                }
            }
        });

        hwv.start();

        return hwv;
    }

    return (
        <>
        <div id="viewer"></div>
        <div id="nodeDisplay" style={{position: "absolute", bottom: "10px", right: "10px", fontSize: "18px", padding: "10px"}}>
            <p id="nodeDisplayText">{"Node ID: " + nodeId}</p>
        </div>
        </>
    )
}
1 Like