如何动态更改查看器模型

How to Change Viewer Model Dynamically

我有一个 React 项目,正在尝试使用 Autodesk Forge 查看器。我有它的工作原理,但我遇到了很多我不理解的错误和奇怪的行为。我对 Forge 查看器和 React 都很陌生,所以我确定我遗漏了一些简单的东西,但我不知道它是什么。

此页面的总体思路是用户从他们可以单击的数据库(Forge 外部)获取位置列表。如果他们点击其中一个 select,系统会检查是否有与之关联的 dwg 文件。如果不是,它会显示一个通用的 div,表示没有关联的文件,但如果是这样,它会在 forge 查看器中显示 dwg。所以查看器本身有时是隐藏的,但应该始终存在,但有时需要更改它显示的文件。

现在我有了它,所以当我单击第一个时,它会出现并正确显示。但是,当我单击另一个然后返回到第一个时,它会消失并在控制台中给我一个错误。这是我的锻造组件:

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;

在使用查看器的父组件中,这里是 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>

我得到的错误是:

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
  ...

它在错误中引用的行号是以 viewer?.loadDocumnentNode(... 开头的行号如果它使用条件访问,它怎么可能为 null 并且仍然抛出错误?在该调用之前,我还在该行(doc、defaultModel 和 viewer)上记录了所有三个变量,它们从未显示为 null...

谁能告诉我我做错了什么?我看过很多不同的样本,但它们似乎都只是处理显示内容,我找不到任何关于更改显示的内容。

异常是在 forge 查看器的某个地方抛出的 Viewer3D.js:1799,而不是您的代码。

我猜 loadDocumentNode 的论点是错误的。 .getDefaultGeometry() 是否包含正确的 Viewable

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

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

此外,似乎没有必要在您的代码中加载查看器 js/css。只需将它们添加到您的 index.html 并固定版本。

<!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>

如果您是 Autodesk Forge React 的新手,我强烈建议您从没有任何框架的基础 UI 开始。此外,Forge 不为其查看器提供官方 React 包装器,因此如果您使用的是第 3 方 React 组件,其中可能存在我们无法为您提供支持的问题。

至于大体思路,当然可以。您可以从查看器的同一实例加载和卸载模型,包括。如果需要隐藏查看器。考虑 https://forge-industrial-construction.autodesk.io 演示 - 如果您取消选中右侧 Areas/Systems table 中的所有框,查看器将保留在原位,但将从中卸载所有模型。

好的,在进行了更多的日志记录和调查之后,我想我已经通过下面描述的一些更改解决了这个问题,适用于发现自己处于与我相同情况的任何人。

首先,事实证明问题主要集中在 useEffect 的析构函数中 'finished' 的查看器:

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

发生这种情况后,查看器实际上已经死了,但我没有将我的值设置为未定义,所以下次它发现查看器为 non-null 并尝试使用它但它已经完成.第一个简单的改变:

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

这确保查看器在完成后重新创建。

其次实际上是在我的消费者中使用这个模块。该组件的道具有一个查看器就绪回调,它会在查看器准备好使用时通知使用模块。然后,我的调用组件对查看器执行其他操作(查找图层和块等)。但是,我将其存储在标准变量中:

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

  ...

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

  ...
}

相反,我需要将这部分设为状态并使用 reducer 来设置当前查看器,如下所示:

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

  ...

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

  ...
}

差不多就这样了,只是它不断地让我重新加载。我将 useEffect 中的依赖项更新为仅 props.urn 以便在 urn 更改时它只会 re-run 。这停止了​​持续的重新加载,但是 React(不管怎样)抱怨依赖数组不包括 viewerReady 事件,因为它被用在 useEffect 方法中。经过更多研究,基本上 viewerReady 事件在每次渲染时都被重做,导致值发生变化,从而导致常量 re-renders。相反,我需要使用 useCallback 来创建方法,除非其中一个依赖项发生更改,否则它不会更改(并且由于我没有依赖项,它只是在第一次渲染时创建)。这是最后的方法:

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

完成所有这一切之后,它似乎很稳定,可以正常工作,而且 React 很开心。希望这可以帮助遇到此问题的其他人。