🏢

Svelte × Typescript × IFC.jsでIFCをWeb上に表示

2022/02/14に公開

※今回の記事は、IFCを触る建築業界の人向けに書いてます。

IFC.jsとは

https://ifcjs.github.io/info/ja/

IFC.jsは、ブラウザでIFCモデルをロード・表示・編集するためのライブラリです。.jsとは書いてありますが、javascriptだけではなく、cなどの言語も含まれたライブラリです。

Ansoniを中心とした、エンジニアのグループが開発しているOSSで、BIMの開発技術が閉じているため、誰でも開発できる環境を作りたいという目的で開発が進められているライブラリです。

IFC.jsは、以下の2つのを可能にしてくれます。

  1. ジオメトリの表示
    IFC.js は、Three.jsBabylon.jsなどの3Dライブラリに対応するように作られており、

  2. 属性データの抽出
    ジオメトリに関連するすべてのプロパティの取得を可能にしてくれます。

  3. IFCファイルの編集・書き込み
    IFCへ編集したデータの編集と書き込みを行うこともできます。

他にも、dxfやpdfの書き出しも書き方次第ですが、可能にしてくれます。

IFC.jsの何がいいか

スピード

IFC.jsのコア部分はC言語で開発されています。それをEmscripten ※1 でWebAssemblyへコンパイルし、Javascriptから呼びだせるようにしています。
WebAssemblyとは、Webブラウザ上で動くバイナリコードのフォーマットで、CやC++、Rustなどからコンパイルすることができます。「バイナリ」、つまり01の機械語にすでに変換されているため、高いパフォーマンスの処理を行うことができるのです。(簡単に言ってしまうと)
Javascriptは、ブラウザのJavascriptインタプリタを介して実行されるインタプリタ方式と呼ばれる方法で実行されます。それに対しコンパイル方式は、機械語に変換されてしまっているので、コードの解釈という段階を踏む必要がなくなるのです。(簡単に言ってしまうと)

※1 Emscriptenとは: C/C++で実装されたプログラムやライブラリをJavaScriptで実行できるフォーマットに変換するコンパイラです。

カスタマイズ性

データの抽出・Three.jsとの連携といった、IFCから画面に表示されるまでの行う必要がある処理が分割されており、それらが密に連動をしていないがために、必要な箇所に手をいれてカスタマイズが行いやすいという利点があります。
詳しくはThree.jsとの関係性の所で説明します。

構成

IFC.jsは、3つのライブラリから構成されます。

web-ifc: パーサー

IFC.jsのコアとなるライブラリで、IFCデータのパース(IFC.jsで利用できる形への変換)とジオメトリの生成を行うライブラリです。

C、C++でコーディングされています。
我々、ライブラリ利用者が触ることはほぼないでしょう。OSSに参加したい方で無い限り…。

https://github.com/tomvandig/web-ifc

web-ifc-three: ローダー

web-ifcのデータをロードし、Three.jsのシーン(ビュー)を生成するライブラリです。利用するときは、このパッケージだけをインストールすれば、WebAssembly化されたweb-ifcも引っ張ってくるので、実質このライブラリが利用する側にとってはメインのライブラリとなります。

https://github.com/IFCjs/web-ifc-three

web-ifc-viewer: ビューワー操作機能集

web-ifc-threeで生成されたシーン(ビューワー) 上で、IFCを操作する機能を詰め込んでいるライブラリです。web-ifc-threeをラップしたコードとなっており、細かい処理を記載しなくてても我々がBIMのViewerでやりたいと思うような処理がすでに入っています。

https://github.com/IFCjs/web-ifc-viewer

Three.jsとの関係性

web-ifc-threeでシーンを生成すると記載しましたが、web-ifc-threeはThree.jsを内部に組み込むというよりは、Three.jsと連動するような作りになっています。

もし、その概念を書くとすればこんな感じでしょうか。
比較対象をAutodesk Forgeとしていますが、Autodesk ForgeのViewerがThree.jsを拡張したようなライブラリ(API)になっています。それに対し、IFC.jsはあくまでThree.jsを操作できるライブラリとして作られている、というような形になっています。

これは、Three.jsと(ある程度)疎な関係になっているということなので、Thrree.jsのバージョンの更新への追従や、Three.jsを用いたカスタマイズがしやすい構成とも言うことができるかと思います。

実装方法例

ViteでSvelte×Typescriptのアプリケーションを作成

今回は、Svelteを使ったアプリケーション上に、IFC.jsを実装してみようと思います。

以下のcommandでTypescriptのSvelteのアプリを生成します。

yarn create vite ifc-viewer-app --template svelte-ts

開発環境起動

開発環境の起動は、以下のcommandです。

yarn dev

lintの設定

lintの設定をします。
lintを気にしない方は、スキップしてもらっていい項目です。

packageのインストール

yarn add -D eslint eslint-plugin-svelte3 @typescript-eslint/parser @typescript-eslint/eslint-plugin  @pyoner/svelte-types

yarn create @eslint/config

eslintの設定

.eslint.js
module.exports = {
  'env': {
    'browser': true,
    'es2021': true,
  },
  'extends': [
    'google',
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-requiring-type-checking'
  ],
  'parser': '@typescript-eslint/parser',
  'parserOptions': {
    'ecmaVersion': 'latest',
    'sourceType': 'module',
    'tsconfigRootDir': __dirname,
    'project': ['./tsconfig.json'],
    'extraFileExtensions': ['.svelte']
  },
  'plugins': [
    'svelte3',
    '@typescript-eslint',
  ],
  'overrides': [
    {
      'files': ['*.svelte'],
      'processor': 'svelte3/svelte3'
    }
  ],
  'settings': {
    'svelte3/typescript': require('typescript'),
    'svelte3/ignore-styles': () => true
  },
  'ignorePatterns': ['node_modules'],
};

Design frameworkのインストール

今回は、SvelteのDesign frameworkであるattractionsを使ってみようと思います。

https://illright.github.io/attractions/

yarn add -D attractions svelte-preprocess sass postcss

web-ifc-threeのインストール

本家公式のチュートリアルでは、バンドラーとしてrollupを利用しているのですが、この事例では、必要ありません。

yarn add web-ifc-three three

node_modulesの中に、「web-ifc」「web-ifc-three」がインストールされていることを確認できるかと思います。

wasmの準備

そして、node_modules/web-ifc/に入っているweb-ifc.wasmweb-ifc-mt.wasmpublicディレクトリにコピーします。

すでにパッケージ内に、WebAssemblyにコンパイルされたものが入っているので、emscriptenをインストールしてコンパイルする必要がありません。
※パーサーのカスタマイズをしたい人はweb-ifcをgit cloneして編集して、emscriptenでコンパイルしましょう。

cp node_modules/web-ifc/web-ifc.wasm public/web-ifc.wasm
cp node_modules/web-ifc/web-ifc-mt.wasm public/web-ifc-mt.wasm

Three.jsのセットアップ

今回は、attractionsのCard内にViewerを作ろうと思います。
Card内にcanvasタグを用意しましょう。
そして、scriptの中で、そのcanvasを使ってThree.jsをセットアップするするコードを記載します。

Viewer.svelte
<script lang="ts">
  import {onMount} from 'svelte';
  import {Card} from 'attractions';
  import {
    AmbientLight,
    AxesHelper,
    DirectionalLight,
    GridHelper, Material,
    PerspectiveCamera,
    Scene,
    WebGLRenderer,
  } from 'three';
  import {
    OrbitControls,
  } from 'three/examples/jsm/controls/OrbitControls';

  let canvasWidth: number;
  let canvasHeight: number;
  let canvas: HTMLCanvasElement;
  export let width = '100%';
  export let height = '80vh';

  let scene: Scene;
  let camera: PerspectiveCamera;
  let renderer: WebGLRenderer;

  onMount(() => {
    // create the Three.js scene
    // Three.jsのシーン作成
    scene = new Scene();

    // creates the camera
    // カメラ(ユーザーの視点)の作成
    camera = new PerspectiveCamera(75, canvasWidth / canvasHeight);
    camera.position.z = 15;
    camera.position.y = 13;
    camera.position.x = 8;

    // define the color of light
    // ライトの色を設定
    const lightColor = 0xffffff;

    // create the ambientLight(環境光の作成)
    const ambientLight = new AmbientLight(lightColor, 0.5);
    scene.add(ambientLight);

    // create the directional light
    // 指向性ライトの作成
    const directionalLight = new DirectionalLight(lightColor, 1);
    directionalLight.position.set(0, 10, 0);
    directionalLight.target.position.set(-5, 0, 0);
    scene.add(directionalLight);
    scene.add(directionalLight.target);

    // create the renderer
    // レンダラの作成
    renderer = new WebGLRenderer({
      canvas,
      alpha: true,
    });
    renderer.setSize(canvasWidth, canvasHeight);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

    // create the grid
    // グリッドの生成
    const grid = new GridHelper(50, 30);
    scene.add(grid);

    // add the axes helper
    // AxesHelperの追加
    const axes = new AxesHelper();
    (axes.material as Material).depthTest = false;
    axes.renderOrder = 1;
    scene.add(axes);

    // create the orbit controls
    // オービットコントロール(シーンをナビゲートするためのもの)の作成
    const controls = new OrbitControls(camera, canvas);
    controls.enableDamping = true;
    controls.target.set(-2, 0, 0);

    // set animation loop
    // アニメーションループ
    const animate = () => {
      controls.update();
      renderer.render(scene, camera);
      requestAnimationFrame(animate);
    };
    animate();
  });

  // check initialization of the canvas
  // canvasの初期化の確認
  const isReadyThreeCanvas = (): boolean => {
    return !!scene && !!camera && !!renderer;
  };

  // update the camera setting for change of the canvas size
  // canvasのサイズが変更された時にカメラの更新をする
  const onResizeCanvas = () => {
    if (!isReadyThreeCanvas()) return;

    camera.aspect = canvasWidth / canvasHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(canvasWidth, canvasHeight);
  };

  // event handler of changing size of the canvas
  // canvasのサイズが変わった際のイベントハンドラー
  $: canvasWidth, onResizeCanvas();
  $: canvasHeight, onResizeCanvas();
</script>

<Card outline tight style="width: {width}; height: {height};">
    <div class="three-canvas-container" bind:clientWidth={canvasWidth} bind:clientHeight={canvasHeight} >
        <canvas bind:this={canvas} ></canvas>
    </div>
</Card>

<style>
.three-canvas-container {
    width: 100%;
    height: 100%;
}
</style>

エントリとなっているApp.svelteも記述を書き換えて

App.svelte
<script lang="ts">
  import Viewer from './lib/Viewer.svelte';
  import {Divider} from 'attractions';
</script>

<main>
  <h1>IFC.js Example on Svelte</h1>
  <Divider text="Example" style="margin-bottom: 20px"/>
  <Viewer />
</main>

<style>
  #three-canvas {
    position: fixed;
    top: 0;
    left: 0;
    outline: none;
  }

  #file-input {
    z-index: 1;
    position: absolute;
  }
</style>

yarn devを実行するとグリッドが表示されるようになっているかと思います。

グリッドが表示されたcanvasが見れるようになっているかと思います。

IFCローダー

web-ifc-threeからIFCLoaderをインポートして、アップロードされてくるファイルをIFCLoaderに流し込むだけです。

IFCLoaderのloadの第2引数に、ifcModelというThree.jsの形式になった情報を渡されるcallbackが指定できます。

そのcallbackの中で、sceneに対してモデルのmeshを渡すことでモデルの描画が行われます。

import {IFCLoader} from 'web-ifc-three/IFCLoader';  
const ifcURL = URL.createObjectURL(file);
const ifcLoader = new IFCLoader();
ifcLoader.load(ifcURL, (ifcModel) => scene.add(ifcModel.mesh));

これを組み込んだファイルのインプットフォームをコンポーネントとして準備します。

の前に、sceneや取込まれたモデルなど、Viewerコンポーネントと共有させたい変数があったりします。それらをcontextでやり取りするよう、contextの定義をしておきます。svelteのコンテキストの詳細は、公式にて。

IFC.ts
import type {IFCModel} from 'web-ifc-three/IFC/components/IFCModel';
import type {Scene} from 'three';
import type {IFCInfo} from './IFC';

export type IFCInfo = {
  file: File
  ifcModel: IFCModel
}

export type SceneContext = {
  getScene: () => Scene
  getIfcInfoList: () => IFCInfo[]
  pushIfcInfoList: (ifcInfo: IFCInfo) => void,
  clearIfcInfoList: () => void,
}

export const key = Symbol();

これらのコンテキストをVeiwer.svelteから与えるように記述を追加します。

Viewer.svelte
...

  setContext<SceneContext>(sceneKey, {
    getScene: () => scene,
    getIfcInfoList: () => ifcInfoLIst,
    pushIfcInfoList: (ifcInfo: IFCInfo) => ifcInfoLIst.push(ifcInfo),
    clearIfcInfoList: () => ifcInfoLIst.splice(0),
  });

  let scene: Scene;
  const ifcInfoLIst: IFCInfo[] = [];
  
...

これらのコンテキストを呼び出す形で、ファイルのインプットフォームコンポーネントを作ります。

IFCinput.svelte
<script lang="ts">
  import {getContext} from 'svelte';
  import {FileDropzone} from 'attractions';
  import {IFCLoader} from 'web-ifc-three/IFCLoader';
  import {key as sceneKey, SceneContext, IFCInfo} from './IFC';
  import type {Scene} from 'three';

  // viteがweb-ifc-three/IFC/components/IFCModelをresolveできず、exampleから取得
  import {IFCModel} from 'three/examples/jsm/loaders/IFCLoader';

  const {
    getScene,
    getIfcInfoList,
    pushIfcInfoList,
    clearIfcInfoList,
  } = getContext<SceneContext>(sceneKey);

  const addIFCModel = (scene: Scene, file: File, ifcModel: IFCModel) => {
    scene.add(ifcModel.mesh);
    pushIfcInfoList({file, ifcModel} as IFCInfo);
  };

  const resetIFCModels = (scene: Scene) => {
    const ifcInfoList = getIfcInfoList();
    ifcInfoList.forEach((ifcInfo) => {
      scene.remove(ifcInfo.ifcModel.mesh);
    });
    clearIfcInfoList(0);
  };

  // sets up the IFC loading
  // IFCの読み込み設定
  const ifcLoader = new IFCLoader();
  const onChange = (value: CustomEvent) => {
    const scene = getScene();
    resetIFCModels(scene);
    /*eslint-disable */
    value.detail.files.forEach((file: File) => {
      const ifcURL = URL.createObjectURL(file);
      ifcLoader.load(ifcURL, (ifcModel) => addIFCModel(scene, file, ifcModel));
    });
    /* eslint-enable */
  };

</script>

<FileDropzone accept=".ifc" on:change={onChange}>
    <div slot="empty-layer" style="justify-content: center;">
        <div class="title">
            {#if $$slots.wrongType}
                Incorrect file type
            {:else if $$slots.dragActive}
                Release to upload
            {:else}
                Upload IFC files
            {/if}
        </div>
    </div>
</FileDropzone>

<style global>
.file-dropzone {
    margin: 0 !important;
}
.empty-layer {
    justify-content: center;
}
</style>

フォームの設置と表示

あとは、Viewer.svelteにslotを追加して、このインプットフォームを置ける場所を作ります。

Viewer.svelte
...
    {#if $$slots.leftSidebar}
        <Card outline class="m-10" style="position: absolute; z-index: 2;">
            <slot name="leftSidebar" />
        </Card>
    {/if}
    {#if $$slots.rightSidebar}
        <Card outline class="m-10" style="position: absolute; z-index: 2; right: 10px">
            <slot name="rightSidebar" />
        </Card>
    {/if}
...
App.svelte
  <Viewer>
    <IFCInput slot="leftSidebar"/>
  </Viewer>

これで、IFCを表示できるようになりました。

▼アップロード前

▼アップロード後

web-ifc-viewerへの置き替え

冒頭に記載したようにweb-ifc-viewerにはViewerの機能が詰まっています。

そのため、ここまで書いたコードをweb-ifc-viewerのクラスに置き替えてしまいます。
onMountの中身を入れ替え、contextもsceneではなくIFCViewerAPIクラスを返すように書き換えてしまいます。

Viewer.svelte
...

  setContext<SceneContext>(sceneKey, {
    getViewer: () => viewer,
    getIfcInfoList: () => ifcInfoLIst,
    pushIfcInfoList: (ifcInfo: IFCInfo) => ifcInfoLIst.push(ifcInfo),
    clearIfcInfoList: () => ifcInfoLIst.splice(0),
  });
  
...
  onMount(() => {
    // create the Three.js scene
    // Three.jsのシーン作成
    viewer = new IfcViewerAPI({container: container});

    // add the axes helper
    // AxesHelperの追加
    viewer.addAxes();
  });
...

レベルの取得

モデルからレベルを取得するためには、以下のようにplaneというものを取得することで取得することができます。

await viewer.plans.computeAllPlanViews(modelID);
const planes = viewer.plans.planLists[modelID]

これを使って、レベル毎の断面を表示することも可能になります。

オブジェクトの情報の取得

オブジェクトの情報は、取得したIFCのモデルのIDとオブジェクトのIDを指定することで、得ることができます。

await viewer.IFC.getProperties(modelID, id, true, false);

オブジェクトのIDは、マウスのクリックから得たり、検索によって求めることができます。

▼ダブルクリックから取得

▼IDから検索

ジオメトリの取得

ジオメトリも同様にモデルのIDとオブジェクトのIDから取得できます。
後はThree.js側で操作してあげれば、いろんなことができるようになっています。

const ids = await viewer.IFC.getAllItemsOfType(0, IFCWALLSTANDARDCASE, false);
const subset = viewer.IFC.loader.ifcManager.createSubset({ modelID: 0, ids, removePrevious: true });
const edgesGeometry = new EdgesGeometry(subset.geometry);
const vertices = edgesGeometry.attributes.position.array;

これを基に、dxfファイルなどへの書き出しのロジックを作ることができるのです。

終わりに

まだ成熟していないDesign frameworkを使ったり、viewerのclassとsvelteでの値の管理方法がぶつかったりで、無駄な実装も多くなりましたが、svelte自体はactionに対しての処理がしやすいこともあり、カスタマイズ性の高いIFC.jsとの相性はいい方なのではないかと思います。

ここまでやったコードはこちらで公開してるので、気になる方は触ってみてください。

https://github.com/MASAMIKI/ifc-js-sample-app

Discussion