EditElements
Copying and pasting? We've got you covered! You can find the full source code of this tutorial here.
Editing BIM Elements 🪑
Architects and engineers frequently need to make quick adjustments — move a column, change a wall color, swap a door geometry — directly in the 3D viewer without round-tripping back to Revit or exporting a new IFC file. The Elements API bridges that gap: it wraps a selected element as editable Three.js meshes, lets you manipulate them with standard tools, and generates the edit requests automatically when you apply. This tutorial covers selecting an element and obtaining its editable mesh representation; attaching Three.js TransformControls to move and rotate the element at the global transform level and at the per-sample local transform level; changing sample materials and overriding sample geometry with a custom shape; applying all changes back to the Fragment model; and discarding changes to restore the original state. By the end, you’ll have a pattern for building interactive element editors that use familiar Three.js workflows and commit changes to the Fragment model in one step.
🖖 Importing our Libraries
First things first, let's install all necessary dependencies to make this example work:
import * as OBC from "@thatopen/components";
import Stats from "stats.js";
import * as THREE from "three";
import * as BUI from "@thatopen/ui";
import { TransformControls } from "three/examples/jsm/controls/TransformControls.js";
// You have to import * as FRAGS from "@thatopen/fragments"
import * as FRAGS from "../../../index";
🌎 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 container = document.getElementById("container") as HTMLDivElement;
const world = worlds.create<
OBC.ShadowedScene,
OBC.OrthoPerspectiveCamera,
OBC.SimpleRenderer
>();
world.scene = new OBC.ShadowedScene(components);
world.renderer = new OBC.SimpleRenderer(components, container);
world.camera = new OBC.OrthoPerspectiveCamera(components);
components.init();
world.scene.three.add(new THREE.AxesHelper());
world.camera.three.far = 10000;
world.renderer.three.shadowMap.enabled = true;
world.renderer.three.shadowMap.type = THREE.VSMShadowMap;
world.scene.setup({
shadows: {
cascade: 1,
resolution: 2048,
},
});
await world.scene.updateShadows();
world.camera.controls.addEventListener("rest", async () => {
await world.scene.updateShadows();
});
Not necessarily! While @thatopen/components simplifies the process of setting up a scene, you can always use plain ThreeJS to create your own custom scene setup. It's entirely up to your preference and project requirements! 😉
🛠️ Setting Up Fragments
Now, let's configure the Fragments library core. This will allow us to load models effortlessly and start manipulating them with ease:
// `FragmentsModels.getWorker()` fetches the matching worker for this library version from unpkg and returns a blob URL.
// You can also pass your own URL to `new FragmentsModels(...)` if you'd rather host the worker yourself.
const workerUrl = await FRAGS.FragmentsModels.getWorker();
const fragments = new FRAGS.FragmentsModels(workerUrl);
world.camera.controls.addEventListener("control", () => fragments.update());
// Remove z fighting
fragments.models.materials.list.onItemSet.add(({ value: material }) => {
if (!("isLodMaterial" in material && material.isLodMaterial)) {
material.polygonOffset = true;
material.polygonOffsetUnits = 1;
material.polygonOffsetFactor = Math.random();
}
});
// Once a model is available in the list, we can tell what camera to use
// in order to perform the culling and LOD operations.
// Also, we add the model to the 3D scene.
fragments.models.list.onItemSet.add(({ value: model }) => {
model.useCamera(world.camera.three);
world.scene.three.add(model.object);
// At the end, you tell fragments to update so the model can be seen given
// the initial camera position
// We will also set up the shadows of all the loaded models here
model.tiles.onItemSet.add(({ value: mesh }) => {
if ("isMesh" in mesh) {
const mat = mesh.material as THREE.MeshStandardMaterial[];
if (mat[0].opacity === 1) {
mesh.castShadow = true;
mesh.receiveShadow = true;
}
}
});
});
📂 Loading a Fragments Model
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.
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 fetched = await fetch("https://thatopen.github.io/engine_fragment/resources/frags/school_arq.frag");
const buffer = await fetched.arrayBuffer();
const model = await fragments.load(buffer, {
modelId: "medium_test",
camera: world.camera.three,
});
world.scene.three.add(model.object);
await fragments.update(true);