Skip to content

Commit

Permalink
feat: improve tiles, make tutorial
Browse files Browse the repository at this point in the history
  • Loading branch information
agviegas committed Mar 1, 2024
2 parents c758266 + 652ecd7 commit eedaa55
Show file tree
Hide file tree
Showing 8 changed files with 5,990 additions and 329 deletions.
5,703 changes: 5,484 additions & 219 deletions resources/openbim-components.js

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions resources/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,10 @@ textarea {
display: inline-flex;
}

.obc-viewer :is(.table){
display: table;
}

.obc-viewer :is(.grid){
display: grid;
}
Expand Down
293 changes: 229 additions & 64 deletions src/fragments/FragmentIfcStreamer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -74,86 +74,219 @@
customEffects.excludedMeshes.push(grid.get());

/*MD
### 🔥🚀 Let's go BIG
### 💪 Let's go BIG
___
Some BIM models are really heavy, so displaying them using normal methods is not possible.
Luckily for you, we have created a system that can load gigabytes of IFC even in modest
devices: BIM Tiles! 🧩
Do you need to open huge big IFC files fast, even on more modest devices?
If so, you are in luck! We can open virtually any model on any device in
seconds thanks to BIM TILES!
:::info Are you using a library/framework like React, Angular or vue?
:::info BIM tiles?
Then probably you have other mechanisms for getting a reference to the
HTML div element. However you do it, as long as you get a reference
in your code, you are good to go!
The idea behind BIM tiles is pretty simple! Instead of loading the whole BIM
model at once, we just load the explicit geometries that are seen by the user.
It's way faster than opening the IFC directly, but for this you'll need
a backend (or to rely on the file system of the user if you are building a
desktop or mobile app).
:::
Let's see how to do this step by step!
### 🧩 Converting the IFC model to tiles
___
The first step is to transform the IFC model into BIM tiles. The reason why we
have to do this is pretty simple: geometry in IFC is implicit (e.g. a wall is
defined as an extrusion). This means that it needs to be computed and converted
to explicit geometry (triangles) so that it can be displayed in 3D. Let's start
converting the IFC geometry:
*/

// GEOMETRY STREAMING - OK
const streamer = new OBC.FragmentIfcStreamConverter(components);
streamer.settings.wasm = {
path: "https://unpkg.com/web-ifc@0.0.51/",
absolute: true
}

// const streamer = new OBC.FragmentIfcStreamConverter(components);
// streamer.settings.wasm = {
// path: "https://unpkg.com/web-ifc@0.0.51/",
// absolute: true
// }
//
// loader.culler.threshold = 10;
//
// const fetchedIfc = await fetch("../../../resources/small.ifc");
// const ifcBuffer = await fetchedIfc.arrayBuffer();
//
// streamer.onGeometryStreamed.add((geometry) => {
// console.log(geometry);
// });
//
// streamer.onAssetStreamed.add((assets) => {
// console.log(assets);
// });
//
// streamer.onIfcLoaded.add(async (groupBuffer) => {
// console.log(groupBuffer);
// })
//
// streamer.onProgress.add((progress) => {
// console.log(progress);
// })
//
// streamer.streamFromBuffer(new Uint8Array(ifcBuffer));
/*MD
The `FragmentIfcStreamConverter` class takes IFC files and transform them into
tiles. You can use events to get the data. The `onGeometryStreamed` event will
give you the geometries bundled in a binary file, as well as an object with
information about the geometries contained within this file.
*/

// PROPERTIES STREAMING - OK
streamer.onGeometryStreamed.add((geometry) => {
console.log(geometry);
});

// const fetchedIfc = await fetch("../../../resources/small.ifc");
// const ifcBuffer = await fetchedIfc.arrayBuffer();
//
// const propsStreamer = new OBC.FragmentPropsStreamConverter(components);
//
// propsStreamer.settings.wasm = {
// path: "https://unpkg.com/web-ifc@0.0.51/",
// absolute: true
// }
//
// propsStreamer.streamFromBuffer(new Uint8Array(ifcBuffer));
/*MD
You can control the amount of geometries inside a file using the settings. The
way the streaming works can't guarantee a precise number of geometries within a file,
but in most cases it will be quite close to the given number.
*/

// propsStreamer.onPropertiesStreamed.add(async (props) => {
// console.log(props);
// })
streamer.settings.minGeometrySize = 20;

// propsStreamer.onProgress.add(async (progress) => {
// console.log(progress);
// })
//
// propsStreamer.onIndicesStreamed.add(async (props) => {
// console.log(props);
// })
/*MD
Similarly, you can get the assets data and control the number of assets per chunk like this:
*/

streamer.onAssetStreamed.add((assets) => {
console.log(assets);
});

streamer.settings.minAssetsSize = 1000;

/*MD
Just like when using the normal `FragmentIfcLoader`, when you stream an IFC file you are
creating a `FragmentsGroup`. Using this event, you can get it:
*/


streamer.onIfcLoaded.add(async (groupBuffer) => {
console.log(groupBuffer);
})

/*MD
Finally, you can use this to get notified as the streaming process progresses:
*/

streamer.onProgress.add((progress) => {
console.log(progress);
})

/*MD
With everything in place, it's time to stream the IFC file and get all the tiles!
*/

// STREAM MODEL LOADING - OK
const fetchedIfc = await fetch("../../../resources/small.ifc");
const ifcBuffer = await fetchedIfc.arrayBuffer();
streamer.streamFromBuffer(new Uint8Array(ifcBuffer));

/*MD
### 📋 Streaming the properties
___
You can also stream the properties of an IFC file. Why? Because some files can have
millions of properties, and trying to save them naively in a normal DB is not very
scalable/affordable. Using this system, you'll be able to store and retrieve the
data of models of any size without big cloud costs. We can do this conversion
using the `FragmentPropsStreamConverter`:
*/


const propsStreamer = new OBC.FragmentPropsStreamConverter(components);

propsStreamer.settings.wasm = {
path: "https://unpkg.com/web-ifc@0.0.51/",
absolute: true
}

/*MD
Similarly to geometries, here you will also get data and progress notification
using events. In addition to properties, you will get `indices`, which is an
indexation data of the properties to be able to use them effectively when
streamed.
*/


propsStreamer.onPropertiesStreamed.add(async (props) => {
console.log(props);
})

propsStreamer.onProgress.add(async (progress) => {
console.log(progress);
})

propsStreamer.onIndicesStreamed.add(async (props) => {
console.log(props);
})

/*MD
Just call the `streamFromBuffer` method and you are ready to go!
*/

propsStreamer.streamFromBuffer(new Uint8Array(ifcBuffer));

/*MD
### 🧱 Assembling the data
___
Now you are ready to start streaming your data. The first step is to create 2 JSON
files so that the library can access your backend: one for the geometries and other for
the properties. You have examples of both JSONs [here](https://github.com/ThatOpen/engine_components/blob/main/resources/small.ifc-processed.json)
and [here](https://github.com/ThatOpen/engine_components/blob/main/resources/small.ifc-processed-properties.json).
The JSON file for the geometries should look like this, and you should
be able to create them with the data given in the previous steps. The `globalDataIDFile`
is expected to have a name that ends with `-global`.
```ts
interface StreamedGeometries {
assets: {
id: number;
geometries: {
color: number[];
geometryID: number;
transformation: number[];
}[];
}[];
geometries: {
[id: number]: {
boundingBox: {[id: number]: number};
hasHoles: boolean;
geometryFile: "url-to-geometry-file-in-your-backend";
};
};
globalDataFileId: "url-to-fragments-group-file-in-your-backend";
}
```
The JSON for geometries should look like this. The values in `types` and in `ids`
are the suffix of the name of the properties file in your backend. In other words,
the library expects that if your globalDataFile is called `small.ifc-global`, the
properties files will be called `small.ifc-properties-1`, `small.ifc-properties-2`,
etc. In other words: `types: {1837: [1, 2], ...}, ids: {8: 1, ...}` means that
all the items of type `1837` are in the files `small.ifc-properties-1` and
`small.ifc-properties-2`, and that the properties of the item with id 8 is in the
file `small.ifc-properties-1`.
```ts
interface StreamedProperties {
types: {
[typeID: number]: number[]
};
ids: {
[id: number]: {
boundingBox: {[id: number]: number};
hasHoles: boolean;
geometryFile: "url-to-geometry-file-in-your-backend";
};
};
indexesFile: "url-to-indexes-file-in-your-backend";
}
```
Once you get both files, you are ready to start streaming!
### 🧱 Streaming the data
___
Now, streaming the data is quite easy once you have the JSON files.
You can just instantiate the loader, give it the base URL to your
backend and just load the models like this:
*/

let loader = new OBC.FragmentStreamLoader(components);
loader.url = "THE_URL_TO_YOUR_BACKEND";
loader.url = "http://YOUR_BACKEND_URL";
let fragments = new OBC.FragmentManager(components);


async function loadModel(geometryURL, propertiesURL) {
const rawGeometryData = await fetch(geometryURL);
const geometryData = await rawGeometryData.json();
Expand All @@ -167,16 +300,48 @@

await loadModel(
"../../../resources/small.ifc-processed.json",
// "../../../resources/small.ifc-processed-properties.json"
"../../../resources/small.ifc-processed-properties.json"
);

// loader.culler.maxHiddenTime = 3000;
// loader.culler.maxLostTime = 10000;
/*MD
Now, streaming works by updating the scene depending on the user's perspective
and getting the necessary geometries from the backend. A simple way to achieve
this is by updating the scene each time the user stops the camera:
*/

components.camera.controls.addEventListener("controlend", () => {
loader.culler.needsUpdate = true;
});

/*MD
As you can imagine, downloading the geometries from the server each time can
take time, especially for heavier geometries. This is why the stream loader
automatically caches the files locally to get them much faster. This means that
the loading experience the first time might be a bit slower, but then later
it will be much better. You can control this using the `useCache` property
and clear the cache using the `clearCache()` method:
*/

loader.useCache = true;
await loader.clearCache();

/*MD
You can also customize the loader through the `culler` property:
- Threshold determines how bit an object must be in the screen to stream it.
- maxHiddenTime determines how long an object must be lost to remove it from the scene.
- maxLostTime determines how long an object must be lost to remove it from memory.
*/

loader.culler.threshold = 20;
loader.culler.maxHiddenTime = 1000;
loader.culler.maxLostTime = 40000;

/*MD
This is it! Now you should be able to stream your own IFC models and open them anywhere,
no matter how big they are! 💪 We will keep improving and making this API more powerful
to handle any model on any device smoothly.
*/

// DISPOSING ALL - OK

// async function dispose() {
Expand Down
Loading

0 comments on commit eedaa55

Please sign in to comment.