Search code examples
reactjsweb-componentclojurescriptreagentre-frame

Integrating Google's <model-viewer> with React/Reagent


Google's <model-viewer> provides all the key features I need without having write a custom solution via something like react-three-fiber or directly in three.js.

I am struggling with how to properly integrate it into a Reagent (and React for that matter) structure.

In order to make it easy to use with vanilla JS, is built as a web component and is largely controlled via the attributes on its html element. Normally that wouldn't be much of a problem, but with the overhead of 3D and loading large models re-rendering this is expensive and in many cases functionality-breaking.

I've tried to naively use it inside a component and trying to eliminate the possibility of re-rendering. Using a ref to mutate it directly.

I have also tried setting it up as a manually created html element from the Reagent/React controlled application and reference it in various events via its dom element.

Both of these options introduced a lot of hacks and were not ideal in a single page application.

I am wondering if anyone has any tips on how to best wrap this in a React/Reagent shell, while still having access to the core element to use their underlying JS api.

Answers don't have to be in ClojureScript.


Here is the example of its usage from their page:

<model-viewer 
  alt="Neil Armstrong's Spacesuit from the Smithsonian Digitization Programs Office and National Air and Space Museum" 
  src="shared-assets/models/NeilArmstrong.glb" 
  ar ar-modes="webxr scene-viewer quick-look" environment-image="shared-assets/environments/moon_1k.hdr" 
  poster="shared-assets/models/NeilArmstrong.webp" 
  seamless-poster 
  shadow-intensity="1" 
  camera-controls>
</model-viewer>

Thanks for any tips.


Additional discussion on Clojurians Slack (link requires access to Slack)

Additional discussion on Clojureverse


Solution

    1. Include it in your index.html as a module type script.
    <!DOCTYPE html>
    <html lang="en">
      <head>
    ....
        <script
          type="module"
          src="https://unpkg.com/@google/model-viewer/dist/model-viewer.min.js"
        ></script>
    ...
    
      </head>
    
      ...
    </html>
    
    1. use it as an element in your react component
        import "./styles.css";
        import React, { useState } from "react";
        
        export default function App() {
          const modelRef = React.useRef();
        
          return (
            <model-viewer
              // className="model-viewer"
              src="./M08.glb"
              alt="A rock"
              exposure="0.008"
              camera-controls
              ar
              ar-modes="webxr"
              ref={(ref) => {
                modelRef.current = ref;
              }}
            >
            </model-viewer>
          );
        }
    
    1. interact with it

    here is an interaction, where an onclick event translates screen coordinates to model coordinates using a function available from the model. It stores these annotations and renders them as children of the model. This is available in the documentation

    import "./styles.css";
    import React, { useState } from "react";
    
    export default function App() {
      const modelRef = React.useRef();
      const [annots, setAnnots] = useState([]);
    
      const handleClick = (event) => {
        const { clientX, clientY } = event;
    
        if (modelRef.current) {
          let hit = modelRef.current.positionAndNormalFromPoint(clientX, clientY);
          if (hit) {
            setAnnots((annots) => {
              return [...annots, hit];
            });
          }
        }
      };
    
      const getDataPosition = (annot) => {
        return `${annot.position.x} ${annot.position.y} ${annot.position.z}`;
      };
    
      const getDataNormal = (annot) => {
        return `${annot.normal.x} ${annot.normal.y} ${annot.normal.z}`;
      };
    
      return (
        <model-viewer
          // className="model-viewer"
          src="./M08.glb"
          alt="A rock"
          exposure="0.008"
          camera-controls
          ar
          ar-modes="webxr"
          onClick={handleClick}
          ref={(ref) => {
            modelRef.current = ref;
          }}
        >
          {annots.map((annot, idx) => (
            <button
              key={`hotspot-${idx}`}
              className="view-button"
              slot={`hotspot-${idx}`}
              data-position={getDataPosition(annot)}
              data-normal={getDataNormal(annot)}
            ></button>
          ))}
        </model-viewer>
      );
    }
    

    Code Sandbox: https://codesandbox.io/s/lingering-tree-d41cr?file=/src/App.js:0-1287