# bimatter-viewer-react

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

All about us you can see on our website [Bimatter](https://bimatter.ru/)

[Demo viewer](https://rkaeplive.github.io/bimatter-viewer-react/)

[GitHub](https://github.com/rkaeplive/bimatter-viewer-react)

[Documentation](https://rkaeplive.github.io/bimatter-viewer-react/api)

Vanila js viewer core: [bimatter-viewer](https://www.npmjs.com/package/bimatter-viewer?activeTab=code)

Write your issuse here: [GitHub](https://github.com/rkaeplive/bimatter-viewer-react/issues)

## 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

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

Install the viewer:

```bash
npm install bimatter-viewer-react
```

## Basic Usage

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

```text
public/
models/
    architecture.bmt
    structure.bmt
```

Replace `src/index.css` or add global sizing:

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

Then replace `src/App.tsx`:

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

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

export default App;
```

Run the app:

```bash
npm run dev
```

## IFC Models

```text
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:

```bash
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:

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

You can also load mixed model lists:

```tsx
<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:

```ts
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:

```tsx
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:

```ts
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:

```ts
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:

```ts
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:

```ts
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

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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.

```tsx
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:

```ts
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:

```ts
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.

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
<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:

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

## Stats And Profiling

Enable FPS stats and console timing logs:

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

Or via API:

```ts
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:

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

Disable it with a prop:

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

Or control it through the viewer API:

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

## Grid Axes

Control model grid axes through the viewer API:

```ts
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
