2

I have a react project and am trying to use the Autodesk Forge viewer. I have it sort of working but am getting a lot of errors and weird behavior that I don't understand. I'm pretty new to both Forge viewer and React so I'm sure I'm missing something simple but I don't know what it is.

The general idea of this page is that the user gets a list of locations from the DB (outside Forge) that they can click on. If they click on one to select it the system checks if there is a dwg file associated with it. If not it displays a generic div that says there is no file associated but if so it displays the dwg in the forge viewer. So the viewer itself is sometimes hidden but should always be there but will need to change the file that it's displaying sometimes.

Right now I have it so that when I click the first one it comes up and displays it correctly. However, when I click another and then back to the first one it blanks out and gives me an error in the console. Here is my forge component:

import React, { useEffect, useRef } from "react";
import styled from "styled-components";
import { BorderedZoneOuter } from "./Generic/CommonStyledComponents";
import { ForgeBackgroundService } from "../services/ForgeBackgroundService";
import { valueIsNullOrUndefined } from "../Utility";

const viewerLibraryURL =
  "https://developer.api.autodesk.com/modelderivative/v2/viewers/viewer3D.min.js?v=v7.*";
const viewerStylesheetURL =
  "https://developer.api.autodesk.com/modelderivative/v2/viewers/style.min.css?v=v7.*";

let viewerLibraryLoaded = false;
let viewerStyleLoaded = false;
let viewerLoading = false;

const ForgeContainer = styled(BorderedZoneOuter)`
  position: relative;
  flex: 1;
`;

const _backgroundService = new ForgeBackgroundService();
let viewer: Autodesk.Viewing.GuiViewer3D | undefined;

const ForgeViewer = (props: {
  urn: string;
  viewerReady?: (viewer: Autodesk.Viewing.GuiViewer3D) => void;
}) => {
  const container: any = useRef();

  useEffect(() => {
    const handleStyleLoad = () => {
      console.log("style loaded");
      viewerStyleLoaded = true;
      viewerLibraryLoaded && loadViewer();
    };

    const handleScriptLoad = () => {
      console.log("script loaded");
      viewerLibraryLoaded = true;
      viewerStyleLoaded && loadViewer();
    };

    const loadViewer = () => {
      console.log("loading viewer");
      if (!viewerLoading) {
        viewerLoading = true;
        Autodesk.Viewing.Initializer(
          {
            env: "AutodeskProduction2",
            api: "streamingV2",
            getAccessToken: (onTokenReady) => {
              if (onTokenReady) {
                _backgroundService.GetToken().then((t) => {
                  if (valueIsNullOrUndefined(t)) {
                    return;
                  }
                  onTokenReady(t!.token, t!.expiresIn);
                });
              }
            },
          },
          () => {
            viewer = new Autodesk.Viewing.GuiViewer3D(container.current);
            viewer.start();
            loadModel(props.urn);
            viewerLoading = false;
          }
        );
      }
    };

    const loadStyleSheet = (href: string) => {
      const styles = document.createElement("link");
      styles.rel = "stylesheet";
      styles.type = "text/css";
      styles.href = href;
      styles.onload = handleStyleLoad;
      document.getElementsByTagName("head")[0].appendChild(styles);
    };

    const loadViewerScript = (href: string) => {
      const script = document.createElement("script");
      script.src = href;
      script.async = true;
      script.onload = handleScriptLoad;
      document.getElementsByTagName("head")[0].appendChild(script);
    };

    function loadModel(urn: string): void {
      console.log(urn);
      console.log(viewer);
      Autodesk.Viewing.Document.load(
        urn,
        (doc) => {
          console.log(doc);
          const defaultModel = doc.getRoot().getDefaultGeometry();
          console.log(defaultModel);
          viewer?.loadDocumentNode(doc, defaultModel)
            .then((m: Autodesk.Viewing.Model) => {
              console.log(m);
              if (props.viewerReady) {
                props.viewerReady(viewer!);
              }
            });
        },
        () => {
          console.error("failed to load document");
        }
      );
    }

    if (!valueIsNullOrUndefined(viewer)) {
      console.log("have viewer, loading model");
      loadModel(props.urn);
    } else {
      console.log("no viewer, loading scripts");
      loadStyleSheet(viewerStylesheetURL);
      loadViewerScript(viewerLibraryURL);
    }

    return () => {
      viewer?.finish();
    };
  }, [props]);

  return <ForgeContainer ref={container} />;
};

export default ForgeViewer;

In the parent component that uses the viewer, here is the relevant portion of the tsx:

  <MainCanvas>
    <PageTitle>Create Spaces</PageTitle>
    <button onClick={select}>select</button>
    {valueIsNullOrUndefined(state.selectedFloor) && (
      <NoBackgroundZone>
        <div>You have not selected a floor</div>
        <div>
          Please select a floor to view the background and create spaces
        </div>
      </NoBackgroundZone>
    )}

    {!valueIsNullOrUndefined(state.selectedFloor) &&
      valueIsNullOrUndefined(state.backgroundUrn) && (
        <NoBackgroundZone>
          <div>There is no background added for this floor</div>
          <LinkButton>Add a background</LinkButton>
        </NoBackgroundZone>
      )}

    {!valueIsNullOrUndefined(state.selectedFloor) &&
      !valueIsNullOrUndefined(state.backgroundUrn) && (
        <ForgeViewer urn={state.backgroundUrn!} viewerReady={viewerReady} />
      )}
  </MainCanvas>

The error I get is:

Uncaught TypeError: Cannot read properties of null (reading 'hasModels') Viewer3D.js:1799
  at C.he.loadDocumentNode (Viewer3D.js:1799:53)
  at ForgeViewer.tsx:98:1
  ...

The line number that it's referring to in the error is the one that starts with viewer?.loadDocumnentNode(... If it's using the conditional access how can it be null and still throwing the error? I also have logged all three variables on that line (doc, defaultModel, and viewer) right before that call and they never show up as null...

Can anyone tell me what I'm doing wrong here? I've looked at a lot of different samples but all of them seem to deal with just getting something displayed and I can't find anything about changing the display.

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
sfaust
  • 2,089
  • 28
  • 54

3 Answers3

1

The exception is thrown somewhere in the forge viewer Viewer3D.js:1799, not your code.

I guess the arguments to loadDocumentNode a wrong. Does .getDefaultGeometry() contain the correct Viewable?

https://forge.autodesk.com/en/docs/viewer/v2/tutorials/basic-viewer/

var viewables = Autodesk.Viewing.Document.getSubItemsWithProperties(doc.getRootItem(), {'type':'geometry'}, true);

Also it seems unnecessary to load the viewer js/css in your code. Just add them to your index.html and pin the version.

<!doctype html>
<html>
<head>
    <link rel="stylesheet" href="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.60/style.min.css">
</head>
<body>
    <div id="app"></div>
    <script src="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.60/viewer3D.min.js"></script>
</body>
</html>
Peter
  • 51
  • 5
  • 1
    Thanks, I think the issue was the viewer being closed by React and I was still trying to reference it. As for loading the viewer in code, I mostly was just following examples I found around the web from various places (mostly Autodesk). Most of them load it dynamically. I'm honestly not sure why they do it that way but I did notice that it marks the script async and then has a loaded handler so maybe it's a page load speed thing? – sfaust Feb 07 '22 at 17:35
1

If you're new to both Autodesk Forge and React, I would strongly recommend to start with a basic UI without any frameworks. Also, Forge does not provide an official React wrapper for its viewer so if you're using a 3rd party React component, there might be issues in there that we won't be able to support you on.

As for the general idea, that is certainly doable. You can load and unload models from the same instance of the viewer, incl. hiding the viewer if needed. Consider the https://forge-industrial-construction.autodesk.io demo - if you uncheck all the boxes in the Areas/Systems table on the right, the viewer will remain in place but all the models will be unloaded from it.

Petr Broz
  • 8,891
  • 2
  • 15
  • 24
  • Well that's why I said 'pretty' new :). I have done a basic testing project with the forge viewer in vanilla JS and I've done some basic things with React. Just I haven't done a lot with either and haven't done anything with the two together which is what I'm having trouble with. I think I may have figured it out though, posting shortly... – sfaust Feb 07 '22 at 17:32
0

Ok so after doing a lot more logging and investigating I think I have this solved with a few changes described below for anyone that finds themselves in the same situation I was.

First, it turns out the issue was mostly centered around the viewer being 'finished' in the destructor function of useEffect here:

return () => {
  viewer?.finish();
};

After this happens the viewer in effect is dead, but I didn't set my value to undefined so the next time it found the viewer as non-null and tried to use it but it had already been finished. First simple change:

return () => {
  viewer?.finish();
  viewer = undefined;
};

This makes sure that the viewer is recreated if it's been finished.

Secondly was actually in my consumer that is using this module. The props for this component has a viewer ready callback that tells the consuming module when the viewer is ready for use. My calling component then does other things with the viewer (finds layers and blocks, etc.). However, I was storing this in a standard variable:

const SpaceCreator = () => {
  let viewer: Autodesk.Viewing.GuiViewer3D | undefined;

  ...

  function viewerReady(v: Autodesk.Viewing.GuiViewer3D): void {
    viewer = v;
  }

  ...
}

Instead I needed to make this part of the state and use the reducer to set the current viewer like so:

const SpaceCreator = () => {
  const [state, dispatch] = useReducer(reducer, new SpaceCreatorState());

  ...

  function viewerReady(v: Autodesk.Viewing.GuiViewer3D): void {
    dispatch({ type: SpaceCreatorActions.viewer, payload: v });
  }

  ...
}

This was almost it, except that it kept reloading on me constantly. I updated the dependencies in the useEffect to be only props.urn so that it would only re-run when the urn changed. This stopped the constant reloading, but React (linting anyway) complained about the dependency array not including the viewerReady event since it was used in the useEffect method. After some more research, basically the viewerReady event was being redone on every render causing the value to change which is causing the constant re-renders. Instead I needed to use useCallback to create the method which makes it not change unless one of it's dependencies changes (and since I have no dependencies it just creates on the first render). Here is the final method:

const viewerReady = useCallback((v: Autodesk.Viewing.GuiViewer3D) => {
    dispatch({ type: SpaceCreatorActions.viewer, payload: v });
  }, []);

After all of this it seems to be stable, working correctly, and React is happy. Hope this helps someone else having this issue.

sfaust
  • 2,089
  • 28
  • 54