Open5

ThatOpenのIFCローダーをNext.jsで動かす

torikasyutorikasyu

Next.jsのプロジェクトを作成する

npx create-next-app@latest

モジュールのインストール

npm i @thatopen/components @thatopen/fragments web-ifc three

torikasyutorikasyu

wasmの設定

このスクラップに書いたように、Macだとwasmはローカルに配置しないと動作しないらしい。Next.jsの設定を変更してwasmをロードできるようにする、

next.config.ts

import type { NextConfig } from "next";
import path from "path";

const nextConfig: NextConfig = {
  outputFileTracingRoot: path.join(__dirname, './'),
  webpack: (config) => {
    // クライアント側でのfsやpathを使用不可にする
    config.resolve.fallback = {
      fs: false,
      path: false,
    }
    
    // WebAssemblyの非同期読み込み設定
    config.experiments = {
      asyncWebAssembly: true,
      layers: true,
    }
    return config
  },

  // WASMファイルのMIMEタイプ設定
  async headers() {
    return [
      {
        source: "/(.*).wasm",
        headers: [
          {
            key: "Content-Type",
            value: "application/wasm",
          },
        ],
      },
    ];
  },
};
export default nextConfig;
torikasyutorikasyu

wasmをローカルにコピーする

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

IfcLoaderExampleコンポーネントを作成する

'use client'

import * as OBC from "@thatopen/components"
import { useEffect, useRef, useState } from 'react'

export const IfcLoaderExample = () => {
    const containerRef = useRef<HTMLDivElement>(null)
    const [components, setComponents] = useState<OBC.Components | null>(null)
    const [isLoading, setIsLoading] = useState(false)

    useEffect(() => {
        const initScene = async () => {
            if (!containerRef.current) return

            // Components初期化
            const components = new OBC.Components()
            
            // World設定
            const worlds = components.get(OBC.Worlds)
            const world = worlds.create<
                OBC.SimpleScene,
                OBC.OrthoPerspectiveCamera,
                OBC.SimpleRenderer
            >()

            world.scene = new OBC.SimpleScene(components)
            world.scene.setup()
            world.scene.three.background = null

            // Renderer設定
            world.renderer = new OBC.SimpleRenderer(components, containerRef.current)
            world.camera = new OBC.OrthoPerspectiveCamera(components)
            await world.camera.controls.setLookAt(78, 20, -2.2, 26, -4, 25)

            components.init()

            // Grid追加
            components.get(OBC.Grids).create(world)

            // IFCLoader設定(ローカル指定)
            const ifcLoader = components.get(OBC.IfcLoader)
            await ifcLoader.setup({
                autoSetWasm: false,
                wasm: {
                    path: "/",
                    absolute: true,
                }
            })

            // FragmentsManager設定
            const githubUrl = "https://thatopen.github.io/engine_fragment/resources/worker.mjs"
            const fetchedUrl = await fetch(githubUrl)
            const workerBlob = await fetchedUrl.blob()
            const workerFile = new File([workerBlob], "worker.mjs", {
                type: "text/javascript",
            })
            const workerUrl = URL.createObjectURL(workerFile)
            const fragments = components.get(OBC.FragmentsManager)
            fragments.init(workerUrl)

            // カメラが停止しているときにフラグメントを更新
            world.camera.controls.addEventListener("rest", () =>
                fragments.core.update(true)
            )

            // フラグメント追加時の処理
            fragments.list.onItemSet.add(({ value: model }) => {
                model.useCamera(world.camera.three)
                world.scene.three.add(model.object)
                fragments.core.update(true)
            })

            // State更新
            setComponents(components)
        }

        initScene()
    }, [])

    const loadExampleIfc = async () => {
        if (!components) return
        
        setIsLoading(true)
        try {
            const ifcLoader = components.get(OBC.IfcLoader)
            const file = await fetch("https://thatopen.github.io/engine_components/resources/ifc/school_str.ifc")
            const data = await file.arrayBuffer()
            const buffer = new Uint8Array(data)
            await ifcLoader.load(buffer, false, "example", {
                processData: {
                    progressCallback: (progress) => console.log("Loading progress:", progress),
                },
            })
        } catch (error) {
            console.error("Error loading IFC:", error)
        } finally {
            setIsLoading(false)
        }
    }
    return (
        <div style={{ position: "relative", width: "100%", height: "600px" }}>
            <div 
                ref={containerRef} 
                style={{ width: "100%", height: "100%", border: "1px solid #ccc" }}
            />
            
            <div style={{ 
                position: "absolute", 
                top: "10px", 
                right: "10px", 
                background: "white", 
                padding: "10px", 
                borderRadius: "5px",
                boxShadow: "0 2px 10px rgba(0,0,0,0.1)"
            }}>
                <button 
                    onClick={loadExampleIfc} 
                    disabled={isLoading}
                    style={{ 
                        padding: "8px 16px", 
                        marginBottom: "8px",
                        backgroundColor: isLoading ? "#ccc" : "#007bff",
                        color: "white",
                        border: "none",
                        borderRadius: "4px",
                        cursor: isLoading ? "not-allowed" : "pointer",
                        display: "block",
                        width: "100%"
                    }}
                >
                    {isLoading ? "Loading..." : "Load Example IFC"}
                </button>
                
                {isLoading && <p style={{ color: "#007bff", fontSize: "12px" }}>Loading...</p>}
            </div>
        </div>
    )
}