如何动态更改查看器模型
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 很开心。希望这可以帮助遇到此问题的其他人。
我有一个 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 很开心。希望这可以帮助遇到此问题的其他人。