Issue with `loadSubtreeFromModel` and Node Visibility in Dynamically Loaded Linked Models

Issue with loadSubtreeFromModel and Node Visibility in Dynamically Loaded Linked Models

We’re dynamically loading linked models using the following logic:

const subRootNodeId = gfxResObject.index === 0 && !this.isExternal
  ? [0]
  : await this.model.loadSubtreeFromModel(this.viewer.linkedModelsNodeId, gfxResObject.fileName);

Sometimes this results in the following error:

IafGraphicsResourceManager.loadGraphicsResourceByIndex failed for /fileName 03-230071-0000100916-WOR-CON-MDL-000001_4.scz
/rootNodeId -2 /subRootNodeId undefined

Error: Bad model: Could not find master model key.

:white_check_mark: My Questions:

  1. Under what circumstances does loadSubtreeFromModel return undefined and throw the Bad model: Could not find master model key error?

  2. After calling loadSubtreeFromModel, is there a recommended approach to ensure the loaded nodeIds are fully functional — especially for visibility control?

  3. Why might setNodesVisibility(viewer, nodeIds, true/false) fail to update visibility for some dynamically loaded nodes Sometimes frequently?

getViewNodeIdsOfActiveCategory = async () => {
      let activeNodeIds = new Set();
      const modelComposition = this.iafViewer.state?.modelComposition?._properties || {};
      const categories = modelComposition.categories || [];

      const activeCategories = categories.filter(cat => cat.isSelected && !cat.isDeleted);

      for (const category of activeCategories) {
        const linkedNodeIds = await this.getViewNodeIdsByCategory(category.id);
        if (linkedNodeIds?.length) {
          activeNodeIds = IafSet.Union(activeNodeIds, linkedNodeIds);
        }
      }

      return [...activeNodeIds];
    };
    
    getViewNodeIdsByCategory = async (categoryId) => {
      let linkedNodeIds = [];
      const modelVersionId = this.dbModelVersionId;
      const categoryLinkedViewIds = this.searchViewIdsByCategory(categoryId);

      for (const viewId of categoryLinkedViewIds) {
        const key = this.getViewNodeCacheKey(viewId, modelVersionId);
        let cacheEntry = this.viewToNodeIdMap.get(key);

        const gfxResObject = this.csdlMapByViewId.get(viewId);
        if (!gfxResObject?.loaded) {
          await this.loadGraphicsResourceByViewId(viewId);
          cacheEntry = this.viewToNodeIdMap.get(key);
          if (!cacheEntry) continue;
        }

        if (!Array.isArray(cacheEntry.linkedNodeIds) || !cacheEntry.linkedNodeIds.length) {
          this.iafViewer.allChildren = [];
          this.iafViewer.getAllChildren(cacheEntry.viewFetchPointNodeId, this.iafViewer._viewer);

          cacheEntry.linkedNodeIds = this.iafViewer.allChildren;
          this.viewToNodeIdMap.set(key, cacheEntry);

          // if (this.iafViewer.iafDatabaseManager._enablePersistence) {
          //   await this.saveGraphicsCache();
          // } else {
            IafStorageUtils.saveGraphicsCache(this.viewer);
          // }
        }

        linkedNodeIds = IafSet.Union(linkedNodeIds, cacheEntry.linkedNodeIds);
      }

      this.categoryToNodeIdsMap.set(categoryId, linkedNodeIds);
      return linkedNodeIds;
    };
    
    toggleVisibilityByCategory = async (layerName, isVisible) => {
      const oldNodeIds = this.categoryToNodeIdsMap.get(layerName) || [];
      const newNodeIds = await this.getViewNodeIdsByCategory(layerName);

      const allActiveNodeIds = await this.getViewNodeIdsOfActiveCategory();
      if (isVisible) {
        const added = IafSet.Difference(newNodeIds, oldNodeIds);
        const removed = IafSet.Difference(oldNodeIds, newNodeIds);

        if (added.length > 0) {
          await this.iafViewer.setNodesVisibility(this.viewer, added, true);
        }

        if (removed.length > 0) {
          const trulyRemoved = removed.filter(nodeId => !allActiveNodeIds.includes(nodeId));
          await this.iafViewer.setNodesVisibility(this.viewer, trulyRemoved, false);
        }
        
        if (added.length === 0 && removed.length === 0 && newNodeIds.length > 0) {
          await this.iafViewer.setNodesVisibility(this.viewer, newNodeIds, true);
        }
      } else {
        let nodeIdsToHide = IafSet.Difference(newNodeIds, allActiveNodeIds);
        
        if (newNodeIds.length === 0 && (allActiveNodeIds.length === 0 || oldNodeIds.length > 0)) {
          nodeIdsToHide = oldNodeIds;
        }
        if (nodeIdsToHide.length > 0) {
          await this.iafViewer.setNodesVisibility(this.viewer, nodeIdsToHide, false);
          this.viewer.graphicsResources?.categoryToNodeIdsMap?.delete(layerName);
        }
      }
      
      await this.overrideVisibilityForSelection(isVisible);
      await this.iafViewer.setHiddenElements();//RRP PLAT-3855 - On demand loading of linked models - Hidden Elements
      await this.updateBoundingBox('toggleVisibilityByCategory');
    }

Looking forward to your insights — thank you!

For all three of your questions, the issue could be due to the timing of calling the functions. More specifically, certain components of Communicator are not yet ready for functions to be called — which can be addressed by using callbacks to avoid the errors.

Given the functions loadSubtreeFromModel and setNodesVisibility are both from the Model object, our recommendation is to call these functions inside the modelStructureReady callback.