Skip to main content

Hider

Source

Copying and pasting? We've got you covered! You can find the full source code of this tutorial here.

📄 Managing Items Visibility


When reviewing a BIM model, seeing everything at once makes it hard to focus — walls block structure, MEP clutters architecture, and the floor you care about is buried under every other storey. Without a visibility system, controlling what's shown means manipulating Three.js objects directly for every feature that needs it. This tutorial covers isolating all elements of a given category (showing only those, hiding everything else), hiding a specific category while keeping the rest visible, and resetting all elements back to fully visible in one call. By the end, you'll have a reusable visibility control that any other component — classifier, finder, selection — can drive by passing a ModelIdMap.

🖖 Importing our Libraries

First things first, let's install all necessary dependencies to make this example work:

import Stats from "stats.js";
import * as BUI from "@thatopen/ui";
// You have to import * as OBC from "@thatopen/components"
import * as OBC from "../..";

🌎 Setting up a Simple Scene

To get started, let's set up a basic ThreeJS scene. This will serve as the foundation for our application and allow us to visualize the 3D models effectively:

const components = new OBC.Components();

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;

const container = document.getElementById("container")!;
world.renderer = new OBC.SimpleRenderer(components, container);
world.camera = new OBC.OrthoPerspectiveCamera(components);
await world.camera.controls.setLookAt(78, 20, -2.2, 26, -4, 25);

components.init();

🛠️ Setting Up Fragments

Now, let's configure the FragmentsManager. This will allow us to load models effortlessly and start manipulating them with ease:

// `FragmentsManager.getWorker()` fetches the matching worker for this library version from unpkg and returns a blob URL.
// You can also pass your own URL to `fragments.init(...)` if you'd rather host the worker yourself.
const workerUrl = await OBC.FragmentsManager.getWorker();
const fragments = components.get(OBC.FragmentsManager);
fragments.init(workerUrl);

world.camera.controls.addEventListener("update", () => fragments.core.update());

world.onCameraChanged.add((camera) => {
for (const [, model] of fragments.list) {
model.useCamera(camera.three);
}
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);
});

// Remove z fighting
fragments.core.models.materials.list.onItemSet.add(({ value: material }) => {
if (!("isLodMaterial" in material && material.isLodMaterial)) {
material.polygonOffset = true;
material.polygonOffsetUnits = 1;
material.polygonOffsetFactor = Math.random();
}
});

📂 Loading Fragments Models

With the core setup complete, it's time to load a Fragments model into our scene. Fragments are optimized for fast loading and rendering, making them ideal for large-scale 3D models.

Where can I find Fragment files?

You can use the sample Fragment files available in our repository for testing. If you have an IFC model you'd like to convert to Fragments, check out the IfcImporter tutorial for detailed instructions.

const fragPaths = [
"https://thatopen.github.io/engine_components/resources/frags/school_arq.frag",
"https://thatopen.github.io/engine_components/resources/frags/school_str.frag",
];

await Promise.all(
fragPaths.map(async (path) => {
const modelId = path.split("/").pop()?.split(".").shift();
if (!modelId) return null;
const file = await fetch(path);
const buffer = await file.arrayBuffer();
return fragments.core.load(buffer, { modelId });
}),
);

✨ Using The Hider Component

The Hider component is very handy, as it is the main tool used for isolating, hiding, or performing any other operation related to item visibility. First of all, let's get the instance:

const hider = components.get(OBC.Hider);

Starting with isolation, let's create a handy function that let's isolate items based on the category:

const isolateByCategory = async (categories: string[]) => {
// An OBC.ModelIdMap represents selections within the engine.
// Here, we are defining a selection of the loaded model
// that includes all items belonging to the specified category.
const modelIdMap: OBC.ModelIdMap = {};

const categoriesRegex = categories.map((cat) => new RegExp(`^${cat}$`));

for (const [, model] of fragments.list) {
const items = await model.getItemsOfCategories(categoriesRegex);
const localIds = Object.values(items).flat();
modelIdMap[model.modelId] = new Set(localIds);
}
await hider.isolate(modelIdMap);
};
Multi-Model Compatibility

You don't need to worry about making the Hider component work with multiple models, it handles this automatically (as do all other components) using the modelIdMap.

As you can see, it's quite straightforward. Now, let's create another helper function to hide items instead of isolating them:

const hideByCategory = async (categories: string[]) => {
const modelIdMap: OBC.ModelIdMap = {};

const categoriesRegex = categories.map((cat) => new RegExp(`^${cat}$`));

for (const [, model] of fragments.list) {
const items = await model.getItemsOfCategories(categoriesRegex);
const localIds = Object.values(items).flat();
modelIdMap[model.modelId] = new Set(localIds);
}

await hider.set(false, modelIdMap);
};
Working with ModelIdMaps

Managing the visibility with ModelIdMaps (selections in That Open Engine) becomes very powerful when combined with other components, such as the ItemsFinder. Check out the tutorial for that component!

Finally, you can easily reset the visibility of all items as follows:

const resetVisibility = async () => {
await hider.set(true);
};

We will use the @thatopen/ui library to add some simple and cool UI elements to our app. First, we need to call the init method of the BUI.Manager class to initialize the library:

BUI.Manager.init();

Now we will add some UI to play around with the actions in this tutorial. For more information about the UI library, you can check the specific documentation for it!

const categoriesDropdownTemplate = () => {
const onCreated = async (e?: Element) => {
if (!e) return;

const dropdown = e as BUI.Dropdown;

const modelCategories = new Set<string>();
for (const [, model] of fragments.list) {
const categories = await model.getItemsWithGeometryCategories();
for (const category of categories) {
if (!category) continue;
modelCategories.add(category);
}
}

for (const category of modelCategories) {
const option = BUI.Component.create(
() => BUI.html`<bim-option label=${category}></bim-option>`,
);
dropdown.append(option);
}
};

return BUI.html`
<bim-dropdown multiple ${BUI.ref(onCreated)}></bim-dropdown>
`;
};

const panel = BUI.Component.create<BUI.PanelSection>(() => {
const categoriesDropdownA = BUI.Component.create<BUI.Dropdown>(
categoriesDropdownTemplate,
);

const categoriesDropdownB = BUI.Component.create<BUI.Dropdown>(
categoriesDropdownTemplate,
);

const onIsolateCategory = async ({ target }: { target: BUI.Button }) => {
if (!categoriesDropdownA) return;
const categories = categoriesDropdownA.value;
if (categories.length === 0) return;
target.loading = true;
await isolateByCategory(categories);
target.loading = false;
};

const onHideCategory = async ({ target }: { target: BUI.Button }) => {
if (!categoriesDropdownB) return;
const categories = categoriesDropdownB.value;
if (categories.length === 0) return;
target.loading = true;
await hideByCategory(categories);
target.loading = false;
};

const onResetVisibility = async ({ target }: { target: BUI.Button }) => {
target.loading = true;
await resetVisibility();
target.loading = false;
};

return BUI.html`
<bim-panel active label="Hider Tutorial" class="options-menu">
<bim-panel-section style="width: 14rem" label="General">
<bim-button label="Reset Visibility" @click=${onResetVisibility}></bim-button>
</bim-panel-section>
<bim-panel-section label="Isolation">
${categoriesDropdownA}
<bim-button label="Isolate Category" @click=${onIsolateCategory}></bim-button>
</bim-panel-section>
<bim-panel-section label="Hiding">
${categoriesDropdownB}
<bim-button label="Hide Category" @click=${onHideCategory}></bim-button>
</bim-panel-section>
</bim-panel>
`;
});

document.body.append(panel);

And we will make some logic that adds a button to the screen when the user is visiting our app from their phone, allowing to show or hide the menu. Otherwise, the menu would make the app unusable.

const button = BUI.Component.create<BUI.PanelSection>(() => {
return BUI.html`
<bim-button class="phone-menu-toggler" icon="solar:settings-bold"
@click="${() => {
if (panel.classList.contains("options-menu-visible")) {
panel.classList.remove("options-menu-visible");
} else {
panel.classList.add("options-menu-visible");
}
}}">
</bim-button>
`;
});

document.body.append(button);

⏱️ Measuring the performance (optional)

We'll use the Stats.js to measure the performance of our app. We will add it to the top left corner of the viewport. This way, we'll make sure that the memory consumption and the FPS of our app are under control.

const stats = new Stats();
stats.showPanel(2);
document.body.append(stats.dom);
stats.dom.style.left = "0px";
stats.dom.style.zIndex = "unset";
world.renderer.onBeforeUpdate.add(() => stats.begin());
world.renderer.onAfterUpdate.add(() => stats.end());

🎉 Wrap up

That's it! Now you're able to manage item visibility in BIM models using the Hider component. You learned how to use the Hider component to isolate, hide, and reset visibility of items. Congratulations! Keep going with more tutorials in the documentation.