ThatOpen / web-ifc-three

The official IFC Loader for Three.js.
https://ifcjs.github.io/info/
MIT License
517 stars 133 forks source link

Slow highlighting/hiding/showing on large models #110

Open axelra82 opened 2 years ago

axelra82 commented 2 years ago

I'm wondering what I'm missing in regards to createSubset, removeSubset and removeFromSubset and the intended use cases.

Some more complex models I've tested contain tens of thousands of IDs in total to check/use when highlighting parts of, or the entire, model. I've notice that around 2 000 IDs things start to get sluggish. Not to mention entire floors on a larger building model (with ~30 storeys), where I've logged upwards 13 000+ IDs, per storey.

So back to my question, are we really supposed to use the subsets this way, i.e. re-creating a subset every time? Isn't there a "better" (i.e. easier 😅) way of dealing with showing/hiding/highlighting parts of an IFC model? 🤔

Also, I've noticed that subsets have a visible value, which is more performant when highlighting parts of a model... however that requires a pre-existing subset, as again, creating large subsets if very heavy when it comes to larger models.

In short, what I'm trying (and have been trying for a couple of weeks now, with talks in different places for IFCjs, with some very helpful people) is to get all the IDs from a specific point in the spatial structure (including nodes with no geometry data, as these group the items, and are necessary to identify all items in a group, e.g. all Basic Walls in the Basic Wall group on a Storey are hidden/highlighted etc).

Then once I have all the IDs, I simple want to show/hide/highlight, without creating or removing a subset, as this doesn't work (in an acceptable way) when there are a huge number of IDs to handle. Also, the scene can contain multiple models, so there's already a lot going on for the browser, adding a "create/remove" subset with tens of thousand IDs with milliseconds appart just doesn't perform well 🤷‍♂️

I've looked a many different examples but haven't been able to find anything that deals with this specific scenario (or similar, that can translate well). At this point I'm also not worrying about/dealing with picking/highlighting using raycast etc. But only focusing on the interaction in the structured side menu view.

Side menu

This is shown when selecting model from list of models. When hovering each row it highlights (but larger sets can take up to 1-2 seconds to show highlight, same goes for show/hide entire storeys, groups or items).

––––––––––––––––––

––––––––––––––––––

I've gone over all the documentation on ifcjs.github.io, and while that's great for touching the surface of things and doing some "Hello world" examples, it (combined with my limited experience with IFC in general) falls short here. All the examples are more of a "proof of concept" thing, rather than complex scenarios with "heavy/large" real world models.

Any guidance or suggestions on how to optimize this would be greatly appreciated 🤓

I've tried a bunch of different ideas, but they all fall short in some aspect. One idea I've been trying out is to create the highlight subsets for storeys, groups and groupItems durring load of the IFC model. While that adds unwanted time to the loading process it does make it more responsive once it's done. However, this requires me to run a mess of javascript just to get the right structure.

Excerpt of setup

Below is an excerpt of the codebase I'm currently working with, trying to get the correct structure in place while loading and creating highlight subsets. This does not work for showing hiding however.

Dependencies

  [...]
  "three": "^0.135.0",
  "three-mesh-bvh": "^0.5.11",
  "vue": "^2.6.11",
  "vuex": "^3.4.0",
  "web-ifc-three": "^0.0.116"
  [...]
}

IFC loading and grouping

[...]
if (ifcModels.length > 0) {
  const ifcLoader = new IFCLoader();
  const ifcManager = ifcLoader.ifcManager;

  await ifcManager.useWebWorkers(true, "/IFCjs/IFCWorker.js");
  ifcManager.applyWebIfcConfig({
    COORDINATE_TO_ORIGIN: false,
    USE_FAST_BOOLS: true,
  });
  ifcManager.setupThreeMeshBVH(computeBoundsTree, disposeBoundsTree, acceleratedRaycast);
  // Doesn't appear to have any effect on "IFCSPACE".
  // await ifcManager.parser.setupOptionalCategories({
  //   [IFCSPACE]: false,
  // });

  commit("setIfcManager", ifcManager);

  await dispatch("loadModelTypes", {
    models: ifcModels,
    dispatcher: "loadIFC",
    loader: ifcLoader,
  });
}
[...]
[...]
const scene: Scene = getters["getScene"];
const ifcManager: IFCManager = getters["getIfcManager"];

commit("setLoadingProgressItem", { id, progress });
const modelBlob = await getBlobWithProgress(path, loadingProgress[id]);

const model: IFCModel = await loader.loadAsync(modelBlob);
const modelId = model.modelID;

const matrixArr = await ifcManager.ifcAPI.GetCoordinationMatrix(modelId);
const matrix = new Matrix4().fromArray(matrixArr);
ifcManager.setupCoordinationMatrix(matrix);

model.position.set(position.x, position.y, position.z);
model.scale.set(scale.x, scale.y, scale.z);
// When setting rotation on model it doesn't apply on subsets.
scene.rotation.set(
  (rotation.x + 90) * (Math.PI / 180),
  rotation.y * (Math.PI / 180),
  rotation.z * (Math.PI / 180)
);

const ids: number[] = await dispatch("ifcGetAllIds", modelId);
const createModelSubset = await dispatch("ifcCreateModelSubset", { modelId, ids });

const ifcSpatialStructure = await ifcManager.getSpatialStructure(modelId);
const ifcProject = ifcSpatialStructure.children[0];
const ifcProjectBuilding = ifcProject.children[0];
const ifcBuildingStoreys = await dispatch("ifcFilterSpaces", ifcProjectBuilding.children);

const storeyPromises: Promise<IFCStoreyInterface>[] = ifcBuildingStoreys.map(
  async (storeyItem: IFCNode) => {
    const { expressID: storeyId, children: storeyChildren } = storeyItem;
    const displayname = await dispatch("ifcDisplayname", { modelId, id: storeyId });
    const filteredGroups: IFCNode[] = await dispatch("ifcFilteredGroups", storeyChildren);
    // const filteredGroups: IFCNode[] = await dispatch("ifcGroups", storeyChildren);
    const storeyIds: number[] = [storeyId];

    const groupPromises: Promise<IFCStoreyGroupInterface>[] = Object.values(filteredGroups).map(
      async (groupItem): Promise<IFCStoreyGroupInterface> => {
        const { expressID: groupId, children: groupChildren } = groupItem;
        const groupDisplayname = await dispatch("ifcDisplayname", { modelId, id: groupId });
        const groupIds: number[] = [groupId];

        const groupItemPromises: Promise<IFCStoreyGroupItemsInterface>[] = groupChildren.map(
          async (item): Promise<IFCStoreyGroupItemsInterface> => {
            const { expressID: itemId, children: itemChildren } = item;
            const itemDisplayname = await dispatch("ifcDisplayname", { modelId, id: itemId });
            const groupItemIds: number[] = [];

            const hasChildren = await dispatch("ifcNodeHasChildren", itemChildren);
            if (hasChildren) {
              const getChildIds = await dispatch("ifcRecursiveChildren", item);
              groupItemIds.push(...getChildIds);
            } else {
              groupItemIds.push(itemId);
            }

            groupIds.push(...groupItemIds);

            const groupItems: IFCStoreyGroupItemsInterface = {
              id: itemId,
              ids: groupItemIds,
              displayname: itemDisplayname,
            };

            const groupItemSubset = ifcManager.createSubset({
              modelID: modelId,
              scene,
              ids: groupItemIds,
              removePrevious: false,
              material,
              customID: `${modelId}-${itemId}-highlight`,
            });
            groupItemSubset.visible = false;

            return groupItems;
          }
        );

        const items =
          groupItemPromises.length !== 0 ? await Promise.all(groupItemPromises) : [];

        storeyIds.push(...groupIds);

        const storeyGroup: IFCStoreyGroupInterface = {
          id: groupId,
          ids: groupIds,
          displayname: groupDisplayname,
          items,
        };

        const groupSubset = ifcManager.createSubset({
          modelID: modelId,
          scene,
          ids: groupIds,
          removePrevious: false,
          material,
          customID: `${modelId}-${groupId}-highlight`,
        });
        groupSubset.visible = false;

        return storeyGroup;
      }
    );

    const groups = await Promise.all(groupPromises);

    const storey: IFCStoreyInterface = {
      id: storeyId,
      ids: storeyIds,
      displayname,
      groups,
    };

    const storeySubset = ifcManager.createSubset({
      modelID: modelId,
      scene,
      ids: storeyIds,
      removePrevious: false,
      material,
      customID: `${modelId}-${storeyId}-highlight`,
    });
    storeySubset.visible = false;

    return storey;
  }
);

const storeys = await Promise.all(storeyPromises);

const modelItem: IFCModelItemInterface = {
  modelId,
  ids,
  storeys: [],
  hidden: [],
  isolated: [],
  highlighted: undefined,
};
commit("setIfcModels", modelItem);
scene.add(model);
model.removeFromParent();
[...]

Highlight toggle function

/**
 * Highligt IFC geometry including children.
 *
 * @param {{ commit: Commit; getters: any; }} Actions { commit, getters }
 * @param {number} id IFC node id.
 * @returns {void}
 */
async ifcToggleHighlight(
  { commit, getters }: { commit: Commit; getters: any },
  id: number
): Promise<void> {
  const ifcManager: IFCManager = getters["getIfcManager"];
  const { modelId }: IFCModelItemInterface = getters["getIfcCurrentModel"];
  const highlighted: number = getters["getIfcHighlight"];
  const isHighlight: boolean = highlighted === id;
  const subset = ifcManager.getSubset(modelId, material, `${modelId}-${id}-highlight`);

  if (isHighlight) {
    commit("removeIfcHighlight", modelId);
    subset.visible = false;
  } else {
    commit("addIfcHighlight", id);
    subset.visible = true;
  }

},

recursive and hasChildren functions

/**
 * Recursive child search.
 *
 * @param {IFCNode} item IFC object with at least one (1) children array object.
 * @returns {Promise<number[]>} Array of express IDs.
 */
async ifcRecursiveChildren({ dispatch }, item: IFCNode): Promise<number[]> {
  const ids: Promise<number>[] = item.children.map(async (child: IFCNode) => {
    const { children: childChildren, expressID: childExpressID } = child;

    const doRecursive: boolean = await dispatch("ifcNodeHasChildren", childChildren);
    if (doRecursive) {
      return await dispatch("ifcRecursiveChildren", child);
    }
    return childExpressID;
  });

  const resolvedIds = await Promise.all(ids);
  return resolvedIds.flat();
},
/**
 * Check if children has items.
 *
 * @param {IFCNode[]} children Child items of current item.
 * @returns {boolean} Truthy falsy
 */
ifcNodeHasChildren(_, children: IFCNode[]): boolean {
  return children.length !== 0;
},
moh-fasial-ansari commented 2 years ago

can you please tell me that how to create tree structure in ifc when we click on asset then show all properties of asset same as forge propperties . Storey 1 <- Get all IDs including nodes that don't contain geometry Basic Wall Basic Wall Basic Wall Basic Wall Storey 2 Stairs Stairs Stairs