Three.js

【Three.js-IFC.js】React Three Fiberを使用してifcデータを読み込む

今回はReact Three FiberとIFC.jsを使ってIFCデータをブラウザで表示していきます。またIFC.jsの公式ドキュメントにあるチュートリアルも少し実装してみたいと思います。公式ドキュメント(こちら)でVanilla.jsで実装しているものをReactに置き換えてみたといった内容になってます。私自身、普段IFCデータを触っているわけではないので、おかしな箇所あったら教えていただけると幸いです。sampleのIFCデータは公式ドキュメントが提供してくれているものを使用しております(こちらから)。ソースコードはgithubにあげてますのでご興味あれば。

Version 一覧

  • React : 18.2.0
  • Typescript : 4.9.3
  • three.js: 0.146.0
  • React-three-fiber : 8.9.1
  • react-three-drei : 9.45.0
  • web-ifc-three : 0.0.121
  • zustand : 4.1.4
  • gsap : 3.11.3
  • chakra-ui : 2.4.2

React appの立ち上げ

まずは、Reactの新規アプリケーションを立ち上げます。今回はTypescriptも使用していこうと思います。

ターミナルで以下を実行します。

npx create-react-app ifc-viewer --template typescript

作成したアプリのディレクトリに移動してローカルサーバーを立ち上げます。http://localhost:3000 にアクセスし、以下の画面が立ち上がればOKです。

cd ifc-viewer
npm run start dev

不要なファイルを削除します。publicディレクトリとsrcディレクトリの中身を以下を残して全て削除します。

index.tsx / App.tsx / index.htmlの中身を以下のように修正し、表示画面も以下のようになっていればOKです。

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
import "./App.css";

function App() {
  return <h1>Hello world</h1>;
}

export default App;
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <title>IFC App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

続いてThree.js / React three fiberなど諸々のライブラリをインストールしていきます。バージョンを指定しながらインストールしてますが、最新版が良い方は@0.146などの@以降の記載不要です。ただバージョンによる違いがあるかもしれないのでご注意ください。

npm install three@0.146.0 @react-three/fiber@8.9.1 @types/three@0.146.0 @react-three/drei@9.45.0 

とりあえずReact three fiber で球体と表示しOrbitControlsで動かしてみましょう。App.tsxを以下のように書き換えて、画面に表示された球体が動かせればOKです。

import { Canvas } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";

import "./App.css";

function App() {
  return (
    <Canvas
      style={{
        width: "100vw",
        height: "100vh",
        background: "#f0f8ff",
      }}
      camera={{
        fov: 75,
        near: 0.1,
        far: 200,
        position: [10, 4, 10],
      }}
    >
      <OrbitControls makeDefault />
      <mesh>
        <sphereGeometry />
        <meshNormalMaterial wireframe />
      </mesh>
    </Canvas>
  );
}

export default App;

立ち上げはこれで完了です。

IFCモデルを表示

それではIFCモデルを表示していきます。とりあえずweb-ifc-threeをインストールします。

npm install web-ifc-three@0.0.121

もしかしたらエラーが出てきます。web-ifc-three@0.0.121が、threejs@0.146にまだ対応してないのでthreejsのバージョンを落とすか、--forceオプションで半強制的にインストールするように指示があるかと思います。react dreiとの依存関係の兼ね合いもあり、今回は強制的にインストールしていきます。(※本来はしっかりバージョンを合わせるべきかと思いますが・・・)

npm install web-ifc-three@0.0.121 --force

node_modules / web-ifc の中に[ web-ifc-mt.wasm ],[ web-ifc.wasm ] という2つのファイルがあるかと思います。この2つのファイルをpublic直下にコピーしていきます。

ifcデータを読み込む準備をしていきます。src ディレクトリにcomponentsディレクトリを作成し、その中にExperience.tsxを作成、App.tsxに記載したOrbitContorolsをこちらに記載します。また、ambientLightも設置しておきます。

その後、App.tsxでExperience.tsxを読み込みます。同じように球体が表示されていればOKです。

import { OrbitControls } from "@react-three/drei";

const Experience = () => {
  return (
    <>
      <ambientLight intensity={0.5} />
      <OrbitControls makeDefault />;
    </>
  );
};

export default Experience;
import { Canvas } from "@react-three/fiber";

import "./App.css";
import Experience from "./components/Experience";

function App() {
  return (
    <Canvas
      style={{
        width: "100vw",
        height: "100vh",
        background: "#f0f8ff",
      }}
      camera={{
        fov: 75,
        near: 0.1,
        far: 200,
        position: [10, 4, 10],
      }}
    >
      <Experience />
      <mesh>
        <sphereGeometry />
        <meshNormalMaterial wireframe />
      </mesh>
    </Canvas>
  );
}

export default App;

sampleのifcデータをこちらからダウンロードし、publicディレクトリの直下におきます。file名はなんでもいいですが、私は'sample-model.ifc'としてます

こちらのファイルを読み込んでいきます。Experience.tsxを以下のように書き換えます。またApp.tsxに記載の球体は削除します。

import { useThree } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
import { IFCLoader } from "web-ifc-three/IFCLoader";
import {
  acceleratedRaycast,
  computeBoundsTree,
  disposeBoundsTree,
} from "three-mesh-bvh";

const Experience = () => {
  const { scene } = useThree();

  const ifcLoader = new IFCLoader();

  ifcLoader.ifcManager.setupThreeMeshBVH(
    computeBoundsTree,
    disposeBoundsTree,
    acceleratedRaycast
  );
  ifcLoader.ifcManager.setWasmPath("../../");

  ifcLoader.load("/sample-model.ifc", (ifcModel) => {
    scene.add(ifcModel);
  });
  console.log(scene);

  return (
    <>
      <ambientLight intensity={0.5} />
      <OrbitControls makeDefault />;
    </>
  );
};

export default Experience;
  • 11行目 useThreeからSceneを取り出してきます。
  • 13行目 その後ifcLoaderのインスタンスを作成。
  • 15行目 ifcLoader.ifcManager.setupThreeMeshBVH …はオブジェクトのピッキングが早くなるようです…正直よく分かってませんがいい感じのライブラリみたいです。こちら参照
  • 20行目 その後、wasmファイルを読み込んでます。ここの相対パス若干ハマりましたが…
  • 22行目 sampleデータを読み込んで、sceneに追加指定ます。
  • 25行目は、一旦sceneの中身を確認しています。

これで以下のように表示されればOKですが、これでは問題があります。developper tool でsceneの中を確認すると、IFC Modelが沢山格納されてしまってます。

これはExperience.tsxが更新されるたびにLoadしてしまっていることが原因です。

useEffectで処理を囲ってあげてもいいのですが、TwitterでR3Fではこうやるんだよと教えていただきました!Pmndrs.Docs

componentsディレクトリ直下にModel.tsxを作成します。こちらでモデルをロードしてprimitiveを返してあげます。また、ifcModelにifcという名前をつけときます。

import { useLoader } from "@react-three/fiber";
import { IFCLoader } from "web-ifc-three";
import { IFCModel } from "web-ifc-three/IFC/components/IFCModel";
import {
  acceleratedRaycast,
  computeBoundsTree,
  disposeBoundsTree,
} from "three-mesh-bvh";

const Model = () => {
  const model: IFCModel = useLoader(
    IFCLoader,
    "/sample-model.ifc",
    (loader) => {
      loader.ifcManager.setupThreeMeshBVH(
        computeBoundsTree,
        disposeBoundsTree,
        acceleratedRaycast
      );
      loader.ifcManager.setWasmPath("../../");
    }
  );

 model.name = 'ifc';

  return <primitive object={model} />;
};

export default Model;

Experience.tsxで読み込めばOKです。

import { OrbitControls } from "@react-three/drei";
import Model from "./Model";

const Experience = () => {
  return (
    <>
      <Model />
      <ambientLight intensity={0.8} />
      <OrbitControls makeDefault />
    </>
  );
};

export default Experience;

これでモデルのロードは完了です。

ロード状態をグローバルStateで管理する

公式ドキュメントにオブジェクトのIDを取得するチュートリアルがあったのでやってみたいのですが、それをやる前にロード状態をグローバルStateで管理しておきます。

グローバルStateの管理には、Redux / Recoil あるいは useContextを使う、 などあるかと思いますが今回はzustandを使用します。

npm install zustand --force

src直下にstoresディレクトリを作成し、その中にuseLoadingState.tsxを作成します。

import { IFCLoader } from "web-ifc-three";
import create from "zustand";

interface LoadedState {
  loaded: boolean;
  setLoaded: (flg: boolean) => void;
  loader: IFCLoader | null;
  setLoader: (loader: IFCLoader) => void;
}

export default create<LoadedState>((set) => ({
  loaded: false,
  setLoaded: (flg: boolean) => {
    set(() => {
      return { loaded: flg };
    });
  },
  loader: null,
  setLoader: (loader: IFCLoader) => {
    set(() => {
      return { loader: loader };
    });
  },
}));

loadedをfalse => trueにすることで、ロード完了としています。

また、後々別のとこでIFCLoaderを使用するので、IFC Loaderもグローバルに管理します。

それではロード完了後 loaded 変数をtrueに、setLoaderでloaderを更新していきます。

R3F公式Docsより、useLoaderの第4引数にcallback関数を取ることで、ロードの進捗が取得できますが、今回はifcManagerが用意してくれているsetOnProgressを使用します。

Model.tsxを修正していきます。

import { useLoader } from "@react-three/fiber";
import { IFCLoader } from "web-ifc-three";
import { IFCModel } from "web-ifc-three/IFC/components/IFCModel";
import {
  acceleratedRaycast,
  computeBoundsTree,
  disposeBoundsTree,
} from "three-mesh-bvh";
import useLoadingState from "../stores/useLoadingState";
import { ParserProgress } from "web-ifc-three/IFC/components/IFCParser";

const Model = () => {
  const { setLoader, setLoaded } = useLoadingState((state) => state);

  const model: IFCModel = useLoader(
    IFCLoader,
    "/sample-model.ifc",
    (loader) => {
      loader.ifcManager.setupThreeMeshBVH(
        computeBoundsTree,
        disposeBoundsTree,
        acceleratedRaycast
      );
      loader.ifcManager.setWasmPath("../../");

      loader.ifcManager.setOnProgress((event: ParserProgress) => {
        const ratio = event.loaded / event.total;
        ratio === 1 && setLoaded(true);
      });

      setLoader(loader);
    }
  );

  model.name = "ifc";

  return <primitive object={model} />;
};

export default Model;
  • これでLoadingの状態をグローバルに管理することができました。

オブジェクトのID取得

公式ドキュメントのオブジェクトIDの取得を実装してみます。hooks直下にuseIdPicker.tsxを作成します。

import { useFrame, useThree } from "@react-three/fiber";
import { IFCModel } from "web-ifc-three/IFC/components/IFCModel";

import { useEffect, useRef, useState } from "react";
import { Object3D } from "three";

import useLoadingState from "../stores/useLoadingState";
import useFocusId from "../stores/useFocusId";

const useIdPicker = () => {
  const { scene, raycaster, gl } = useThree();
  const canvas = gl.domElement;

  const { loaded, loader } = useLoadingState((state) => state);

  const idRef = useRef<string>("");
  const [rayObjects, setRayObjects] = useState<Object3D[] | null>(null);

  useEffect(() => {
    if (loaded) {
      const model = scene.children.filter((mesh) => {
        const ifc = mesh.name === "ifc" && mesh;
        return ifc;
      });
      setRayObjects(model);
      canvas.addEventListener("dblclick", () => {
        console.log(idRef.current);
      });
    }
  }, [loaded]);

  useFrame(() => {
    if (rayObjects && rayObjects.length > 0) {
      raycaster.firstHitOnly = true;
      const obj = raycaster.intersectObjects(rayObjects);
      if (obj.length > 0 && loader && loaded) {
        const ifcObject = obj[0];
        const index = ifcObject.faceIndex;
        const ifcModel = ifcObject.object as IFCModel;
        const geometry = ifcModel.geometry;
        const ifc = loader.ifcManager;
        const id: string = index
          ? ifc.getExpressId(geometry, index).toString()
          : "";
        idRef.current = id;
      } else {
        idRef.current = "";
      }
    }
  });

  return;
};

export default useIdPicker;
  • まずはグローバルに管理しているloaderとloadedrをインポートします。
  • useThreeからscene ,raycaster, glを取得し、glからcanvas要素を取得します。
  • useRefで取得したidを更新します。
  • useStateではidを取得する対象のモデル(IFCModel)を管理します。初期値はnullでロードが完了したら事前にモデルにつけておいたifcという名前のMeshを探し、セットします。
  • useEffect内でロードが完了後、IFCModelを取得し、raycastの対象オブジェクトとしてセットしています。またcanvasをダブルクリックしたら、現在取得しているidをconsoleで出力するようにしています。
  • useFrame内でraycastで最初にぶつかったオブジェクトを取得し、そのIDを取得してuseRefの値を更新しています。IFCModelとぶつかっていない場合は、useRefの値を''に更新しています。

Experience.tsx内で実行してあげます。

import { OrbitControls } from "@react-three/drei";

import useIdPicker from "../hooks/useIdPicker";
import Model from "./Model";

const Experience = () => {
  useIdPicker();

  return (
    <>
      <Model />
      <ambientLight intensity={0.8} />
      <OrbitControls makeDefault />
    </>
  );
};

export default Experience;
  • 以下の画像のように、consoleで確認すると、IDが取得できてそうです。

取得したIDを画面に表示

画面上にIDを表示していきます。CSSのフレームワークChakraUIを使用していきます。まずはインストールします。

npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion --force

取得したIDをグローバルStateで管理していきます。storesディレクトリ直下にuseFocusId.tsxを作成します。その後、useIdPicker.tsx内で取得したIDを更新します。

import create from "zustand";

interface FocusId {
  focusId: string;
  setFocusId: (id: string) => void;
}

export default create<FocusId>((set) => ({
  focusId: "",
  setFocusId: (id: string) => {
    set(() => {
      return { focusId: id };
    });
  },
}));
import { useFrame, useThree } from "@react-three/fiber";
import { IFCModel } from "web-ifc-three/IFC/components/IFCModel";

import { useEffect, useRef, useState } from "react";
import { Object3D } from "three";

import useLoadingState from "../stores/useLoadingState";
import useFocusId from "../stores/useFocusId";

const useIdPicker = () => {
  const { scene, raycaster, gl } = useThree();
  const canvas = gl.domElement;

  const { loaded, loader } = useLoadingState((state) => state);
  const { setFocusId } = useFocusId((state) => state);

  const idRef = useRef<string>("");
  const [rayObjects, setRayObjects] = useState<Object3D[] | null>(null);

  useEffect(() => {
    if (loaded) {
      const model = scene.children.filter((mesh) => {
        const ifc = mesh.name === "ifc" && mesh;
        return ifc;
      });
      setRayObjects(model);
      canvas.addEventListener("dblclick", () => {
        setFocusId(idRef.current);
      });
    }
  }, [loaded]);

  useFrame(() => {
    if (rayObjects && rayObjects.length > 0) {
      raycaster.firstHitOnly = true;
      const obj = raycaster.intersectObjects(rayObjects);
      if (obj.length > 0 && loader && loaded) {
        const ifcObject = obj[0];
        const index = ifcObject.faceIndex;
        const ifcModel = ifcObject.object as IFCModel;
        const geometry = ifcModel.geometry;
        const ifc = loader.ifcManager;
        const id: string = index
          ? ifc.getExpressId(geometry, index).toString()
          : "";
        idRef.current = id;
      } else {
        idRef.current = "";
      }
    }
  });

  return;
};

export default useIdPicker;

componentsディレクトリにInterface.tsxを作成します。その後App.tsxでInterface.tsxをインポートします。

ID が空文字でない時、画面左上にIDが表示されればOKです。

import { Box } from "@chakra-ui/react";

import useFocusId from "../stores/useFocusId";

const Interface = () => {
  const { focusId } = useFocusId((state) => state);

  return (
    <>{focusId !== "" && <Box sx={idDisplayStyle}>{`ID : ${focusId}`}</Box>}</>
  );
};

export default Interface;

const idDisplayStyle = {
  width: "90px",
  height: "40px",
  display: "flex",
  top: "0",
  left: "0",
  marginLeft: "5px",
  lineHeight: "40px",
  padding: "0 5px 0 5px",
  background: "rgba(255, 255, 255, 0.8)",
  textAlign: "center",
  position: "absolute",
};
import { Canvas } from "@react-three/fiber";

import "./App.css";
import Experience from "./components/Experience";
import Interface from "./components/Interface";

function App() {
  return (
    <>
      <Canvas
        style={{
          width: "100vw",
          height: "100vh",
          background: "#f0f8ff",
        }}
        camera={{
          fov: 75,
          near: 0.1,
          far: 200,
          position: [10, 4, 10],
        }}
      >
        <Experience />
      </Canvas>
      <Interface />
    </>
  );
}

export default App;

ロード画面を作成

最後に上のGIF画像のようなロード画面を作っていきます。fadeoutする処理にライブラリgsapを使用するので、インストールします。

npm install gsap --force

とりあえずInterface.tsxにLoadingバーを書いていきます。

import { Box } from "@chakra-ui/react";

import useFocusId from "../stores/useFocusId";

const Interface = () => {
  const { focusId } = useFocusId((state) => state);

  return (
    <>
      {focusId !== "" && <Box sx={idDisplayStyle}>{`ID : ${focusId}`}</Box>}
      <Box id="barContainer" sx={barContainerStyle}>
        <Box id="loadingBar" sx={loadingBarStyle} />
        <Box id="loadingText" sx={loadingTextStyle}>
          Loading...
        </Box>
      </Box>
    </>
  );
};

export default Interface;

const idDisplayStyle = {
  width: "90px",
  height: "40px",
  display: "flex",
  top: "0",
  left: "0",
  marginLeft: "5px",
  lineHeight: "40px",
  padding: "0 5px 0 5px",
  background: "rgba(255, 255, 255, 0.8)",
  textAlign: "center",
  position: "absolute",
};

const barContainerStyle = {
  height: "100vh",
  width: "100vw",
  top: "0",
  alignItems: "center",
  position: "absolute",
};

const loadingBarStyle = {
  top: "50%",
  width: "100vw",
  height: "2px",
  position: "absolute",
  background: "black",
  transform: "scaleX(1)",
  transformOrigin: "top center",
  transition: "transform 1.0s",
};

const loadingTextStyle = {
  width: "100%",
  fontSize: "20px",
  textAlign: "center",
  position: "absolute",
  top: "45%",
};

ロードの進捗によって、このバーとLoadingのテキストを変更していきます。

まずは、Loading中にカメラの前に配置する白いPlane Meshを生成していきたいのですが、その前にPlaneMeshのStateを管理するためにstoresディレクトリにuseOverrayState.tsxを作成します。

import create from "zustand";

interface OverrayState {
  removeOverray: boolean;
  setRemoveOverray: (flg: boolean) => void;
}

export default create<OverrayState>((set) => ({
  removeOverray: false,
  setRemoveOverray: (flg: boolean) => {
    set(() => {
      return { removeOverray: flg };
    });
  },
}));

次にplaneMeshを作成していきます。componentsディレクトリにLoadingOverRay.tsxを作成し、Experience.tsxで読み込みます。

plameMeshはShaderを使用してfadeoutさせていきます。また、removeOverrayがtrueになったらsceneから削除するようにしています。

import * as THREE from "three";
import useOverrayState from "../stores/useOverrayState";

const overlayGeometry = new THREE.PlaneGeometry(300, 300, 1, 1);
const overlayMaterial = new THREE.ShaderMaterial({
  transparent: true,
  uniforms: {
    uAlpha: { value: 1.0 },
    uColor1: { value: new THREE.Color("#f0f8ff") },
    uColor2: { value: new THREE.Color("#ffffff") },
  },
  vertexShader: `
        varying vec2 vUv;
        void main()
        {
          vec4 modelPosition = modelMatrix * vec4(position, 1.0);
          vec4 viewPosition = viewMatrix * modelPosition;
          vec4 projectedPosition = projectionMatrix * viewPosition;
          gl_Position = projectedPosition;                
          vUv = uv;
        }
    `,
  fragmentShader: `
        varying vec2 vUv;
        uniform float uAlpha;
        uniform vec3 uColor1;
        uniform vec3 uColor2;
        void main()
        {
            float strength = distance(vUv, vec2(0.5));
            vec3 color = mix(uColor1, uColor2, strength + 0.2 );
            gl_FragColor = vec4(color, uAlpha);
        }
    `,
});

const LoadingOverRay = () => {
  const { removeOverray } = useOverrayState((state) => state);

  return !removeOverray ? (
    <mesh
      geometry={overlayGeometry}
      material={overlayMaterial}
      position={[0, 0, 14]}
      rotation-y={Math.PI * 0.25}
      name="overray"
      dispose={null}
    />
  ) : null;
};

export default LoadingOverRay;
import { OrbitControls } from "@react-three/drei";

import useIdPicker from "../hooks/useIdPicker";
import LoadingOverRay from "./LoadingOverRay";
import Model from "./Model";

const Experience = () => {
  useIdPicker();

  return (
    <>
      <LoadingOverRay />
      <Model />
      <ambientLight intensity={0.8} />
      <OrbitControls makeDefault />
    </>
  );
};

export default Experience;

次にInterface.tsxのloadingBarStyleのtransform: "scaleX(1)"を0にしときます。

.....省略
const loadingBarStyle = {
  top: "50%",
  width: "100vw",
  height: "2px",
  position: "absolute",
  background: "black",
  transform: "scaleX(0)",
  transformOrigin: "top center",
  transition: "transform 1.0s",
};
.....省略

最後にModel.tsxにローディング中の処理を追記します。(※すみません、ここコードかなり汚いです。hooksに切り出そうとしたのですが、ifcManagerのsetonprogressの扱いが難しく、試行錯誤してこんな感じになっちゃいました......)

import { useLoader, useThree } from "@react-three/fiber";
import { IFCLoader } from "web-ifc-three";
import { IFCModel } from "web-ifc-three/IFC/components/IFCModel";
import {
  acceleratedRaycast,
  computeBoundsTree,
  disposeBoundsTree,
} from "three-mesh-bvh";
import useLoadingState from "../stores/useLoadingState";
import { ParserProgress } from "web-ifc-three/IFC/components/IFCParser";
import useOverrayState from "../stores/useOverrayState";
import { gsap } from "gsap";

const Model = () => {
  const { gl, scene } = useThree();
  const canvas = gl.domElement;
  const loadingBar = document.getElementById("loadingBar");
  const loadingText = document.getElementById("loadingText");
  const barContainer = document.getElementById("barContainer");

  const { setLoader, setLoaded } = useLoadingState((state) => state);
  const { setRemoveOverray } = useOverrayState((state) => state);

  const handleLoading = () => {
    setLoaded(true);
    if (loadingText && barContainer) {
      loadingText.innerHTML = "Go to Model !!";
      loadingText.style.cursor = "pointer";
      loadingText.addEventListener("click", () => {
        barContainer.style.display = "none";
        canvas.style.background =
          "linear-gradient(0deg, rgba(2,0,36,1) 0%, rgba(9,9,121,1) 35%, rgba(0,212,255,1) 100%)";
        const overrayMesh = scene.children.filter((mesh) => {
          if (mesh.name === "overray") {
            return mesh as THREE.Mesh;
          }
        });
        const mesh = overrayMesh[0] as unknown as THREE.Mesh;
        const material = mesh.material as unknown as THREE.ShaderMaterial;
        gsap.to(material.uniforms.uAlpha, { duration: 1, value: 0 });
        setTimeout(() => {
          setRemoveOverray(true);
        }, 500);
      });
    }
  };

  const model: IFCModel = useLoader(
    IFCLoader,
    "/sample-model.ifc",
    (loader) => {
      loader.ifcManager.setupThreeMeshBVH(
        computeBoundsTree,
        disposeBoundsTree,
        acceleratedRaycast
      );
      loader.ifcManager.setWasmPath("../../");

      loader.ifcManager.setOnProgress((event: ParserProgress) => {
        const ratio = event.loaded / event.total;
        loadingBar!.style.transform = `scaleX(${ratio})`;
        ratio === 1 && handleLoading();
      });

      setLoader(loader);
    }
  );

  model.name = "ifc";

  return <primitive object={model} />;
};

export default Model;

  • setOnProgressでローディングの進捗が取得できます。ratioが<1未満だとロード中、1になったらロード完了です。
  • ratio < 1の時は、loadingBarをratioでx方向にscaleさせることで、ロードの進捗に伴ってバーが伸びていくようにしています。
  • ratio === 1になったら、handleLoading関数を実行します。
  • handleLoading関数では、setLoadedでロード状態のStateを更新。
  • Loadingのスタイルを変更し、テキストをクリックしたら、テキストやバーをdisplay:noneにして、Plane Meshをfadeoutさせてます。その0.5s後にplaneMeshを削除しています。

完成

以上になります。普段からIFCデータやIFC.jsを触っているわけではないので、変な記述などあったら教えていただけると嬉しいです。m(_ _)m
ライブラリのインポートを --forceで強制的にインポートしているのがちょっと・・・って感じですが、この辺はしっかりバージョン合わせていく必要はあるかなと思います。
ソースコードはgithubにあげてますのでご興味あれば。

[ 参考 ]

  • IFC.js Docs : https://ifcjs.github.io/info/ja/docs/Introduction
  • Svelte × Typescript × IFC.jsでIFCをWeb上に表示 : https://zenn.dev/masamiki/articles/c9a34119acfd6c
  • pmndrs Docs : https://docs.pmnd.rs/

以上

【2022年最新】React(v18)完全入門ガイド|Hooks、Next.js、Redux、TypeScript HTML、CSS、JavaScriptの基礎を終えた方に最適!React入門の決定版!Reactについて知っておくべき基礎知識について体系的、かつ網羅的に学習して、最短でReactをマスターしよう! icon
夢は大きく. 対象コースが¥1,600から。

-Three.js
-, , ,