npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bimatter-viewer-react

v0.8.5

Published

React viewer for Bimatter `.bmt` models and `.ifc` models.

Readme

bimatter-viewer-react

React viewer for Bimatter .bmt models and .ifc models.

All about us you can see on our website Bimatter

Demo viewer

GitHub

Documentation

Vanila js viewer core: bimatter-viewer

Write your issuse here: GitHub

License

bimatter-viewer-react is licensed under PolyForm-Noncommercial-1.0.0.

You may use it for testing, evaluation, research, education, and personal noncommercial projects. Commercial use requires separate written permission.

Create A New React + Vite + TypeScript Project

npm create vite@latest bimatter-viewer-demo -- --template react-ts
cd bimatter-viewer-demo
npm install

Install the viewer:

npm install bimatter-viewer-react

Basic Usage

Put your .bmt or .ifc files in public/models:

public/
models/
    architecture.bmt
    structure.bmt

Replace src/index.css or add global sizing:

html,
body,
#root {
    height: 100%;
    margin: 0;
}

Then replace src/App.tsx:

import { Viewer } from "bimatter-viewer-react";

function App() {
    return (
        <Viewer
            modelUrls={["/models/architecture.bmt", "/models/structure.bmt"]}
        />
    );
}

export default App;

Run the app:

npm run dev

IFC Models

public/
    ifc-parser.wasm
models/
    architecture.ifc

For IFC loading you need to copy ifc parser wasm file into your public folder. You can take it from bimatter-viewer-react/public/ifc-parser.wasm or by command:

cp node_modules/bimatter-viewer-react/public/ifc-parser.wasm public/ifc-parser.wasm

After that, IFC models can be loaded the same way as BMT models:

<Viewer modelUrls={["/models/architecture.ifc"]} />

You can also load mixed model lists:

<Viewer
    modelUrls={[
        "/models/architecture.bmt",
        "/models/structure.bmt",
        "/models/architecture.ifc",
    ]}
/>

IFC spaces are loaded by default when the IFC contains room geometry, but they are hidden in the viewer by default. Disable loading them when room volumes are not needed:

await viewerRef.current?.models.loadModels(["/models/architecture.ifc"], {
    clearViewer: true,
    useIfcSpace: false,
});

Loader API

You can load models through the viewer API and keep parsed data in your app state:

import { useState } from "react";
import {
    Viewer,
    type ViewerApi,
    type ViewerLoadedModels,
} from "bimatter-viewer-react";

function App() {
    const [modelsData, setModelsData] = useState<ViewerLoadedModels>({});

    function loadInitialModels(viewer: ViewerApi) {
        void viewer.models.loadModels(
            ["/models/architecture.bmt", "/models/structure.bmt"],
            {
                clearViewer: true,
                onModelsDataChange: setModelsData,
            },
        );
    }

    return <Viewer modelsData={modelsData} onReady={loadInitialModels} />;
}

viewer.models.loadModels accepts paths or browser File objects:

await viewerRef.current?.models.loadModels(["/models/model.bmt"]);
await viewerRef.current?.models.loadModels(["/models/model.ifc"]);
await viewerRef.current?.models.loadModels(["/models/model.ifc"], {
    useIfcSpace: false,
});
await viewerRef.current?.models.loadModels(["/models/model.bmt"], {
    materialMode: "performance",
});
await viewerRef.current?.models.loadModels(["/models/model.bmt"], {
    useDoubleSideMaterial: true,
});
await viewerRef.current?.models.loadModels([
    "/models/model.bmt",
    "/models/model.ifc",
]);
await viewerRef.current?.models.loadModels(files);

materialMode: "performance" uses a cheaper unlit material. Use "quality" or omit it for the default lit material.

useDoubleSideMaterial is disabled by default for better FPS. Enable it only when the model needs back faces to be visible.

Use useWorker to parse BMT and IFC models outside the main thread:

const models = await viewerRef.current?.models.loadModels(
    ["/models/model.ifc"],
    {
        clearViewer: true,
        chunk: 200,
        onModelsDataChange: setModelsData,
        useWorker: true,
        onModelProgress: (event) => {
            if (!event) return;
            console.log(event.phase, Math.round(event.progress * 100));
        },
        onChunk: (chunk) => {
            console.log(chunk.modelID, chunk.geometryID);
        },
    },
);

IFC worker loading uses the same parser wasm file. By default, ifc-parser.wasm is resolved relative to the app document base URL, so it works with Vite base paths like /my-app/ when the file is in that app's public folder. Pass a custom wasmPath only when the file is hosted elsewhere:

await viewerRef.current?.models.loadModels(["/models/model.ifc"], {
    useWorker: true,
    wasmPath: "/ifc-parser.wasm",
});

For IFC, chunk controls how many geometries are packed before a streamed mesh is emitted. For BMT, chunks are read as they are already stored in the file.

When you render chunks yourself in onChunk, keep collectWorkerChunks: false to avoid keeping the same streamed geometry in the loader helper until the final result:

await viewerRef.current?.models.loadModels(["/models/model.bmt"], {
    collectWorkerChunks: false,
    useWorker: true,
    onChunk: (chunk) => {
        // Store chunk.geometry in your own model state.
    },
});

File Upload Example

import { useRef, useState } from "react";
import {
    Viewer,
    type ViewerApi,
    type ViewerLoadedModels,
} from "bimatter-viewer-react";

function App() {
    const viewerRef = useRef<ViewerApi>(null);
    const [modelsData, setModelsData] = useState<ViewerLoadedModels>({});

    async function onFilesChange(files: FileList | null) {
        if (!files?.length) return;
        await viewerRef.current?.models.loadModels(Array.from(files), {
            clearViewer: true,
            onModelsDataChange: setModelsData,
        });
    }

    return (
        <>
            <input
                accept=".bmt,.ifc"
                multiple
                type="file"
                onChange={(event) => onFilesChange(event.target.files)}
            />
            <Viewer ref={viewerRef} modelsData={modelsData} />
        </>
    );
}

Geometry Utils

Use ref to call viewer actions:

import { useRef } from "react";
import { Viewer, type ViewerApi } from "bimatter-viewer-react";

function App() {
    const viewerRef = useRef<ViewerApi>(null);

    return (
        <>
            <button onClick={() => viewerRef.current?.camera.fitCamera()}>
                Fit
            </button>
            <button
                onClick={() => viewerRef.current?.geometryUtils.hideSelected()}
            >
                Hide selected
            </button>
            <button
                onClick={() =>
                    viewerRef.current?.geometryUtils.isolateSelected()
                }
            >
                Isolate selected
            </button>
            <button onClick={() => viewerRef.current?.geometryUtils.showAll()}>
                Show all
            </button>

            <Viewer ref={viewerRef} modelUrls={["/models/model.bmt"]} />
        </>
    );
}

IFC space visibility is a separate mesh-level layer. When spaces are disabled through setIfcSpacesVisibility(false), showAll() does not show them again; use setIfcSpacesVisibility(true) or toggleIIfcSpacesVisibility() for that.

Colorize Elements

Use viewerRef.current.colors to apply temporary color overrides to model elements. The color is applied by modelID and element ids:

viewerRef.current?.colors.setColor(0, [10, 20, 30], "#ff3355");
viewerRef.current?.colors.rebuildByColors({
    0: {
        "#ff3355": [10, 20, 30],
        "#3b82f6": [40, 50],
    },
    1: {
        "#22c55e": [10, 20],
    },
});
viewerRef.current?.colors.rebuildByColors(
    {
        0: {
            "#ff3355": [10, 20, 30],
            "#3b82f6": [40, 50],
        },
    },
    "#d1d5db",
);
viewerRef.current?.colors.rebuildModelByColors(
    0,
    {
        "#ff3355": [10, 20, 30],
        "#3b82f6": [40, 50],
    },
    "#d1d5db",
);
viewerRef.current?.colors.clearColor(0, [10, 20]);
viewerRef.current?.colors.clearModelColors(0);
viewerRef.current?.colors.clearAllColors();

Example with current selection:

import { useRef, useState } from "react";
import {
    Viewer,
    type ViewerApi,
    type ViewerSelection,
} from "bimatter-viewer-react";

function App() {
    const viewerRef = useRef<ViewerApi>(null);
    const [selected, setSelected] = useState<ViewerSelection>({});

    function colorizeSelected() {
        Object.entries(selected).forEach(([modelID, ids]) => {
            viewerRef.current?.colors.setColor(Number(modelID), ids, "#ff3355");
        });
    }

    return (
        <>
            <button onClick={colorizeSelected}>Colorize selected</button>
            <button onClick={() => viewerRef.current?.colors.clearAllColors()}>
                Clear colors
            </button>
            <Viewer
                ref={viewerRef}
                modelUrls={["/models/model.bmt"]}
                selected={selected}
                onSelectedChange={setSelected}
            />
        </>
    );
}

Convert To BMT

Use viewerRef.current.converter to export loaded models to the BMT format. With activeView: true, the converter uses the current viewer state and exports only visible elements.

import { useRef } from "react";
import { Viewer, type ViewerApi } from "bimatter-viewer-react";

function downloadFiles(files: { blob: Blob; name: string }[]) {
    files.forEach((file) => {
        const url = URL.createObjectURL(file.blob);
        const link = document.createElement("a");

        link.href = url;
        link.download = file.name;
        link.click();
        URL.revokeObjectURL(url);
    });
}

function App() {
    const viewerRef = useRef<ViewerApi>(null);

    function convertToBmt() {
        const result = viewerRef.current?.converter.convertToBmt({
            activeView: true,
            fileName: "model",
            useMinVersion: true,
        });

        if (result) {
            downloadFiles(result.files);
        }
    }

    return (
        <>
            <button onClick={convertToBmt}>Export BMT</button>
            <Viewer ref={viewerRef} modelUrls={["/models/model.ifc"]} />
        </>
    );
}

Converter options:

viewerRef.current?.converter.convertToBmt({
    activeView: true,
    fileName: "model",
    fileNames: {
        0: "architecture",
        1: "structure",
    },
    filterElement: ({ modelID, elementID }) => {
        return modelID === 0 && elementID !== 10;
    },
    useMinVersion: true,
});

activeView exports only currently visible elements. This includes hide/isolate state because the viewer passes the current scene state to the converter.

useMinVersion exports name.min.bmt plus name_props.json. Without it, props and structure are written into one .bmt file.

fileName sets a base name for exported files. fileNames sets names per modelID.

filterElement is an optional custom element filter. Return true to keep an element in the exported BMT.

IFC file conversion is also available through the same API:

await viewerRef.current?.converter.convertIfcFileToBmt(files, {
    fileName: "converted_ifc",
    useIfcColors: true,
    useIfcElementAssembly: true,
    useMinVersion: true,
});

useIfcColors, useIfcElementAssembly, and useIfcSpace are used when converting IFC files directly. They do not change already loaded model data in convertToBmt. useIfcSpace is enabled by default and loads IFCSPACE geometry as a separate almost-transparent gray mesh when the IFC contains room geometry.

Controlled Selection

Selection can be controlled by your app state, including Zustand, Redux, or local React state.

import { useState } from "react";
import { Viewer, type ViewerSelection } from "bimatter-viewer-react";

function App() {
    const [selected, setSelected] = useState<ViewerSelection>({});

    return (
        <Viewer
            modelUrls={["/models/model.bmt"]}
            selected={selected}
            onSelectedChange={setSelected}
        />
    );
}

Filtered Elements Collector

Use selector.collector() to query element properties from loaded models:

import type { IfcClass } from "bimatter-viewer-react";

const wallType: IfcClass = "IfcWall";

const walls = viewerRef.current?.selector
    .collector()
    .ofType(wallType)
    .toElements();

const basicWallIds = viewerRef.current?.selector
    .collector()
    .ofModel(0)
    .ofType("IfcWall")
    .Where((element) => element.props.Name.startsWith("basic wall"))
    .toElementsIds();

const levels = viewerRef.current?.properties.getAllLevels(true);
const firstLevel = levels?.[0]?.[0];

const firstLevelWallIds = firstLevel
    ? viewerRef.current?.selector
          .collector()
          .ofLevel(firstLevel)
          .ofType("IfcWall")
          .toElementsIds()
    : [];

Without ofModel(), toElements() returns { [modelID]: { elementID, props }[] } and toElementsIds() returns { [modelID]: number[] }.

With ofModel(0), toElements() returns { elementID, props }[] and toElementsIds() returns number[].

With ofLevel(level), toElements() returns { elementID, props }[] and toElementsIds() returns number[]. Use properties.getAllLevels(true) so each level includes elements.

ofType() accepts IfcClass | string, so TypeScript suggests IFC classes like "IfcWall" while still allowing custom type strings.

Using Colorize With Collectors

Collectors can be used directly as color inputs. This is useful when you want to highlight elements by IFC type, level, or property value:

const viewer = viewerRef.current;

viewer?.colors.rebuildModelByColors(
    0,
    {
        "#ff3355":
            viewer.selector
                .collector()
                .ofModel(0)
                .Where((element) => {
                    return (
                        viewer.properties.getParamValueByName(
                            element.props,
                            "System Name",
                            "Mechanical",
                        ) === "Mechanical Supply Air 2"
                    );
                })
                .toElementsIds() ?? [],
    },
    "#d1d5db",
);

You can also combine level filters with type filters:

const viewer = viewerRef.current;
const firstLevel = viewer?.properties.getAllModelLevels(0, true)[0];

if (viewer && firstLevel) {
    viewer.colors.rebuildModelByColors(
        0,
        {
            "#3b82f6": viewer.selector
                .collector()
                .ofLevel(firstLevel)
                .ofType("IfcWall")
                .toElementsIds(),
        },
        "#d1d5db",
    );
}

Performance Mode

Use performanceMode for heavy models or weaker GPUs:

<Viewer modelUrls={["/models/model.bmt"]} performanceMode />

It caps canvas DPR at 1, disables antialiasing, asks the browser for a high-performance GPU, and uses materialMode="performance" unless another material mode is passed.

You can also switch only the model material:

<Viewer modelUrls={["/models/model.bmt"]} materialMode="performance" />

Stats And Profiling

Enable FPS stats and console timing logs:

<Viewer modelUrls={["/models/model.bmt"]} showStats />

Or via API:

viewerRef.current?.utils.setShowStats(true);

When enabled, the viewer logs timing for selection, hide, isolate, show, and reset actions.

Navigation Cube

The navigation cube is enabled by default:

<Viewer modelUrls={["/models/model.bmt"]} />

Disable it with a prop:

<Viewer modelUrls={["/models/model.bmt"]} showNavCube={false} />

Or control it through the viewer API:

viewerRef.current?.utils.setShowNavCube(true);
viewerRef.current?.utils.toggleShowNavCube();

Grid Axes

Control model grid axes through the viewer API:

viewerRef.current?.utils.setShowGridAxes(true);
viewerRef.current?.utils.toggleShowGridAxes();
viewerRef.current?.utils.setGridAxisVisibility("right", false);
viewerRef.current?.utils.setGridAxesVisibility({
    bottom: true,
    left: true,
    right: false,
    top: false,
});

HotKeys

Selection

Left Click - Select element

Shift + Left Click - Multi-select element

Ctrl + Left Click - Toggle element selection

Shift + Drag Right - Select elements fully inside rectangle

Shift + Drag Left - Select elements intersected by rectangle

Ctrl + Drag Right - Unselect elements fully inside rectangle

Ctrl + Drag Left - Unselect elements intersected by rectangle

Visibility

C - Clear hidden / isolated elements

H - Hide selected elements

I - Isolate selected elements

Clipping

P - Create clipping plane by model intersection

ALT + Drag - Drag clipping plane

Dimensions

M - Toggle dimension drawing mode