Svelte × Typescript × IFC.jsでIFCをWeb上に表示
※今回の記事は、IFCを触る建築業界の人向けに書いてます。
IFC.jsとは
IFC.jsは、ブラウザでIFCモデルをロード・表示・編集するためのライブラリです。.js
とは書いてありますが、javascriptだけではなく、cなどの言語も含まれたライブラリです。
Ansoniを中心とした、エンジニアのグループが開発しているOSSで、BIMの開発技術が閉じているため、誰でも開発できる環境を作りたいという目的で開発が進められているライブラリです。
IFC.jsは、以下の2つのを可能にしてくれます。
-
ジオメトリの表示
IFC.js は、Three.jsやBabylon.jsなどの3Dライブラリに対応するように作られており、 -
属性データの抽出
ジオメトリに関連するすべてのプロパティの取得を可能にしてくれます。 -
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に参加したい方で無い限り…。
web-ifc-three: ローダー
web-ifcのデータをロードし、Three.jsのシーン(ビュー)を生成するライブラリです。利用するときは、このパッケージだけをインストールすれば、WebAssembly化されたweb-ifcも引っ張ってくるので、実質このライブラリが利用する側にとってはメインのライブラリとなります。
web-ifc-viewer: ビューワー操作機能集
web-ifc-threeで生成されたシーン(ビューワー) 上で、IFCを操作する機能を詰め込んでいるライブラリです。web-ifc-threeをラップしたコードとなっており、細かい処理を記載しなくてても我々がBIMの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の設定
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を使ってみようと思います。
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.wasm
とweb-ifc-mt.wasm
をpublic
ディレクトリにコピーします。
すでにパッケージ内に、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をセットアップするするコードを記載します。
<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も記述を書き換えて
<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のコンテキストの詳細は、公式にて。
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から与えるように記述を追加します。
...
setContext<SceneContext>(sceneKey, {
getScene: () => scene,
getIfcInfoList: () => ifcInfoLIst,
pushIfcInfoList: (ifcInfo: IFCInfo) => ifcInfoLIst.push(ifcInfo),
clearIfcInfoList: () => ifcInfoLIst.splice(0),
});
let scene: Scene;
const ifcInfoLIst: IFCInfo[] = [];
...
これらのコンテキストを呼び出す形で、ファイルのインプットフォームコンポーネントを作ります。
<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を追加して、このインプットフォームを置ける場所を作ります。
...
{#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}
...
<Viewer>
<IFCInput slot="leftSidebar"/>
</Viewer>
これで、IFCを表示できるようになりました。
▼アップロード前
▼アップロード後
web-ifc-viewerへの置き替え
冒頭に記載したようにweb-ifc-viewerにはViewerの機能が詰まっています。
そのため、ここまで書いたコードをweb-ifc-viewerのクラスに置き替えてしまいます。
onMountの中身を入れ替え、contextもsceneではなくIFCViewerAPIクラスを返すように書き換えてしまいます。
...
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との相性はいい方なのではないかと思います。
ここまでやったコードはこちらで公開してるので、気になる方は触ってみてください。
Discussion