From 4703ecc0b1127b89239bada00af423a5ae6fe806 Mon Sep 17 00:00:00 2001 From: Grigas <35135765+grigasp@users.noreply.github.com> Date: Tue, 24 May 2022 23:48:59 +0300 Subject: [PATCH 1/2] UI: Fix performance of getting subject models (#3666) (cherry picked from commit 69241852c88cd2c160ccf66dfb27e030e0c63b46) # Conflicts: # ui/appui-react/src/appui-react/imodel-components/models-tree/ModelsVisibilityHandler.ts # ui/appui-react/src/test/imodel-components/models-tree/ModelsVisibilityHandler.test.ts --- ...t-models-performance_2022-05-24-08-55.json | 10 + .../models-tree/ModelsVisibilityHandler.ts | 596 ++++++ .../ModelsVisibilityHandler.test.ts | 1647 +++++++++++++++++ 3 files changed, 2253 insertions(+) create mode 100644 common/changes/@itwin/appui-react/ui-models-tree-fix-determining-subject-models-performance_2022-05-24-08-55.json create mode 100644 ui/appui-react/src/appui-react/imodel-components/models-tree/ModelsVisibilityHandler.ts create mode 100644 ui/appui-react/src/test/imodel-components/models-tree/ModelsVisibilityHandler.test.ts diff --git a/common/changes/@itwin/appui-react/ui-models-tree-fix-determining-subject-models-performance_2022-05-24-08-55.json b/common/changes/@itwin/appui-react/ui-models-tree-fix-determining-subject-models-performance_2022-05-24-08-55.json new file mode 100644 index 000000000000..34129ce779b0 --- /dev/null +++ b/common/changes/@itwin/appui-react/ui-models-tree-fix-determining-subject-models-performance_2022-05-24-08-55.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/appui-react", + "comment": "Models Tree: Fix performance of determining Subject nodes' display state.", + "type": "none" + } + ], + "packageName": "@itwin/appui-react" +} diff --git a/ui/appui-react/src/appui-react/imodel-components/models-tree/ModelsVisibilityHandler.ts b/ui/appui-react/src/appui-react/imodel-components/models-tree/ModelsVisibilityHandler.ts new file mode 100644 index 000000000000..efe4fbc6fafc --- /dev/null +++ b/ui/appui-react/src/appui-react/imodel-components/models-tree/ModelsVisibilityHandler.ts @@ -0,0 +1,596 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ +/** @packageDocumentation + * @module IModelComponents + */ + +import { TreeNodeItem } from "@itwin/components-react"; +import { BeEvent, Id64String } from "@itwin/core-bentley"; +import { QueryBinder, QueryRowFormat } from "@itwin/core-common"; +import { IModelConnection, PerModelCategoryVisibility, Viewport } from "@itwin/core-frontend"; +import { ECClassGroupingNodeKey, GroupingNodeKey, Keys, KeySet, NodeKey } from "@itwin/presentation-common"; +import { IFilteredPresentationTreeDataProvider, IPresentationTreeDataProvider } from "@itwin/presentation-components"; +import { Presentation } from "@itwin/presentation-frontend"; +import { UiFramework } from "../../UiFramework"; +import { IVisibilityHandler, VisibilityChangeListener, VisibilityStatus } from "../VisibilityTreeEventHandler"; + +/** + * Visibility tree node types. + * @beta + */ +export enum ModelsTreeNodeType { + Unknown, + Subject, + Model, + Category, + Element, + Grouping, +} + +/** + * Type definition of predicate used to decide if node can be selected + * @beta + */ +export type ModelsTreeSelectionPredicate = (key: NodeKey, type: ModelsTreeNodeType) => boolean; + +/** + * Props for [[ModelsVisibilityHandler]] + * @alpha + */ +export interface ModelsVisibilityHandlerProps { + rulesetId: string; + viewport: Viewport; + hierarchyAutoUpdateEnabled?: boolean; +} + +/** + * Visibility handler used by [[ModelsTree]] to control visibility of the tree items. + * @alpha + */ +export class ModelsVisibilityHandler implements IVisibilityHandler { + private _props: ModelsVisibilityHandlerProps; + private _pendingVisibilityChange: any | undefined; + private _subjectModelIdsCache: SubjectModelIdsCache; + private _filteredDataProvider?: IFilteredPresentationTreeDataProvider; + private _elementIdsCache: ElementIdsCache; + private _listeners = new Array<() => void>(); + + constructor(props: ModelsVisibilityHandlerProps) { + this._props = props; + this._subjectModelIdsCache = new SubjectModelIdsCache(this._props.viewport.iModel); + this._elementIdsCache = new ElementIdsCache(this._props.viewport.iModel, this._props.rulesetId); + this._listeners.push(this._props.viewport.onViewedCategoriesPerModelChanged.addListener(this.onViewChanged)); + this._listeners.push(this._props.viewport.onViewedCategoriesChanged.addListener(this.onViewChanged)); + this._listeners.push(this._props.viewport.onViewedModelsChanged.addListener(this.onViewChanged)); + this._listeners.push(this._props.viewport.onAlwaysDrawnChanged.addListener(this.onElementAlwaysDrawnChanged)); + this._listeners.push(this._props.viewport.onNeverDrawnChanged.addListener(this.onElementNeverDrawnChanged)); + if (this._props.hierarchyAutoUpdateEnabled) { + this._listeners.push(Presentation.presentation.onIModelHierarchyChanged.addListener(/* istanbul ignore next */() => this._elementIdsCache.clear())); + } + } + + public dispose() { + this._listeners.forEach((remove) => remove()); + clearTimeout(this._pendingVisibilityChange); + } + + public onVisibilityChange = new BeEvent(); + + /** Sets data provider that is used to get filtered tree hierarchy. */ + public setFilteredDataProvider(provider: IFilteredPresentationTreeDataProvider | undefined) { this._filteredDataProvider = provider; } + + public static getNodeType(item: TreeNodeItem, dataProvider: IPresentationTreeDataProvider) { + if (NodeKey.isClassGroupingNodeKey(dataProvider.getNodeKey(item))) + return ModelsTreeNodeType.Grouping; + + if (!item.extendedData) + return ModelsTreeNodeType.Unknown; + + if (this.isSubjectNode(item)) + return ModelsTreeNodeType.Subject; + if (this.isModelNode(item)) + return ModelsTreeNodeType.Model; + if (this.isCategoryNode(item)) + return ModelsTreeNodeType.Category; + return ModelsTreeNodeType.Element; + } + + public static isSubjectNode(node: TreeNodeItem) { + return node.extendedData && node.extendedData.isSubject; + } + public static isModelNode(node: TreeNodeItem) { + return node.extendedData && node.extendedData.isModel; + } + public static isCategoryNode(node: TreeNodeItem) { + return node.extendedData && node.extendedData.isCategory; + } + + /** Returns visibility status of the tree node. */ + public getVisibilityStatus(node: TreeNodeItem, nodeKey: NodeKey): VisibilityStatus | Promise { + if (NodeKey.isClassGroupingNodeKey(nodeKey)) + return this.getElementGroupingNodeDisplayStatus(node.id, nodeKey); + + if (!NodeKey.isInstancesNodeKey(nodeKey)) + return { state: "hidden", isDisabled: true }; + + if (ModelsVisibilityHandler.isSubjectNode(node)) { + // note: subject nodes may be merged to represent multiple subject instances + return this.getSubjectNodeVisibility(nodeKey.instanceKeys.map((key) => key.id), node); + } + if (ModelsVisibilityHandler.isModelNode(node)) + return this.getModelDisplayStatus(nodeKey.instanceKeys[0].id); + if (ModelsVisibilityHandler.isCategoryNode(node)) + return this.getCategoryDisplayStatus(nodeKey.instanceKeys[0].id, this.getCategoryParentModelId(node)); + return this.getElementDisplayStatus(nodeKey.instanceKeys[0].id, this.getElementModelId(node), this.getElementCategoryId(node)); + } + + /** Changes visibility of the items represented by the tree node. */ + public async changeVisibility(node: TreeNodeItem, nodeKey: NodeKey, on: boolean) { + if (NodeKey.isClassGroupingNodeKey(nodeKey)) { + await this.changeElementGroupingNodeState(nodeKey, on); + return; + } + + if (!NodeKey.isInstancesNodeKey(nodeKey)) + return; + + if (ModelsVisibilityHandler.isSubjectNode(node)) { + await this.changeSubjectNodeState(nodeKey.instanceKeys.map((key) => key.id), node, on); + } else if (ModelsVisibilityHandler.isModelNode(node)) { + await this.changeModelState(nodeKey.instanceKeys[0].id, on); + } else if (ModelsVisibilityHandler.isCategoryNode(node)) { + this.changeCategoryState(nodeKey.instanceKeys[0].id, this.getCategoryParentModelId(node), on); + } else { + await this.changeElementState(nodeKey.instanceKeys[0].id, this.getElementModelId(node), this.getElementCategoryId(node), on); + } + } + + protected async getSubjectNodeVisibility(ids: Id64String[], node: TreeNodeItem): Promise { + if (!this._props.viewport.view.isSpatialView()) + return { isDisabled: true, state: "hidden", tooltip: createTooltip("disabled", "subject.nonSpatialView") }; + + if (this._filteredDataProvider) + return this.getFilteredSubjectDisplayStatus(this._filteredDataProvider, ids, node); + + return this.getSubjectDisplayStatus(ids); + } + + private async getSubjectDisplayStatus(ids: Id64String[]): Promise { + const modelIds = await this.getSubjectModelIds(ids); + const isDisplayed = modelIds.some((modelId) => this.getModelDisplayStatus(modelId).state === "visible"); + if (isDisplayed) + return { state: "visible", tooltip: createTooltip("visible", "subject.atLeastOneModelVisible") }; + return { state: "hidden", tooltip: createTooltip("hidden", "subject.allModelsHidden") }; + } + + private async getFilteredSubjectDisplayStatus(provider: IFilteredPresentationTreeDataProvider, ids: Id64String[], node: TreeNodeItem): Promise { + if (provider.nodeMatchesFilter(node)) + return this.getSubjectDisplayStatus(ids); + + const children = await provider.getNodes(node); + const childrenDisplayStatuses = await Promise.all(children.map((childNode) => this.getVisibilityStatus(childNode, provider.getNodeKey(childNode)))); + if (childrenDisplayStatuses.some((status) => status.state === "visible")) + return { state: "visible", tooltip: createTooltip("visible", "subject.atLeastOneModelVisible") }; + return { state: "hidden", tooltip: createTooltip("hidden", "subject.allModelsHidden") }; + } + + protected getModelDisplayStatus(id: Id64String): VisibilityStatus { + if (!this._props.viewport.view.isSpatialView()) + return { isDisabled: true, state: "hidden", tooltip: createTooltip("disabled", "model.nonSpatialView") }; + const isDisplayed = this._props.viewport.view.viewsModel(id); + return { state: isDisplayed ? "visible" : "hidden", tooltip: createTooltip(isDisplayed ? "visible" : "hidden", undefined) }; + } + + protected getCategoryDisplayStatus(id: Id64String, parentModelId: Id64String | undefined): VisibilityStatus { + if (parentModelId) { + if (this.getModelDisplayStatus(parentModelId).state === "hidden") + return { state: "hidden", isDisabled: true, tooltip: createTooltip("disabled", "category.modelNotDisplayed") }; + + const override = this._props.viewport.perModelCategoryVisibility.getOverride(parentModelId, id); + switch (override) { + case PerModelCategoryVisibility.Override.Show: + return { state: "visible", tooltip: createTooltip("visible", "category.displayedThroughPerModelOverride") }; + case PerModelCategoryVisibility.Override.Hide: + return { state: "hidden", tooltip: createTooltip("hidden", "category.hiddenThroughPerModelOverride") }; + } + } + const isDisplayed = this._props.viewport.view.viewsCategory(id); + return { + state: isDisplayed ? "visible" : "hidden", + tooltip: isDisplayed + ? createTooltip("visible", "category.displayedThroughCategorySelector") + : createTooltip("hidden", "category.hiddenThroughCategorySelector"), + }; + } + + protected async getElementGroupingNodeDisplayStatus(_id: string, key: ECClassGroupingNodeKey): Promise { + const { modelId, categoryId, elementIds } = await this.getGroupedElementIds(key); + + if (!modelId || !this._props.viewport.view.viewsModel(modelId)) + return { isDisabled: true, state: "hidden", tooltip: createTooltip("disabled", "element.modelNotDisplayed") }; + + if (this._props.viewport.alwaysDrawn !== undefined && this._props.viewport.alwaysDrawn.size > 0) { + let atLeastOneElementForceDisplayed = false; + for await (const elementId of elementIds.getElementIds()) { + if (this._props.viewport.alwaysDrawn.has(elementId)) { + atLeastOneElementForceDisplayed = true; + break; + } + } + if (atLeastOneElementForceDisplayed) + return { state: "visible", tooltip: createTooltip("visible", "element.displayedThroughAlwaysDrawnList") }; + } + + if (this._props.viewport.alwaysDrawn !== undefined && this._props.viewport.alwaysDrawn.size !== 0 && this._props.viewport.isAlwaysDrawnExclusive) + return { state: "hidden", tooltip: createTooltip("hidden", "element.hiddenDueToOtherElementsExclusivelyAlwaysDrawn") }; + + if (this._props.viewport.neverDrawn !== undefined && this._props.viewport.neverDrawn.size > 0) { + let allElementsForceHidden = true; + for await (const elementId of elementIds.getElementIds()) { + if (!this._props.viewport.neverDrawn.has(elementId)) { + allElementsForceHidden = false; + break; + } + } + if (allElementsForceHidden) + return { state: "hidden", tooltip: createTooltip("visible", "element.hiddenThroughNeverDrawnList") }; + } + + if (categoryId && this.getCategoryDisplayStatus(categoryId, modelId).state === "visible") + return { state: "visible", tooltip: createTooltip("visible", undefined) }; + + return { state: "hidden", tooltip: createTooltip("hidden", "element.hiddenThroughCategory") }; + } + + protected getElementDisplayStatus(elementId: Id64String, modelId: Id64String | undefined, categoryId: Id64String | undefined): VisibilityStatus { + if (!modelId || !this._props.viewport.view.viewsModel(modelId)) + return { isDisabled: true, state: "hidden", tooltip: createTooltip("disabled", "element.modelNotDisplayed") }; + if (this._props.viewport.neverDrawn !== undefined && this._props.viewport.neverDrawn.has(elementId)) + return { state: "hidden", tooltip: createTooltip("hidden", "element.hiddenThroughNeverDrawnList") }; + if (this._props.viewport.alwaysDrawn !== undefined) { + if (this._props.viewport.alwaysDrawn.has(elementId)) + return { state: "visible", tooltip: createTooltip("visible", "element.displayedThroughAlwaysDrawnList") }; + if (this._props.viewport.alwaysDrawn.size !== 0 && this._props.viewport.isAlwaysDrawnExclusive) + return { state: "hidden", tooltip: createTooltip("hidden", "element.hiddenDueToOtherElementsExclusivelyAlwaysDrawn") }; + } + if (categoryId && this.getCategoryDisplayStatus(categoryId, modelId).state === "visible") + return { state: "visible", tooltip: createTooltip("visible", undefined) }; + return { state: "hidden", tooltip: createTooltip("hidden", "element.hiddenThroughCategory") }; + } + + protected async changeSubjectNodeState(ids: Id64String[], node: TreeNodeItem, on: boolean) { + if (!this._props.viewport.view.isSpatialView()) + return; + + if (this._filteredDataProvider) + return this.changeFilteredSubjectState(this._filteredDataProvider, ids, node, on); + + return this.changeSubjectState(ids, on); + } + + private async changeFilteredSubjectState(provider: IFilteredPresentationTreeDataProvider, ids: Id64String[], node: TreeNodeItem, on: boolean) { + if (provider.nodeMatchesFilter(node)) + return this.changeSubjectState(ids, on); + + const children = await provider.getNodes(node); + return Promise.all(children.map(async (childNode) => this.changeVisibility(childNode, provider.getNodeKey(childNode), on))); + } + + private async changeSubjectState(ids: Id64String[], on: boolean) { + const modelIds = await this.getSubjectModelIds(ids); + return this.changeModelsVisibility(modelIds, on); + } + + protected async changeModelState(id: Id64String, on: boolean) { + if (!this._props.viewport.view.isSpatialView()) + return; + + return this.changeModelsVisibility([id], on); + } + + protected async changeModelsVisibility(ids: Id64String[], visible: boolean) { + if (visible) + return this._props.viewport.addViewedModels(ids); + else + this._props.viewport.changeModelDisplay(ids, false); + } + + protected changeCategoryState(categoryId: Id64String, parentModelId: Id64String | undefined, on: boolean) { + if (parentModelId) { + const isDisplayedInSelector = this._props.viewport.view.viewsCategory(categoryId); + const ovr = (on === isDisplayedInSelector) ? PerModelCategoryVisibility.Override.None + : on ? PerModelCategoryVisibility.Override.Show : PerModelCategoryVisibility.Override.Hide; + this._props.viewport.perModelCategoryVisibility.setOverride(parentModelId, categoryId, ovr); + if (ovr === PerModelCategoryVisibility.Override.None && on) { + // we took off the override which means the category is displayed in selector, but + // doesn't mean all its subcategories are displayed - this call ensures that + this._props.viewport.changeCategoryDisplay([categoryId], true, true); + } + return; + } + this._props.viewport.changeCategoryDisplay([categoryId], on, on ? true : false); + } + + protected async changeElementGroupingNodeState(key: ECClassGroupingNodeKey, on: boolean) { + const { modelId, categoryId, elementIds } = await this.getGroupedElementIds(key); + await this.changeElementsState(modelId, categoryId, elementIds.getElementIds(), on); + } + + protected async changeElementState(id: Id64String, modelId: Id64String | undefined, categoryId: Id64String | undefined, on: boolean) { + const childIdsContainer = this.getAssemblyElementIds(id); + async function* elementIds() { + yield id; + for await (const childId of childIdsContainer.getElementIds()) + yield childId; + } + await this.changeElementsState(modelId, categoryId, elementIds(), on); + } + + protected async changeElementsState(modelId: Id64String | undefined, categoryId: Id64String | undefined, elementIds: AsyncGenerator, on: boolean) { + const isDisplayedByDefault = modelId && this.getModelDisplayStatus(modelId).state === "visible" + && categoryId && this.getCategoryDisplayStatus(categoryId, modelId).state === "visible"; + const isHiddenDueToExclusiveAlwaysDrawnElements = this._props.viewport.isAlwaysDrawnExclusive && this._props.viewport.alwaysDrawn && 0 !== this._props.viewport.alwaysDrawn.size; + const currNeverDrawn = new Set(this._props.viewport.neverDrawn ? this._props.viewport.neverDrawn : []); + const currAlwaysDrawn = new Set(this._props.viewport.alwaysDrawn ? + this._props.viewport.alwaysDrawn : /* istanbul ignore next */[], + ); + for await (const elementId of elementIds) { + if (on) { + currNeverDrawn.delete(elementId); + if (!isDisplayedByDefault || isHiddenDueToExclusiveAlwaysDrawnElements) + currAlwaysDrawn.add(elementId); + } else { + currAlwaysDrawn.delete(elementId); + if (isDisplayedByDefault && !isHiddenDueToExclusiveAlwaysDrawnElements) + currNeverDrawn.add(elementId); + } + } + this._props.viewport.setNeverDrawn(currNeverDrawn); + this._props.viewport.setAlwaysDrawn(currAlwaysDrawn, this._props.viewport.isAlwaysDrawnExclusive); + } + + private onVisibilityChangeInternal() { + if (this._pendingVisibilityChange) + return; + + this._pendingVisibilityChange = setTimeout(() => { + this.onVisibilityChange.raiseEvent(); + this._pendingVisibilityChange = undefined; + }, 0); + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + private onViewChanged = (_vp: Viewport) => { + this.onVisibilityChangeInternal(); + }; + + // eslint-disable-next-line @typescript-eslint/naming-convention + private onElementAlwaysDrawnChanged = () => { + this.onVisibilityChangeInternal(); + }; + + // eslint-disable-next-line @typescript-eslint/naming-convention + private onElementNeverDrawnChanged = () => { + this.onVisibilityChangeInternal(); + }; + + private getCategoryParentModelId(categoryNode: TreeNodeItem): Id64String | undefined { + return categoryNode.extendedData ? categoryNode.extendedData.modelId : /* istanbul ignore next */ undefined; + } + + private getElementModelId(elementNode: TreeNodeItem): Id64String | undefined { + return elementNode.extendedData ? elementNode.extendedData.modelId : /* istanbul ignore next */ undefined; + } + + private getElementCategoryId(elementNode: TreeNodeItem): Id64String | undefined { + return elementNode.extendedData ? elementNode.extendedData.categoryId : /* istanbul ignore next */ undefined; + } + + private async getSubjectModelIds(subjectIds: Id64String[]) { + return (await Promise.all(subjectIds.map(async (id) => this._subjectModelIdsCache.getSubjectModelIds(id)))) + .reduce((allModelIds: Id64String[], curr: Id64String[]) => [...allModelIds, ...curr], []); + } + + // istanbul ignore next + private getAssemblyElementIds(assemblyId: Id64String) { + return this._elementIdsCache.getAssemblyElementIds(assemblyId); + } + + // istanbul ignore next + private async getGroupedElementIds(groupingNodeKey: GroupingNodeKey) { + return this._elementIdsCache.getGroupedElementIds(groupingNodeKey); + } +} + +interface ModelInfo { + id: Id64String; + isHidden: boolean; +} + +class SubjectModelIdsCache { + private _imodel: IModelConnection; + private _subjectsHierarchy: Map | undefined; + private _subjectModels: Map | undefined; + private _init: Promise | undefined; + + constructor(imodel: IModelConnection) { + this._imodel = imodel; + } + + private async initSubjectModels() { + const querySubjects = (): AsyncIterableIterator<{ id: Id64String, parentId?: Id64String, targetPartitionId?: Id64String }> => { + const subjectsQuery = ` + SELECT ECInstanceId id, Parent.Id parentId, json_extract(JsonProperties, '$.Subject.Model.TargetPartition') targetPartitionId + FROM bis.Subject + `; + return this._imodel.query(subjectsQuery, undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames }); + }; + const queryModels = (): AsyncIterableIterator<{ id: Id64String, parentId: Id64String, content?: string }> => { + const modelsQuery = ` + SELECT p.ECInstanceId id, p.Parent.Id parentId, json_extract(p.JsonProperties, '$.PhysicalPartition.Model.Content') content + FROM bis.InformationPartitionElement p + INNER JOIN bis.GeometricModel3d m ON m.ModeledElement.Id = p.ECInstanceId + WHERE NOT m.IsPrivate + `; + return this._imodel.query(modelsQuery, undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames }); + }; + + function pushToMap(map: Map, key: TKey, value: TValue) { + let list = map.get(key); + if (!list) { + list = []; + map.set(key, list); + } + list.push(value); + } + + this._subjectsHierarchy = new Map(); + const targetPartitionSubjects = new Map(); + for await (const subject of querySubjects()) { + if (subject.parentId) + pushToMap(this._subjectsHierarchy, subject.parentId, subject.id); + if (subject.targetPartitionId) + pushToMap(targetPartitionSubjects, subject.targetPartitionId, subject.id); + } + + this._subjectModels = new Map(); + for await (const model of queryModels()) { + const subjectIds = targetPartitionSubjects.get(model.id) ?? []; + if (!subjectIds.includes(model.parentId)) + subjectIds.push(model.parentId); + + const v = { id: model.id, isHidden: (model.content !== undefined) }; + subjectIds.forEach((subjectId) => { + pushToMap(this._subjectModels!, subjectId, v); + }); + } + } + + private async initCache() { + if (!this._init) { + this._init = this.initSubjectModels().then(() => { }); + } + return this._init; + } + + private appendSubjectModelsRecursively(modelIds: Id64String[], subjectId: Id64String) { + const subjectModelIds = this._subjectModels!.get(subjectId); + if (subjectModelIds) + modelIds.push(...subjectModelIds.map((info) => info.id)); + + const childSubjectIds = this._subjectsHierarchy!.get(subjectId); + if (childSubjectIds) + childSubjectIds.forEach((cs) => this.appendSubjectModelsRecursively(modelIds, cs)); + } + + public async getSubjectModelIds(subjectId: Id64String): Promise { + await this.initCache(); + const modelIds = new Array(); + this.appendSubjectModelsRecursively(modelIds, subjectId); + return modelIds; + } +} + +interface GroupedElementIds { + modelId?: string; + categoryId?: string; + elementIds: CachingElementIdsContainer; +} + +// istanbul ignore next +class ElementIdsCache { + private _assemblyElementIdsCache = new Map(); + private _groupedElementIdsCache = new Map(); + + constructor(private _imodel: IModelConnection, private _rulesetId: string) { + } + + public clear() { + this._assemblyElementIdsCache.clear(); + this._groupedElementIdsCache.clear(); + } + + public getAssemblyElementIds(assemblyId: Id64String) { + const ids = this._assemblyElementIdsCache.get(assemblyId); + if (ids) + return ids; + + const container = createAssemblyElementIdsContainer(this._imodel, this._rulesetId, assemblyId); + this._assemblyElementIdsCache.set(assemblyId, container); + return container; + } + + public async getGroupedElementIds(groupingNodeKey: GroupingNodeKey): Promise { + const keyString = JSON.stringify(groupingNodeKey); + const ids = this._groupedElementIdsCache.get(keyString); + if (ids) + return ids; + const info = await createGroupedElementsInfo(this._imodel, this._rulesetId, groupingNodeKey); + this._groupedElementIdsCache.set(keyString, info); + return info; + } +} + +async function* createInstanceIdsGenerator(imodel: IModelConnection, rulesetId: string, displayType: string, inputKeys: Keys) { + const res = await Presentation.presentation.getContentInstanceKeys({ + imodel, + rulesetOrId: rulesetId, + displayType, + keys: new KeySet(inputKeys), + }); + for await (const key of res.items()) { + yield key.id; + } +} + +// istanbul ignore next +class CachingElementIdsContainer { + private _generator; + private _ids; + constructor(generator: AsyncGenerator) { + this._generator = generator; + this._ids = new Array(); + } + public async* getElementIds() { + for (const id of this._ids) { + yield id; + } + for await (const id of this._generator) { + this._ids.push(id); + yield id; + } + } +} + +function createAssemblyElementIdsContainer(imodel: IModelConnection, rulesetId: string, assemblyId: Id64String) { + return new CachingElementIdsContainer(createInstanceIdsGenerator(imodel, rulesetId, "AssemblyElementsRequest", [{ className: "BisCore:Element", id: assemblyId }])); +} + +async function createGroupedElementsInfo(imodel: IModelConnection, rulesetId: string, groupingNodeKey: GroupingNodeKey) { + const groupedElementIdsContainer = new CachingElementIdsContainer(createInstanceIdsGenerator(imodel, rulesetId, "AssemblyElementsRequest", [groupingNodeKey])); + const elementId = await groupedElementIdsContainer.getElementIds().next(); + if (elementId.done) + throw new Error("Invalid grouping node key"); + + let modelId, categoryId; + const query = `SELECT Model.Id AS modelId, Category.Id AS categoryId FROM bis.GeometricElement3d WHERE ECInstanceId = ? LIMIT 1`; + for await (const modelAndCategoryIds of imodel.query(query, QueryBinder.from([elementId.value]), { rowFormat: QueryRowFormat.UseJsPropertyNames })) { + modelId = modelAndCategoryIds.modelId; + categoryId = modelAndCategoryIds.categoryId; + break; + } + return { modelId, categoryId, elementIds: groupedElementIdsContainer }; +} + +const createTooltip = (status: "visible" | "hidden" | "disabled", tooltipStringId: string | undefined): string => { + const statusStringId = `UiFramework:modelTree.status.${status}`; + const statusString = UiFramework.localization.getLocalizedString(statusStringId); + if (!tooltipStringId) + return statusString; + + tooltipStringId = `UiFramework:modelTree.tooltips.${tooltipStringId}`; + const tooltipString = UiFramework.localization.getLocalizedString(tooltipStringId); + return `${statusString}: ${tooltipString}`; +}; diff --git a/ui/appui-react/src/test/imodel-components/models-tree/ModelsVisibilityHandler.test.ts b/ui/appui-react/src/test/imodel-components/models-tree/ModelsVisibilityHandler.test.ts new file mode 100644 index 000000000000..28497818ec0a --- /dev/null +++ b/ui/appui-react/src/test/imodel-components/models-tree/ModelsVisibilityHandler.test.ts @@ -0,0 +1,1647 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ + +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as moq from "typemoq"; +import { PropertyRecord } from "@itwin/appui-abstract"; +import { BeEvent, Id64String, using } from "@itwin/core-bentley"; +import { QueryRowFormat } from "@itwin/core-common"; +import { + IModelApp, IModelConnection, NoRenderApp, PerModelCategoryVisibility, SpatialViewState, Viewport, ViewState, ViewState3d, +} from "@itwin/core-frontend"; +import { isPromiseLike } from "@itwin/core-react"; +import { createRandomId } from "@itwin/presentation-common/lib/cjs/test"; +import { FilteredPresentationTreeDataProvider } from "@itwin/presentation-components"; +import { IModelHierarchyChangeEventArgs, Presentation, PresentationManager } from "@itwin/presentation-frontend"; +import { ModelsVisibilityHandler, ModelsVisibilityHandlerProps } from "../../../appui-react/imodel-components/models-tree/ModelsVisibilityHandler"; +import { TestUtils } from "../../TestUtils"; +import { createCategoryNode, createElementClassGroupingNode, createElementNode, createModelNode, createSubjectNode } from "../Common"; + +describe("ModelsVisibilityHandler", () => { + + before(async () => { + await TestUtils.initializeUiFramework(); + await NoRenderApp.startup(); + }); + + after(async () => { + TestUtils.terminateUiFramework(); + await IModelApp.shutdown(); + }); + + const imodelMock = moq.Mock.ofType(); + + beforeEach(() => { + imodelMock.reset(); + }); + + interface ViewportMockProps { + viewState?: ViewState; + perModelCategoryVisibility?: PerModelCategoryVisibility.Overrides; + onViewedCategoriesPerModelChanged?: BeEvent<(vp: Viewport) => void>; + onViewedCategoriesChanged?: BeEvent<(vp: Viewport) => void>; + onViewedModelsChanged?: BeEvent<(vp: Viewport) => void>; + onAlwaysDrawnChanged?: BeEvent<() => void>; + onNeverDrawnChanged?: BeEvent<() => void>; + } + + const mockViewport = (props?: ViewportMockProps) => { + if (!props) + props = {}; + if (!props.viewState) + props.viewState = moq.Mock.ofType().object; + if (!props.perModelCategoryVisibility) + props.perModelCategoryVisibility = moq.Mock.ofType().object; + if (!props.onViewedCategoriesPerModelChanged) + props.onViewedCategoriesPerModelChanged = new BeEvent<(vp: Viewport) => void>(); + if (!props.onViewedCategoriesChanged) + props.onViewedCategoriesChanged = new BeEvent<(vp: Viewport) => void>(); + if (!props.onViewedModelsChanged) + props.onViewedModelsChanged = new BeEvent<(vp: Viewport) => void>(); + if (!props.onAlwaysDrawnChanged) + props.onAlwaysDrawnChanged = new BeEvent<() => void>(); + if (!props.onNeverDrawnChanged) + props.onNeverDrawnChanged = new BeEvent<() => void>(); + const vpMock = moq.Mock.ofType(); + vpMock.setup((x) => x.iModel).returns(() => imodelMock.object); + vpMock.setup((x) => x.view).returns(() => props!.viewState!); + vpMock.setup((x) => x.perModelCategoryVisibility).returns(() => props!.perModelCategoryVisibility!); + vpMock.setup((x) => x.onViewedCategoriesPerModelChanged).returns(() => props!.onViewedCategoriesPerModelChanged!); + vpMock.setup((x) => x.onViewedCategoriesChanged).returns(() => props!.onViewedCategoriesChanged!); + vpMock.setup((x) => x.onViewedModelsChanged).returns(() => props!.onViewedModelsChanged!); + vpMock.setup((x) => x.onAlwaysDrawnChanged).returns(() => props!.onAlwaysDrawnChanged!); + vpMock.setup((x) => x.onNeverDrawnChanged).returns(() => props!.onNeverDrawnChanged!); + return vpMock; + }; + + const createHandler = (partialProps?: Partial): ModelsVisibilityHandler => { + if (!partialProps) + partialProps = {}; + const props: ModelsVisibilityHandlerProps = { + rulesetId: "test", + viewport: partialProps.viewport || mockViewport().object, + hierarchyAutoUpdateEnabled: partialProps.hierarchyAutoUpdateEnabled, + }; + return new ModelsVisibilityHandler(props); + }; + + interface SubjectModelIdsMockProps { + imodelMock: moq.IMock; + subjectsHierarchy: Map; + subjectModels: Map>; + } + + const mockSubjectModelIds = (props: SubjectModelIdsMockProps) => { + props.imodelMock.setup((x) => x.query(moq.It.is((q: string) => (-1 !== q.indexOf("FROM bis.Subject"))), undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames })) + .returns(async function* () { + const list = new Array<{ id: Id64String, parentId: Id64String }>(); + props.subjectsHierarchy.forEach((ids, parentId) => ids.forEach((id) => list.push({ id, parentId }))); + while (list.length) + yield list.shift(); + }); + props.imodelMock.setup((x) => x.query(moq.It.is((q: string) => (-1 !== q.indexOf("FROM bis.InformationPartitionElement"))), undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames })) + .returns(async function* () { + const list = new Array<{ id: Id64String, parentId: Id64String, content?: string }>(); + props.subjectModels.forEach((modelInfos, subjectId) => modelInfos.forEach((modelInfo) => list.push({ id: modelInfo.id, parentId: subjectId, content: modelInfo.content }))); + while (list.length) + yield list.shift(); + }); + }; + + describe("constructor", () => { + + it("should subscribe for viewport change events", () => { + const vpMock = mockViewport(); + createHandler({ viewport: vpMock.object }); + expect(vpMock.object.onViewedCategoriesPerModelChanged.numberOfListeners).to.eq(1); + expect(vpMock.object.onViewedCategoriesChanged.numberOfListeners).to.eq(1); + expect(vpMock.object.onViewedModelsChanged.numberOfListeners).to.eq(1); + expect(vpMock.object.onAlwaysDrawnChanged.numberOfListeners).to.eq(1); + expect(vpMock.object.onNeverDrawnChanged.numberOfListeners).to.eq(1); + }); + + it("should subscribe for 'onIModelHierarchyChanged' event if hierarchy auto update is enabled", () => { + const presentationManagerMock = moq.Mock.ofType(); + const changeEvent = new BeEvent<(args: IModelHierarchyChangeEventArgs) => void>(); + presentationManagerMock.setup((x) => x.onIModelHierarchyChanged).returns(() => changeEvent); + Presentation.setPresentationManager(presentationManagerMock.object); + createHandler({ viewport: mockViewport().object, hierarchyAutoUpdateEnabled: true }); + expect(changeEvent.numberOfListeners).to.eq(1); + }); + + }); + + describe("dispose", () => { + + it("should unsubscribe from viewport change events", () => { + const vpMock = mockViewport(); + using(createHandler({ viewport: vpMock.object }), (_) => { }); + expect(vpMock.object.onViewedCategoriesPerModelChanged.numberOfListeners).to.eq(0); + expect(vpMock.object.onViewedCategoriesChanged.numberOfListeners).to.eq(0); + expect(vpMock.object.onViewedModelsChanged.numberOfListeners).to.eq(0); + expect(vpMock.object.onAlwaysDrawnChanged.numberOfListeners).to.eq(0); + expect(vpMock.object.onNeverDrawnChanged.numberOfListeners).to.eq(0); + }); + + it("should unsubscribe from 'onIModelHierarchyChanged' event", () => { + const presentationManagerMock = moq.Mock.ofType(); + const changeEvent = new BeEvent<(args: IModelHierarchyChangeEventArgs) => void>(); + presentationManagerMock.setup((x) => x.onIModelHierarchyChanged).returns(() => changeEvent); + Presentation.setPresentationManager(presentationManagerMock.object); + using(createHandler({ viewport: mockViewport().object, hierarchyAutoUpdateEnabled: true }), (_) => { }); + expect(changeEvent.numberOfListeners).to.eq(0); + }); + + }); + + describe("getDisplayStatus", () => { + + it("returns disabled when node is not an instance node", async () => { + const node = { + __key: { + type: "custom", + version: 0, + pathFromRoot: [], + }, + id: "custom", + label: PropertyRecord.fromString("custom"), + }; + + const vpMock = mockViewport(); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.false; + expect(result).to.include({ state: "hidden", isDisabled: true }); + }); + }); + + describe("subject", () => { + + it("return disabled when active view is not spatial", async () => { + const node = createSubjectNode(); + const vpMock = mockViewport(); + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.true; + if (isPromiseLike(result)) + expect(await result).to.include({ state: "hidden", isDisabled: true }); + }); + }); + + it("return 'hidden' when all models are not displayed", async () => { + const subjectIds = ["0x1", "0x2"]; + const node = createSubjectNode(subjectIds); + mockSubjectModelIds({ + imodelMock, + subjectsHierarchy: new Map([["0x0", subjectIds]]), + subjectModels: new Map([ + [subjectIds[0], [{ id: "0x3" }, { id: "0x4" }]], + [subjectIds[1], [{ id: "0x5" }, { id: "0x6" }]], + ]), + }); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x3")).returns(() => false); + viewStateMock.setup((x) => x.viewsModel("0x4")).returns(() => false); + viewStateMock.setup((x) => x.viewsModel("0x5")).returns(() => false); + viewStateMock.setup((x) => x.viewsModel("0x6")).returns(() => false); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.true; + if (isPromiseLike(result)) + expect(await result).to.include({ state: "hidden" }); + }); + }); + + it("return 'visible' when at least one direct model is displayed", async () => { + const subjectIds = ["0x1", "0x2"]; + const node = createSubjectNode(subjectIds); + mockSubjectModelIds({ + imodelMock, + subjectsHierarchy: new Map([["0x0", subjectIds]]), + subjectModels: new Map([ + [subjectIds[0], [{ id: "0x3" }, { id: "0x4" }]], + [subjectIds[1], [{ id: "0x5" }, { id: "0x6" }]], + ]), + }); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x3")).returns(() => false); + viewStateMock.setup((x) => x.viewsModel("0x4")).returns(() => false); + viewStateMock.setup((x) => x.viewsModel("0x5")).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x6")).returns(() => false); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.true; + if (isPromiseLike(result)) + expect(await result).to.include({ state: "visible" }); + }); + }); + + it("return 'visible' when at least one nested model is displayed", async () => { + const subjectIds = ["0x1", "0x2"]; + const node = createSubjectNode(subjectIds); + mockSubjectModelIds({ + imodelMock, + subjectsHierarchy: new Map([ + [subjectIds[0], ["0x3"]], + [subjectIds[1], ["0x4"]], + ["0x3", ["0x5", "0x6"]], + ["0x7", ["0x8"]], + ]), + subjectModels: new Map([ + ["0x6", [{ id: "0x10" }, { id: "0x11" }]], + ]), + }); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x10")).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x11")).returns(() => false); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.true; + if (isPromiseLike(result)) + expect(await result).to.include({ state: "visible" }); + }); + }); + + it("initializes subject models cache only once", async () => { + const node = createSubjectNode(); + const key = node.__key.instanceKeys[0]; + + mockSubjectModelIds({ + imodelMock, + subjectsHierarchy: new Map([["0x0", [key.id]]]), + subjectModels: new Map([[key.id, [{ id: "0x1" }, { id: "0x2" }]]]), + }); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel(moq.It.isAny())).returns(() => false); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + await Promise.all([handler.getVisibilityStatus(node, node.__key), handler.getVisibilityStatus(node, node.__key)]); + // expect the `query` to be called only twice (once for subjects and once for models) + imodelMock.verify((x) => x.query(moq.It.isAnyString(), undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames }), moq.Times.exactly(2)); + }); + }); + + describe("filtered", () => { + + it("return 'visible' when subject node matches filter and at least one model is visible", async () => { + const node = createSubjectNode(); + const key = node.__key.instanceKeys[0]; + + const filteredProvider = moq.Mock.ofType(); + filteredProvider.setup((x) => x.nodeMatchesFilter(node)).returns(() => true); + + mockSubjectModelIds({ + imodelMock, + subjectsHierarchy: new Map([["0x0", [key.id]]]), + subjectModels: new Map([[key.id, [{ id: "0x10" }, { id: "0x20" }]]]), + }); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x10")).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x20")).returns(() => false); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + handler.setFilteredDataProvider(filteredProvider.object); + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.true; + if (isPromiseLike(result)) + expect(await result).to.include({ state: "visible" }); + filteredProvider.verifyAll(); + }); + }); + + it("return 'visible' when subject node with children matches filter and at least one model is visible", async () => { + const parentSubjectId = "0x1"; + const childSubjectId = "0x2"; + const node = createSubjectNode(parentSubjectId); + const childNode = createSubjectNode(childSubjectId); + + const filteredProvider = moq.Mock.ofType(); + filteredProvider.setup(async (x) => x.getNodes(node)).returns(async () => [childNode]).verifiable(moq.Times.never()); + filteredProvider.setup(async (x) => x.getNodes(childNode)).returns(async () => []).verifiable(moq.Times.never()); + filteredProvider.setup((x) => x.nodeMatchesFilter(moq.It.isAny())).returns(() => true); + + mockSubjectModelIds({ + imodelMock, + subjectsHierarchy: new Map([ + [parentSubjectId, [childSubjectId]], + ]), + subjectModels: new Map([ + [parentSubjectId, [{ id: "0x10" }, { id: "0x11" }]], + [childSubjectId, [{ id: "0x20" }]], + ]), + }); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x10")).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x11")).returns(() => false); + viewStateMock.setup((x) => x.viewsModel("0x20")).returns(() => false); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + handler.setFilteredDataProvider(filteredProvider.object); + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.true; + if (isPromiseLike(result)) + expect(await result).to.include({ state: "visible" }); + filteredProvider.verifyAll(); + }); + }); + + it("return 'visible' when subject node with children does not match filter and at least one child has visible models", async () => { + const parentSubjectId = "0x1"; + const childSubjectIds = ["0x2", "0x3"]; + const node = createSubjectNode(parentSubjectId); + const childNodes = [createSubjectNode(childSubjectIds[0]), createSubjectNode(childSubjectIds[1])]; + + const filteredProvider = moq.Mock.ofType(); + filteredProvider.setup(async (x) => x.getNodes(node)).returns(async () => childNodes).verifiable(moq.Times.once()); + filteredProvider.setup(async (x) => x.getNodes(childNodes[0])).returns(async () => []).verifiable(moq.Times.never()); + filteredProvider.setup(async (x) => x.getNodes(childNodes[1])).returns(async () => []).verifiable(moq.Times.never()); + filteredProvider.setup((x) => x.getNodeKey(childNodes[0])).returns(() => childNodes[0].__key).verifiable(moq.Times.once()); + filteredProvider.setup((x) => x.getNodeKey(childNodes[1])).returns(() => childNodes[1].__key).verifiable(moq.Times.once()); + filteredProvider.setup((x) => x.nodeMatchesFilter(node)).returns(() => false); + filteredProvider.setup((x) => x.nodeMatchesFilter(childNodes[0])).returns(() => true); + filteredProvider.setup((x) => x.nodeMatchesFilter(childNodes[1])).returns(() => true); + + mockSubjectModelIds({ + imodelMock, + subjectsHierarchy: new Map([ + [parentSubjectId, childSubjectIds], + ]), + subjectModels: new Map([ + [parentSubjectId, [{ id: "0x10" }]], + [childSubjectIds[0], [{ id: "0x20" }]], + [childSubjectIds[1], [{ id: "0x30" }]], + ]), + }); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x10")).returns(() => false); + viewStateMock.setup((x) => x.viewsModel("0x20")).returns(() => false); + viewStateMock.setup((x) => x.viewsModel("0x30")).returns(() => true); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + handler.setFilteredDataProvider(filteredProvider.object); + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.true; + if (isPromiseLike(result)) + expect(await result).to.include({ state: "visible" }); + filteredProvider.verifyAll(); + }); + }); + + it("return 'hidden' when subject node with children does not match filter and children models are not visible", async () => { + const parentSubjectIds = ["0x1", "0x2"]; + const childSubjectId = "0x3"; + const node = createSubjectNode(parentSubjectIds); + const childNode = createSubjectNode(childSubjectId); + + const filteredProvider = moq.Mock.ofType(); + filteredProvider.setup(async (x) => x.getNodes(node)).returns(async () => [childNode]).verifiable(moq.Times.once()); + filteredProvider.setup(async (x) => x.getNodes(childNode)).returns(async () => []).verifiable(moq.Times.never()); + filteredProvider.setup((x) => x.getNodeKey(childNode)).returns(() => childNode.__key).verifiable(moq.Times.once()); + filteredProvider.setup((x) => x.nodeMatchesFilter(node)).returns(() => false); + filteredProvider.setup((x) => x.nodeMatchesFilter(childNode)).returns(() => true); + + mockSubjectModelIds({ + imodelMock, + subjectsHierarchy: new Map([ + [parentSubjectIds[0], [childSubjectId]], + [parentSubjectIds[1], [childSubjectId]], + ]), + subjectModels: new Map([ + [parentSubjectIds[0], [{ id: "0x10" }]], + [parentSubjectIds[1], [{ id: "0x20" }]], + [childSubjectId, [{ id: "0x30" }]], + ]), + }); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x10")).returns(() => false); + viewStateMock.setup((x) => x.viewsModel("0x20")).returns(() => false); + viewStateMock.setup((x) => x.viewsModel("0x30")).returns(() => false); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + handler.setFilteredDataProvider(filteredProvider.object); + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.true; + if (isPromiseLike(result)) + expect(await result).to.include({ state: "hidden" }); + filteredProvider.verifyAll(); + }); + }); + + }); + + }); + + describe("model", () => { + + it("return disabled when active view is not spatial", async () => { + const node = createModelNode(); + const vpMock = mockViewport(); + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.false; + expect(result).to.include({ state: "hidden", isDisabled: true }); + }); + }); + + it("return 'visible' when displayed", async () => { + const node = createModelNode(); + const key = node.__key.instanceKeys[0]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel(key.id)).returns(() => true); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.false; + expect(result).to.include({ state: "visible" }); + }); + }); + + it("returns 'hidden' when not displayed", async () => { + const node = createModelNode(); + const key = node.__key.instanceKeys[0]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel(key.id)).returns(() => false); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.false; + expect(result).to.include({ state: "hidden" }); + }); + }); + + }); + + describe("category", () => { + + it("return disabled when model not displayed", async () => { + const parentModelNode = createModelNode(); + const parentModelKey = parentModelNode.__key.instanceKeys[0]; + const categoryNode = createCategoryNode(parentModelKey); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel(parentModelKey.id)).returns(() => false); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(categoryNode, categoryNode.__key); + expect(isPromiseLike(result)).to.be.false; + expect(result).to.include({ state: "hidden", isDisabled: true }); + }); + }); + + it("return 'visible' when model displayed, category not displayed but per-model override says it's displayed", async () => { + const parentModelNode = createModelNode(); + const parentModelKey = parentModelNode.__key.instanceKeys[0]; + const categoryNode = createCategoryNode(parentModelKey); + const categoryKey = categoryNode.__key.instanceKeys[0]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory(categoryKey.id)).returns(() => false); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel(parentModelKey.id)).returns(() => true); + + const perModelCategoryVisibilityMock = moq.Mock.ofType(); + perModelCategoryVisibilityMock.setup((x) => x.getOverride(parentModelKey.id, categoryKey.id)).returns(() => PerModelCategoryVisibility.Override.Show); + + const vpMock = mockViewport({ viewState: viewStateMock.object, perModelCategoryVisibility: perModelCategoryVisibilityMock.object }); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(categoryNode, categoryNode.__key); + expect(isPromiseLike(result)).to.be.false; + expect(result).to.include({ state: "visible" }); + }); + }); + + it("return 'visible' when model displayed, category displayed and there're no per-model overrides", async () => { + const parentModelNode = createModelNode(); + const parentModelKey = parentModelNode.__key.instanceKeys[0]; + const categoryNode = createCategoryNode(parentModelKey); + const key = categoryNode.__key.instanceKeys[0]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory(key.id)).returns(() => true); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel(parentModelKey.id)).returns(() => true); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(categoryNode, categoryNode.__key); + expect(isPromiseLike(result)).to.be.false; + expect(result).to.include({ state: "visible" }); + }); + }); + + it("return 'hidden' when model displayed, category displayed but per-model override says it's not displayed", async () => { + const parentModelNode = createModelNode(); + const parentModelKey = parentModelNode.__key.instanceKeys[0]; + const categoryNode = createCategoryNode(parentModelKey); + const categoryKey = categoryNode.__key.instanceKeys[0]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory(categoryKey.id)).returns(() => true); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel(parentModelKey.id)).returns(() => true); + + const perModelCategoryVisibilityMock = moq.Mock.ofType(); + perModelCategoryVisibilityMock.setup((x) => x.getOverride(parentModelKey.id, categoryKey.id)).returns(() => PerModelCategoryVisibility.Override.Hide); + + const vpMock = mockViewport({ viewState: viewStateMock.object, perModelCategoryVisibility: perModelCategoryVisibilityMock.object }); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(categoryNode, categoryNode.__key); + expect(isPromiseLike(result)).to.be.false; + expect(result).to.include({ state: "hidden" }); + }); + }); + + it("return 'hidden' when model displayed, category not displayed and there're no per-model overrides", async () => { + const parentModelNode = createModelNode(); + const parentModelKey = parentModelNode.__key.instanceKeys[0]; + const categoryNode = createCategoryNode(parentModelKey); + const key = categoryNode.__key.instanceKeys[0]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory(key.id)).returns(() => false); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel(parentModelKey.id)).returns(() => true); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(categoryNode, categoryNode.__key); + expect(isPromiseLike(result)).to.be.false; + expect(result).to.include({ state: "hidden" }); + }); + }); + + it("return 'hidden' when category has no parent model and category is not displayed", async () => { + const categoryNode = createCategoryNode(); + const categoryKey = categoryNode.__key.instanceKeys[0]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory(categoryKey.id)).returns(() => false); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(categoryNode, categoryNode.__key); + expect(isPromiseLike(result)).to.be.false; + expect(result).to.include({ state: "hidden" }); + }); + }); + + }); + + describe("element class grouping", () => { + + it("returns disabled when model not displayed", async () => { + const groupedElementIds = ["0x11", "0x12", "0x13"]; + const node = createElementClassGroupingNode(groupedElementIds); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => false); + const vpMock = mockViewport({ viewState: viewStateMock.object }); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + // note: need to override to avoid running queries on the imodel + (handler as any).getGroupedElementIds = async () => ({ + categoryId: "0x1", + modelId: "0x2", + elementIds: { + async* getElementIds() { + for (const id of groupedElementIds) + yield id; + }, + }, + }); + + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.true; + expect(await result).to.include({ state: "hidden", isDisabled: true }); + }); + }); + + it("returns 'visible' when model displayed and at least one element is in always displayed list", async () => { + const groupedElementIds = ["0x11", "0x12", "0x13"]; + const node = createElementClassGroupingNode(groupedElementIds); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => false); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); + const vpMock = mockViewport({ viewState: viewStateMock.object }); + const alwaysDrawn = new Set([groupedElementIds[1]]); + vpMock.setup((x) => x.neverDrawn).returns(() => undefined); + vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDrawn); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + // note: need to override to avoid running queries on the imodel + (handler as any).getGroupedElementIds = async () => ({ + categoryId: "0x1", + modelId: "0x2", + elementIds: { + async* getElementIds() { + for (const id of groupedElementIds) + yield id; + }, + }, + }); + + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.true; + expect(await result).to.include({ state: "visible" }); + }); + }); + + it("returns 'hidden' when model displayed and there's at least one element in always exclusive displayed list that's not grouped under node", async () => { + const groupedElementIds = ["0x11", "0x12", "0x13"]; + const node = createElementClassGroupingNode(groupedElementIds); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => true); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); + const vpMock = mockViewport({ viewState: viewStateMock.object }); + const alwaysDrawn = new Set(["0x4"]); + vpMock.setup((x) => x.neverDrawn).returns(() => undefined); + vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDrawn); + vpMock.setup((x) => x.isAlwaysDrawnExclusive).returns(() => true); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + // note: need to override to avoid running queries on the imodel + (handler as any).getGroupedElementIds = async () => ({ + categoryId: "0x1", + modelId: "0x2", + elementIds: { + async* getElementIds() { + for (const id of groupedElementIds) + yield id; + }, + }, + }); + + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.true; + expect(await result).to.include({ state: "hidden" }); + }); + }); + + it("returns 'hidden' when model displayed and all elements are in never displayed list", async () => { + const groupedElementIds = ["0x11", "0x12", "0x13"]; + const node = createElementClassGroupingNode(groupedElementIds); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => true); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); + const vpMock = mockViewport({ viewState: viewStateMock.object }); + const neverDrawn = new Set(groupedElementIds); + vpMock.setup((x) => x.neverDrawn).returns(() => neverDrawn); + vpMock.setup((x) => x.alwaysDrawn).returns(() => new Set()); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + // note: need to override to avoid running queries on the imodel + (handler as any).getGroupedElementIds = async () => ({ + categoryId: "0x1", + modelId: "0x2", + elementIds: { + async* getElementIds() { + for (const id of groupedElementIds) + yield id; + }, + }, + }); + + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.true; + expect(await result).to.include({ state: "hidden" }); + }); + }); + + it("returns 'hidden' when model displayed and category not displayed", async () => { + const groupedElementIds = ["0x11", "0x12", "0x13"]; + const node = createElementClassGroupingNode(groupedElementIds); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => false); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); + const vpMock = mockViewport({ viewState: viewStateMock.object }); + const neverDrawn = new Set(["0x11"]); + vpMock.setup((x) => x.neverDrawn).returns(() => neverDrawn); + vpMock.setup((x) => x.alwaysDrawn).returns(() => new Set()); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + // note: need to override to avoid running queries on the imodel + (handler as any).getGroupedElementIds = async () => ({ + categoryId: "0x1", + modelId: "0x2", + elementIds: { + async* getElementIds() { + for (const id of groupedElementIds) + yield id; + }, + }, + }); + + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.true; + expect(await result).to.include({ state: "hidden" }); + }); + }); + + it("returns 'visible' when model displayed and category displayed", async () => { + const groupedElementIds = ["0x11", "0x12", "0x13"]; + const node = createElementClassGroupingNode(groupedElementIds); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => true); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); + const vpMock = mockViewport({ viewState: viewStateMock.object }); + const neverDrawn = new Set(["0x11"]); + vpMock.setup((x) => x.neverDrawn).returns(() => neverDrawn); + vpMock.setup((x) => x.alwaysDrawn).returns(() => new Set()); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + // note: need to override to avoid running queries on the imodel + (handler as any).getGroupedElementIds = async () => ({ + categoryId: "0x1", + modelId: "0x2", + elementIds: { + async* getElementIds() { + for (const id of groupedElementIds) + yield id; + }, + }, + }); + + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.true; + expect(await result).to.include({ state: "visible" }); + }); + }); + + }); + + describe("element", () => { + + it("returns disabled when modelId not set", async () => { + const node = createElementNode(undefined, "0x1"); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel(moq.It.isAny())).returns(() => true); + const vpMock = mockViewport({ viewState: viewStateMock.object }); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.false; + expect(result).to.include({ state: "hidden", isDisabled: true }); + }); + }); + + it("returns disabled when model not displayed", async () => { + const node = createElementNode("0x2", "0x1"); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => false); + const vpMock = mockViewport({ viewState: viewStateMock.object }); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.false; + expect(result).to.include({ state: "hidden", isDisabled: true }); + }); + }); + + it("returns 'hidden' when model displayed, category displayed, but element is in never displayed list", async () => { + const node = createElementNode("0x2", "0x1"); + const key = node.__key.instanceKeys[0]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => true); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); + const vpMock = mockViewport({ viewState: viewStateMock.object }); + const neverDrawn = new Set([key.id]); + vpMock.setup((x) => x.neverDrawn).returns(() => neverDrawn); + vpMock.setup((x) => x.alwaysDrawn).returns(() => undefined); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.false; + expect(result).to.include({ state: "hidden" }); + }); + }); + + it("returns 'visible' when model displayed and element is in always displayed list", async () => { + const node = createElementNode("0x2", "0x1"); + const key = node.__key.instanceKeys[0]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => false); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); + const vpMock = mockViewport({ viewState: viewStateMock.object }); + const alwaysDrawn = new Set([key.id]); + vpMock.setup((x) => x.neverDrawn).returns(() => undefined); + vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDrawn); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.false; + expect(result).to.include({ state: "visible" }); + }); + }); + + it("returns 'visible' when model displayed, category displayed and element is in neither 'never' nor 'always' displayed", async () => { + const node = createElementNode("0x2", "0x1"); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => true); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); + const vpMock = mockViewport({ viewState: viewStateMock.object }); + vpMock.setup((x) => x.alwaysDrawn).returns(() => undefined); + vpMock.setup((x) => x.neverDrawn).returns(() => undefined); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.false; + expect(result).to.include({ state: "visible" }); + }); + }); + + it("returns 'hidden' when model displayed, category not displayed and element is in neither 'never' nor 'always' displayed", async () => { + const node = createElementNode("0x2", "0x1"); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => false); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); + const vpMock = mockViewport({ viewState: viewStateMock.object }); + vpMock.setup((x) => x.alwaysDrawn).returns(() => undefined); + vpMock.setup((x) => x.neverDrawn).returns(() => undefined); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.false; + expect(result).to.include({ state: "hidden" }); + }); + }); + + it("returns 'hidden' when model displayed, category displayed and some other element is exclusively 'always' displayed", async () => { + const node = createElementNode("0x2", "0x1"); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => true); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); + const vpMock = mockViewport({ viewState: viewStateMock.object }); + vpMock.setup((x) => x.isAlwaysDrawnExclusive).returns(() => true); + vpMock.setup((x) => x.alwaysDrawn).returns(() => new Set([createRandomId()])); + vpMock.setup((x) => x.neverDrawn).returns(() => undefined); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.false; + expect(result).to.include({ state: "hidden" }); + }); + }); + + it("returns 'hidden' when model displayed, categoryId not set and element is in neither 'never' nor 'always' displayed", async () => { + const node = createElementNode("0x2", undefined); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory(moq.It.isAny())).returns(() => true); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); + const vpMock = mockViewport({ viewState: viewStateMock.object }); + vpMock.setup((x) => x.alwaysDrawn).returns(() => new Set()); + vpMock.setup((x) => x.neverDrawn).returns(() => new Set()); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const result = handler.getVisibilityStatus(node, node.__key); + expect(isPromiseLike(result)).to.be.false; + expect(result).to.include({ state: "hidden" }); + }); + }); + + }); + + }); + + describe("changeVisibility", () => { + + it("does nothing when node is not an instance node", async () => { + const node = { + __key: { + type: "custom", + version: 0, + pathFromRoot: [], + }, + id: "custom", + label: PropertyRecord.fromString("custom"), + }; + + const vpMock = mockViewport(); + vpMock.setup(async (x) => x.addViewedModels(moq.It.isAny())).verifiable(moq.Times.never()); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + await handler.changeVisibility(node, node.__key, true); + vpMock.verifyAll(); + }); + }); + + describe("subject", () => { + + it("does nothing for non-spatial views", async () => { + const node = createSubjectNode(); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => false); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + vpMock.setup(async (x) => x.addViewedModels(moq.It.isAny())).verifiable(moq.Times.never()); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + // note: need to override to avoid running a query on the imodel + (handler as any).getSubjectModelIds = async () => ["0x1", "0x2"]; + + await handler.changeVisibility(node, node.__key, true); + vpMock.verifyAll(); + }); + }); + + it("makes all subject models visible", async () => { + const node = createSubjectNode(); + const subjectModelIds = ["0x1", "0x2"]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + vpMock.setup(async (x) => x.addViewedModels(subjectModelIds)).verifiable(); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + // note: need to override to avoid running a query on the imodel + (handler as any).getSubjectModelIds = async () => subjectModelIds; + + await handler.changeVisibility(node, node.__key, true); + vpMock.verifyAll(); + }); + }); + + it("makes all subject models hidden", async () => { + const node = createSubjectNode(); + const subjectModelIds = ["0x1", "0x2"]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + vpMock.setup((x) => x.changeModelDisplay(subjectModelIds, false)).verifiable(); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + // note: need to override to avoid running a query on the imodel + (handler as any).getSubjectModelIds = async () => subjectModelIds; + + await handler.changeVisibility(node, node.__key, false); + vpMock.verifyAll(); + }); + }); + + describe("filtered", () => { + + ["visible", "hidden"].map((mode) => { + it(`makes all subject models ${mode} when subject node does not have children`, async () => { + const node = createSubjectNode(); + const key = node.__key.instanceKeys[0]; + const subjectModelIds = ["0x1", "0x2"]; + + const filteredDataProvider = moq.Mock.ofType(); + filteredDataProvider.setup(async (x) => x.getNodes(node)).returns(async () => []).verifiable(moq.Times.never()); + filteredDataProvider.setup((x) => x.nodeMatchesFilter(node)).returns(() => true); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + + mockSubjectModelIds({ + imodelMock, + subjectsHierarchy: new Map([]), + subjectModels: new Map([ + [key.id, [{ id: subjectModelIds[0], content: "reference" }, { id: subjectModelIds[1] }]], + ]), + }); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + if (mode === "visible") { + vpMock.setup(async (x) => x.addViewedModels(subjectModelIds)).verifiable(); + } else { + vpMock.setup((x) => x.changeModelDisplay(subjectModelIds, false)).verifiable(); + } + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + handler.setFilteredDataProvider(filteredDataProvider.object); + await handler.changeVisibility(node, node.__key, mode === "visible"); + vpMock.verifyAll(); + filteredDataProvider.verifyAll(); + }); + }); + + it(`makes only children ${mode} if parent node does not match filter`, async () => { + const node = createSubjectNode("0x1"); + const childNode = createSubjectNode("0x2"); + const parentSubjectModelIds = ["0x10", "0x11"]; + const childSubjectModelIds = ["0x20"]; + + const filteredDataProvider = moq.Mock.ofType(); + filteredDataProvider.setup(async (x) => x.getNodes(node)).returns(async () => [childNode]).verifiable(moq.Times.once()); + filteredDataProvider.setup(async (x) => x.getNodes(childNode)).returns(async () => []).verifiable(moq.Times.never()); + filteredDataProvider.setup((x) => x.getNodeKey(childNode)).returns(() => childNode.__key).verifiable(moq.Times.once()); + filteredDataProvider.setup((x) => x.nodeMatchesFilter(node)).returns(() => false); + filteredDataProvider.setup((x) => x.nodeMatchesFilter(childNode)).returns(() => true); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + + mockSubjectModelIds({ + imodelMock, + subjectsHierarchy: new Map([ + ["0x1", ["0x2"]], + ]), + subjectModels: new Map([ + ["0x1", [{ id: parentSubjectModelIds[0] }, { id: parentSubjectModelIds[1] }]], + ["0x2", [{ id: childSubjectModelIds[0] }]], + ]), + }); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + if (mode === "visible") { + vpMock.setup(async (x) => x.addViewedModels(childSubjectModelIds)).verifiable(); + } else { + vpMock.setup((x) => x.changeModelDisplay(childSubjectModelIds, false)).verifiable(); + } + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + handler.setFilteredDataProvider(filteredDataProvider.object); + await handler.changeVisibility(node, node.__key, mode === "visible"); + vpMock.verifyAll(); + filteredDataProvider.verifyAll(); + }); + + }); + + }); + + }); + + }); + + describe("model", () => { + + it("does nothing for non-spatial views", async () => { + const node = createModelNode(); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => false); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + vpMock.setup(async (x) => x.addViewedModels(moq.It.isAny())).verifiable(moq.Times.never()); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + await handler.changeVisibility(node, node.__key, true); + vpMock.verifyAll(); + }); + }); + + it("makes model visible", async () => { + const node = createModelNode(); + const key = node.__key.instanceKeys[0]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + vpMock.setup(async (x) => x.addViewedModels([key.id])).verifiable(); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + await handler.changeVisibility(node, node.__key, true); + vpMock.verifyAll(); + }); + }); + + it("makes model hidden", async () => { + const node = createModelNode(); + const key = node.__key.instanceKeys[0]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + vpMock.setup((x) => x.changeModelDisplay([key.id], false)).verifiable(); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + await handler.changeVisibility(node, node.__key, false); + vpMock.verifyAll(); + }); + }); + + }); + + describe("category", () => { + + it("makes category visible through per-model override when it's not visible through category selector", async () => { + const parentModelNode = createModelNode(); + const parentModelKey = parentModelNode.__key.instanceKeys[0]; + const categoryNode = createCategoryNode(parentModelKey); + const categoryKey = categoryNode.__key.instanceKeys[0]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory(categoryKey.id)).returns(() => false); + + const perModelCategoryVisibilityMock = moq.Mock.ofType(); + + const vpMock = mockViewport({ viewState: viewStateMock.object, perModelCategoryVisibility: perModelCategoryVisibilityMock.object }); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + await handler.changeVisibility(categoryNode, categoryNode.__key, true); + perModelCategoryVisibilityMock.verify((x) => x.setOverride(parentModelKey.id, categoryKey.id, PerModelCategoryVisibility.Override.Show), moq.Times.once()); + vpMock.verify((x) => x.changeCategoryDisplay(moq.It.isAny(), moq.It.isAny(), moq.It.isAny()), moq.Times.never()); + }); + }); + + it("makes category hidden through override when it's visible through category selector", async () => { + const parentModelNode = createModelNode(); + const parentModelKey = parentModelNode.__key.instanceKeys[0]; + const categoryNode = createCategoryNode(parentModelKey); + const categoryKey = categoryNode.__key.instanceKeys[0]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory(categoryKey.id)).returns(() => true); + + const perModelCategoryVisibilityMock = moq.Mock.ofType(); + + const vpMock = mockViewport({ viewState: viewStateMock.object, perModelCategoryVisibility: perModelCategoryVisibilityMock.object }); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + await handler.changeVisibility(categoryNode, categoryNode.__key, false); + perModelCategoryVisibilityMock.verify((x) => x.setOverride(parentModelKey.id, categoryKey.id, PerModelCategoryVisibility.Override.Hide), moq.Times.once()); + vpMock.verify((x) => x.changeCategoryDisplay(moq.It.isAny(), moq.It.isAny(), moq.It.isAny()), moq.Times.never()); + }); + }); + + it("removes category override and enables all sub-categories when making visible and it's visible through category selector", async () => { + const parentModelNode = createModelNode(); + const parentModelKey = parentModelNode.__key.instanceKeys[0]; + const categoryNode = createCategoryNode(parentModelKey); + const categoryKey = categoryNode.__key.instanceKeys[0]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory(categoryKey.id)).returns(() => true); + + const perModelCategoryVisibilityMock = moq.Mock.ofType(); + + const vpMock = mockViewport({ viewState: viewStateMock.object, perModelCategoryVisibility: perModelCategoryVisibilityMock.object }); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + await handler.changeVisibility(categoryNode, categoryNode.__key, true); + perModelCategoryVisibilityMock.verify((x) => x.setOverride(parentModelKey.id, categoryKey.id, PerModelCategoryVisibility.Override.None), moq.Times.once()); + vpMock.verify((x) => x.changeCategoryDisplay([categoryKey.id], true, true), moq.Times.once()); + }); + }); + + it("removes category override when making hidden and it's hidden through category selector", async () => { + const parentModelNode = createModelNode(); + const parentModelKey = parentModelNode.__key.instanceKeys[0]; + const categoryNode = createCategoryNode(parentModelKey); + const categoryKey = categoryNode.__key.instanceKeys[0]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory(categoryKey.id)).returns(() => false); + + const perModelCategoryVisibilityMock = moq.Mock.ofType(); + + const vpMock = mockViewport({ viewState: viewStateMock.object, perModelCategoryVisibility: perModelCategoryVisibilityMock.object }); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + await handler.changeVisibility(categoryNode, categoryNode.__key, false); + perModelCategoryVisibilityMock.verify((x) => x.setOverride(parentModelKey.id, categoryKey.id, PerModelCategoryVisibility.Override.None), moq.Times.once()); + vpMock.verify((x) => x.changeCategoryDisplay(moq.It.isAny(), moq.It.isAny(), moq.It.isAny()), moq.Times.never()); + }); + }); + + it("makes category visible in selector and enables all sub-categories when category has no parent model", async () => { + const categoryNode = createCategoryNode(); + const categoryKey = categoryNode.__key.instanceKeys[0]; + + const vpMock = mockViewport(); + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + await handler.changeVisibility(categoryNode, categoryNode.__key, true); + vpMock.verify((x) => x.changeCategoryDisplay([categoryKey.id], true, true), moq.Times.once()); + }); + }); + + it("makes category hidden in selector when category has no parent model", async () => { + const categoryNode = createCategoryNode(); + const categoryKey = categoryNode.__key.instanceKeys[0]; + + const vpMock = mockViewport(); + vpMock.setup((x) => x.changeCategoryDisplay([categoryKey.id], false)).verifiable(); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + await handler.changeVisibility(categoryNode, categoryNode.__key, false); + vpMock.verify((x) => x.changeCategoryDisplay([categoryKey.id], false, false), moq.Times.once()); + }); + }); + + }); + + describe("element class grouping", () => { + + it("makes elements visible by removing from never displayed list and adding to always displayed list when category is not displayed", async () => { + const groupedElementIds = ["0x11", "0x12", "0x13"]; + const node = createElementClassGroupingNode(groupedElementIds); + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => false); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => false); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + + const alwaysDisplayed = new Set(); + const neverDisplayed = new Set([groupedElementIds[0]]); + vpMock.setup((x) => x.isAlwaysDrawnExclusive).returns(() => false); + vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDisplayed); + vpMock.setup((x) => x.neverDrawn).returns(() => neverDisplayed); + vpMock.setup((x) => x.setAlwaysDrawn(moq.It.is((set) => { + return set.size === 3 + && groupedElementIds.reduce((result, id) => (result && set.has(id)), true); + }), false)).verifiable(); + vpMock.setup((x) => x.setNeverDrawn(moq.It.is((set) => (set.size === 0)))).verifiable(); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + // note: need to override to avoid running queries on the imodel + (handler as any).getGroupedElementIds = async () => ({ + categoryId: "0x1", + modelId: "0x2", + elementIds: { + async* getElementIds() { + for (const id of groupedElementIds) + yield id; + }, + }, + }); + + await handler.changeVisibility(node, node.__key, true); + vpMock.verifyAll(); + }); + }); + + }); + + describe("element", () => { + + it("makes element visible by only removing from never displayed list when element's category is displayed", async () => { + const node = createElementNode("0x4", "0x3"); + const key = node.__key.instanceKeys[0]; + const assemblyChildrenIds = ["0x1", "0x2"]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory("0x3")).returns(() => true); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x4")).returns(() => true); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + + const alwaysDisplayed = new Set(); + const neverDisplayed = new Set([key.id]); + vpMock.setup((x) => x.isAlwaysDrawnExclusive).returns(() => false); + vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDisplayed); + vpMock.setup((x) => x.neverDrawn).returns(() => neverDisplayed); + vpMock.setup((x) => x.setNeverDrawn(moq.It.is((set) => (set.size === 0)))).verifiable(); + vpMock.setup((x) => x.setAlwaysDrawn(moq.It.is((set) => (set.size === 0)), false)).verifiable(); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + // note: need to override to avoid running queries on the imodel + (handler as any).getAssemblyElementIds = () => ({ + async* getElementIds() { + for (const id of assemblyChildrenIds) + yield id; + }, + }); + + await handler.changeVisibility(node, node.__key, true); + vpMock.verifyAll(); + }); + }); + + it("makes element visible by removing from never displayed list and adding to always displayed list when category is not displayed", async () => { + const node = createElementNode("0x4", "0x3"); + const key = node.__key.instanceKeys[0]; + const assemblyChildrenIds = ["0x1", "0x2"]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory("0x4")).returns(() => false); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x3")).returns(() => false); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + + const alwaysDisplayed = new Set(); + const neverDisplayed = new Set([key.id]); + vpMock.setup((x) => x.isAlwaysDrawnExclusive).returns(() => false); + vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDisplayed); + vpMock.setup((x) => x.neverDrawn).returns(() => neverDisplayed); + vpMock.setup((x) => x.setAlwaysDrawn(moq.It.is((set) => { + return set.size === 3 + && set.has(key.id) + && assemblyChildrenIds.reduce((result, id) => (result && set.has(id)), true); + }), false)).verifiable(); + vpMock.setup((x) => x.setNeverDrawn(moq.It.is((set) => (set.size === 0)))).verifiable(); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + // note: need to override to avoid running a query on the imodel + (handler as any).getAssemblyElementIds = () => ({ + async* getElementIds() { + for (const id of assemblyChildrenIds) + yield id; + }, + }); + + await handler.changeVisibility(node, node.__key, true); + vpMock.verifyAll(); + }); + }); + + it("makes element visible by adding to always displayed list when category is displayed, but element is hidden due to other elements exclusively always drawn", async () => { + const node = createElementNode("0x4", "0x3"); + const key = node.__key.instanceKeys[0]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory("0x4")).returns(() => true); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x3")).returns(() => true); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + + const alwaysDisplayed = new Set([createRandomId()]); + vpMock.setup((x) => x.isAlwaysDrawnExclusive).returns(() => true); + vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDisplayed); + vpMock.setup((x) => x.neverDrawn).returns(() => undefined); + vpMock.setup((x) => x.setAlwaysDrawn(moq.It.is((set) => { + return set.size === 2 && set.has(key.id); + }), true)).verifiable(); + vpMock.setup((x) => x.setNeverDrawn(moq.It.is((set) => (set.size === 0)))).verifiable(); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + // note: need to override to avoid running a query on the imodel + (handler as any).getAssemblyElementIds = () => ({ + async* getElementIds() { }, + }); + + await handler.changeVisibility(node, node.__key, true); + vpMock.verifyAll(); + }); + }); + + it("makes element hidden by only removing from always displayed list when element's category is not displayed", async () => { + const node = createElementNode("0x4", "0x3"); + const key = node.__key.instanceKeys[0]; + const assemblyChildrenIds = ["0x1", "0x2"]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory("0x3")).returns(() => false); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x4")).returns(() => true); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + + const alwaysDisplayed = new Set([key.id]); + const neverDisplayed = new Set(); + vpMock.setup((x) => x.isAlwaysDrawnExclusive).returns(() => false); + vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDisplayed); + vpMock.setup((x) => x.neverDrawn).returns(() => neverDisplayed); + vpMock.setup((x) => x.setNeverDrawn(moq.It.is((set) => (set.size === 0)))).verifiable(); + vpMock.setup((x) => x.setAlwaysDrawn(moq.It.is((set) => (set.size === 0)), false)).verifiable(); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + // note: need to override to avoid running queries on the imodel + (handler as any).getAssemblyElementIds = () => ({ + async* getElementIds() { + for (const id of assemblyChildrenIds) + yield id; + }, + }); + + await handler.changeVisibility(node, node.__key, false); + vpMock.verifyAll(); + }); + }); + + it("makes element hidden by removing from always displayed list and adding to never displayed list when category is displayed", async () => { + const node = createElementNode("0x4", "0x3"); + const key = node.__key.instanceKeys[0]; + const assemblyChildrenIds = ["0x1", "0x2"]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory("0x3")).returns(() => true); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x4")).returns(() => true); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + + const alwaysDisplayed = new Set([key.id]); + const neverDisplayed = new Set(); + vpMock.setup((x) => x.isAlwaysDrawnExclusive).returns(() => false); + vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDisplayed); + vpMock.setup((x) => x.neverDrawn).returns(() => neverDisplayed); + vpMock.setup((x) => x.setAlwaysDrawn(moq.It.is((set) => (set.size === 0)), false)).verifiable(); + vpMock.setup((x) => x.setNeverDrawn(moq.It.is((set) => { + return set.size === 3 + && set.has(key.id) + && assemblyChildrenIds.reduce((result, id) => (result && set.has(id)), true); + }))).verifiable(); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + // note: need to override to avoid running a query on the imodel + (handler as any).getAssemblyElementIds = () => ({ + async* getElementIds() { + for (const id of assemblyChildrenIds) + yield id; + }, + }); + + await handler.changeVisibility(node, node.__key, false); + vpMock.verifyAll(); + }); + }); + + it("makes element hidden by removing from always displayed list when category is displayed and there are exclusively always drawn elements", async () => { + const node = createElementNode("0x4", "0x3"); + const key = node.__key.instanceKeys[0]; + + const viewStateMock = moq.Mock.ofType(); + viewStateMock.setup((x) => x.viewsCategory("0x3")).returns(() => true); + viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); + viewStateMock.setup((x) => x.viewsModel("0x4")).returns(() => true); + + const vpMock = mockViewport({ viewState: viewStateMock.object }); + + const alwaysDisplayed = new Set([key.id, createRandomId()]); + vpMock.setup((x) => x.isAlwaysDrawnExclusive).returns(() => true); + vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDisplayed); + vpMock.setup((x) => x.neverDrawn).returns(() => undefined); + vpMock.setup((x) => x.setAlwaysDrawn(moq.It.is((set) => (set.size === 1 && !set.has(key.id))), true)).verifiable(); + vpMock.setup((x) => x.setNeverDrawn(moq.It.is((set) => (set.size === 0)))).verifiable(); + + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + // note: need to override to avoid running a query on the imodel + (handler as any).getAssemblyElementIds = () => ({ + async* getElementIds() { }, + }); + + await handler.changeVisibility(node, node.__key, false); + vpMock.verifyAll(); + }); + }); + + }); + + }); + + describe("visibility change event", () => { + + it("raises event on `onAlwaysDrawnChanged` event", async () => { + const evt = new BeEvent(); + const vpMock = mockViewport({ onAlwaysDrawnChanged: evt }); + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const spy = sinon.spy(); + handler.onVisibilityChange.addListener(spy); + evt.raiseEvent(vpMock.object); + await new Promise((resolve) => setTimeout(resolve)); + expect(spy).to.be.calledOnce; + }); + }); + + it("raises event on `onNeverDrawnChanged` event", async () => { + const evt = new BeEvent(); + const vpMock = mockViewport({ onNeverDrawnChanged: evt }); + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const spy = sinon.spy(); + handler.onVisibilityChange.addListener(spy); + evt.raiseEvent(vpMock.object); + await new Promise((resolve) => setTimeout(resolve)); + expect(spy).to.be.calledOnce; + }); + }); + + it("raises event on `onViewedCategoriesChanged` event", async () => { + const evt = new BeEvent(); + const vpMock = mockViewport({ onViewedCategoriesChanged: evt }); + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const spy = sinon.spy(); + handler.onVisibilityChange.addListener(spy); + evt.raiseEvent(vpMock.object); + await new Promise((resolve) => setTimeout(resolve)); + expect(spy).to.be.calledOnce; + }); + }); + + it("raises event on `onViewedModelsChanged` event", async () => { + const evt = new BeEvent(); + const vpMock = mockViewport({ onViewedModelsChanged: evt }); + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const spy = sinon.spy(); + handler.onVisibilityChange.addListener(spy); + evt.raiseEvent(vpMock.object); + await new Promise((resolve) => setTimeout(resolve)); + expect(spy).to.be.calledOnce; + }); + }); + + it("raises event on `onViewedCategoriesPerModelChanged` event", async () => { + const evt = new BeEvent(); + const vpMock = mockViewport({ onViewedCategoriesPerModelChanged: evt }); + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const spy = sinon.spy(); + handler.onVisibilityChange.addListener(spy); + evt.raiseEvent(vpMock.object); + await new Promise((resolve) => setTimeout(resolve)); + expect(spy).to.be.calledOnce; + }); + }); + + it("raises event once when multiple affecting events are fired", async () => { + const evts = { + onViewedCategoriesPerModelChanged: new BeEvent<(vp: Viewport) => void>(), + onViewedCategoriesChanged: new BeEvent<(vp: Viewport) => void>(), + onViewedModelsChanged: new BeEvent<(vp: Viewport) => void>(), + onAlwaysDrawnChanged: new BeEvent<() => void>(), + onNeverDrawnChanged: new BeEvent<() => void>(), + }; + const vpMock = mockViewport({ ...evts }); + await using(createHandler({ viewport: vpMock.object }), async (handler) => { + const spy = sinon.spy(); + handler.onVisibilityChange.addListener(spy); + evts.onViewedCategoriesPerModelChanged.raiseEvent(vpMock.object); + evts.onViewedCategoriesChanged.raiseEvent(vpMock.object); + evts.onViewedModelsChanged.raiseEvent(vpMock.object); + evts.onAlwaysDrawnChanged.raiseEvent(); + evts.onNeverDrawnChanged.raiseEvent(); + await new Promise((resolve) => setTimeout(resolve)); + expect(spy).to.be.calledOnce; + }); + }); + + }); + +}); From 5320b7970ce217a0b3a2a32746738982c69a7946 Mon Sep 17 00:00:00 2001 From: Grigas <35135765+grigasp@users.noreply.github.com> Date: Wed, 25 May 2022 08:38:57 +0300 Subject: [PATCH 2/2] Fix merge --- ...ease-2.19.x-pr-3666_2022-05-25-05-39.json} | 7 +- .../models-tree/ModelsVisibilityHandler.ts | 596 ------ .../ModelsVisibilityHandler.test.ts | 1647 ----------------- .../ModelsVisibilityHandler.test.ts | 4 +- .../models-tree/ModelsVisibilityHandler.ts | 71 +- 5 files changed, 51 insertions(+), 2274 deletions(-) rename common/changes/{@itwin/appui-react/ui-models-tree-fix-determining-subject-models-performance_2022-05-24-08-55.json => @bentley/ui-framework/mergify-bp-release-2.19.x-pr-3666_2022-05-25-05-39.json} (50%) delete mode 100644 ui/appui-react/src/appui-react/imodel-components/models-tree/ModelsVisibilityHandler.ts delete mode 100644 ui/appui-react/src/test/imodel-components/models-tree/ModelsVisibilityHandler.test.ts diff --git a/common/changes/@itwin/appui-react/ui-models-tree-fix-determining-subject-models-performance_2022-05-24-08-55.json b/common/changes/@bentley/ui-framework/mergify-bp-release-2.19.x-pr-3666_2022-05-25-05-39.json similarity index 50% rename from common/changes/@itwin/appui-react/ui-models-tree-fix-determining-subject-models-performance_2022-05-24-08-55.json rename to common/changes/@bentley/ui-framework/mergify-bp-release-2.19.x-pr-3666_2022-05-25-05-39.json index 34129ce779b0..c16cf3454554 100644 --- a/common/changes/@itwin/appui-react/ui-models-tree-fix-determining-subject-models-performance_2022-05-24-08-55.json +++ b/common/changes/@bentley/ui-framework/mergify-bp-release-2.19.x-pr-3666_2022-05-25-05-39.json @@ -1,10 +1,11 @@ { "changes": [ { - "packageName": "@itwin/appui-react", + "packageName": "@bentley/ui-framework", "comment": "Models Tree: Fix performance of determining Subject nodes' display state.", "type": "none" } ], - "packageName": "@itwin/appui-react" -} + "packageName": "@bentley/ui-framework", + "email": "35135765+grigasp@users.noreply.github.com" +} \ No newline at end of file diff --git a/ui/appui-react/src/appui-react/imodel-components/models-tree/ModelsVisibilityHandler.ts b/ui/appui-react/src/appui-react/imodel-components/models-tree/ModelsVisibilityHandler.ts deleted file mode 100644 index efe4fbc6fafc..000000000000 --- a/ui/appui-react/src/appui-react/imodel-components/models-tree/ModelsVisibilityHandler.ts +++ /dev/null @@ -1,596 +0,0 @@ -/*--------------------------------------------------------------------------------------------- -* Copyright (c) Bentley Systems, Incorporated. All rights reserved. -* See LICENSE.md in the project root for license terms and full copyright notice. -*--------------------------------------------------------------------------------------------*/ -/** @packageDocumentation - * @module IModelComponents - */ - -import { TreeNodeItem } from "@itwin/components-react"; -import { BeEvent, Id64String } from "@itwin/core-bentley"; -import { QueryBinder, QueryRowFormat } from "@itwin/core-common"; -import { IModelConnection, PerModelCategoryVisibility, Viewport } from "@itwin/core-frontend"; -import { ECClassGroupingNodeKey, GroupingNodeKey, Keys, KeySet, NodeKey } from "@itwin/presentation-common"; -import { IFilteredPresentationTreeDataProvider, IPresentationTreeDataProvider } from "@itwin/presentation-components"; -import { Presentation } from "@itwin/presentation-frontend"; -import { UiFramework } from "../../UiFramework"; -import { IVisibilityHandler, VisibilityChangeListener, VisibilityStatus } from "../VisibilityTreeEventHandler"; - -/** - * Visibility tree node types. - * @beta - */ -export enum ModelsTreeNodeType { - Unknown, - Subject, - Model, - Category, - Element, - Grouping, -} - -/** - * Type definition of predicate used to decide if node can be selected - * @beta - */ -export type ModelsTreeSelectionPredicate = (key: NodeKey, type: ModelsTreeNodeType) => boolean; - -/** - * Props for [[ModelsVisibilityHandler]] - * @alpha - */ -export interface ModelsVisibilityHandlerProps { - rulesetId: string; - viewport: Viewport; - hierarchyAutoUpdateEnabled?: boolean; -} - -/** - * Visibility handler used by [[ModelsTree]] to control visibility of the tree items. - * @alpha - */ -export class ModelsVisibilityHandler implements IVisibilityHandler { - private _props: ModelsVisibilityHandlerProps; - private _pendingVisibilityChange: any | undefined; - private _subjectModelIdsCache: SubjectModelIdsCache; - private _filteredDataProvider?: IFilteredPresentationTreeDataProvider; - private _elementIdsCache: ElementIdsCache; - private _listeners = new Array<() => void>(); - - constructor(props: ModelsVisibilityHandlerProps) { - this._props = props; - this._subjectModelIdsCache = new SubjectModelIdsCache(this._props.viewport.iModel); - this._elementIdsCache = new ElementIdsCache(this._props.viewport.iModel, this._props.rulesetId); - this._listeners.push(this._props.viewport.onViewedCategoriesPerModelChanged.addListener(this.onViewChanged)); - this._listeners.push(this._props.viewport.onViewedCategoriesChanged.addListener(this.onViewChanged)); - this._listeners.push(this._props.viewport.onViewedModelsChanged.addListener(this.onViewChanged)); - this._listeners.push(this._props.viewport.onAlwaysDrawnChanged.addListener(this.onElementAlwaysDrawnChanged)); - this._listeners.push(this._props.viewport.onNeverDrawnChanged.addListener(this.onElementNeverDrawnChanged)); - if (this._props.hierarchyAutoUpdateEnabled) { - this._listeners.push(Presentation.presentation.onIModelHierarchyChanged.addListener(/* istanbul ignore next */() => this._elementIdsCache.clear())); - } - } - - public dispose() { - this._listeners.forEach((remove) => remove()); - clearTimeout(this._pendingVisibilityChange); - } - - public onVisibilityChange = new BeEvent(); - - /** Sets data provider that is used to get filtered tree hierarchy. */ - public setFilteredDataProvider(provider: IFilteredPresentationTreeDataProvider | undefined) { this._filteredDataProvider = provider; } - - public static getNodeType(item: TreeNodeItem, dataProvider: IPresentationTreeDataProvider) { - if (NodeKey.isClassGroupingNodeKey(dataProvider.getNodeKey(item))) - return ModelsTreeNodeType.Grouping; - - if (!item.extendedData) - return ModelsTreeNodeType.Unknown; - - if (this.isSubjectNode(item)) - return ModelsTreeNodeType.Subject; - if (this.isModelNode(item)) - return ModelsTreeNodeType.Model; - if (this.isCategoryNode(item)) - return ModelsTreeNodeType.Category; - return ModelsTreeNodeType.Element; - } - - public static isSubjectNode(node: TreeNodeItem) { - return node.extendedData && node.extendedData.isSubject; - } - public static isModelNode(node: TreeNodeItem) { - return node.extendedData && node.extendedData.isModel; - } - public static isCategoryNode(node: TreeNodeItem) { - return node.extendedData && node.extendedData.isCategory; - } - - /** Returns visibility status of the tree node. */ - public getVisibilityStatus(node: TreeNodeItem, nodeKey: NodeKey): VisibilityStatus | Promise { - if (NodeKey.isClassGroupingNodeKey(nodeKey)) - return this.getElementGroupingNodeDisplayStatus(node.id, nodeKey); - - if (!NodeKey.isInstancesNodeKey(nodeKey)) - return { state: "hidden", isDisabled: true }; - - if (ModelsVisibilityHandler.isSubjectNode(node)) { - // note: subject nodes may be merged to represent multiple subject instances - return this.getSubjectNodeVisibility(nodeKey.instanceKeys.map((key) => key.id), node); - } - if (ModelsVisibilityHandler.isModelNode(node)) - return this.getModelDisplayStatus(nodeKey.instanceKeys[0].id); - if (ModelsVisibilityHandler.isCategoryNode(node)) - return this.getCategoryDisplayStatus(nodeKey.instanceKeys[0].id, this.getCategoryParentModelId(node)); - return this.getElementDisplayStatus(nodeKey.instanceKeys[0].id, this.getElementModelId(node), this.getElementCategoryId(node)); - } - - /** Changes visibility of the items represented by the tree node. */ - public async changeVisibility(node: TreeNodeItem, nodeKey: NodeKey, on: boolean) { - if (NodeKey.isClassGroupingNodeKey(nodeKey)) { - await this.changeElementGroupingNodeState(nodeKey, on); - return; - } - - if (!NodeKey.isInstancesNodeKey(nodeKey)) - return; - - if (ModelsVisibilityHandler.isSubjectNode(node)) { - await this.changeSubjectNodeState(nodeKey.instanceKeys.map((key) => key.id), node, on); - } else if (ModelsVisibilityHandler.isModelNode(node)) { - await this.changeModelState(nodeKey.instanceKeys[0].id, on); - } else if (ModelsVisibilityHandler.isCategoryNode(node)) { - this.changeCategoryState(nodeKey.instanceKeys[0].id, this.getCategoryParentModelId(node), on); - } else { - await this.changeElementState(nodeKey.instanceKeys[0].id, this.getElementModelId(node), this.getElementCategoryId(node), on); - } - } - - protected async getSubjectNodeVisibility(ids: Id64String[], node: TreeNodeItem): Promise { - if (!this._props.viewport.view.isSpatialView()) - return { isDisabled: true, state: "hidden", tooltip: createTooltip("disabled", "subject.nonSpatialView") }; - - if (this._filteredDataProvider) - return this.getFilteredSubjectDisplayStatus(this._filteredDataProvider, ids, node); - - return this.getSubjectDisplayStatus(ids); - } - - private async getSubjectDisplayStatus(ids: Id64String[]): Promise { - const modelIds = await this.getSubjectModelIds(ids); - const isDisplayed = modelIds.some((modelId) => this.getModelDisplayStatus(modelId).state === "visible"); - if (isDisplayed) - return { state: "visible", tooltip: createTooltip("visible", "subject.atLeastOneModelVisible") }; - return { state: "hidden", tooltip: createTooltip("hidden", "subject.allModelsHidden") }; - } - - private async getFilteredSubjectDisplayStatus(provider: IFilteredPresentationTreeDataProvider, ids: Id64String[], node: TreeNodeItem): Promise { - if (provider.nodeMatchesFilter(node)) - return this.getSubjectDisplayStatus(ids); - - const children = await provider.getNodes(node); - const childrenDisplayStatuses = await Promise.all(children.map((childNode) => this.getVisibilityStatus(childNode, provider.getNodeKey(childNode)))); - if (childrenDisplayStatuses.some((status) => status.state === "visible")) - return { state: "visible", tooltip: createTooltip("visible", "subject.atLeastOneModelVisible") }; - return { state: "hidden", tooltip: createTooltip("hidden", "subject.allModelsHidden") }; - } - - protected getModelDisplayStatus(id: Id64String): VisibilityStatus { - if (!this._props.viewport.view.isSpatialView()) - return { isDisabled: true, state: "hidden", tooltip: createTooltip("disabled", "model.nonSpatialView") }; - const isDisplayed = this._props.viewport.view.viewsModel(id); - return { state: isDisplayed ? "visible" : "hidden", tooltip: createTooltip(isDisplayed ? "visible" : "hidden", undefined) }; - } - - protected getCategoryDisplayStatus(id: Id64String, parentModelId: Id64String | undefined): VisibilityStatus { - if (parentModelId) { - if (this.getModelDisplayStatus(parentModelId).state === "hidden") - return { state: "hidden", isDisabled: true, tooltip: createTooltip("disabled", "category.modelNotDisplayed") }; - - const override = this._props.viewport.perModelCategoryVisibility.getOverride(parentModelId, id); - switch (override) { - case PerModelCategoryVisibility.Override.Show: - return { state: "visible", tooltip: createTooltip("visible", "category.displayedThroughPerModelOverride") }; - case PerModelCategoryVisibility.Override.Hide: - return { state: "hidden", tooltip: createTooltip("hidden", "category.hiddenThroughPerModelOverride") }; - } - } - const isDisplayed = this._props.viewport.view.viewsCategory(id); - return { - state: isDisplayed ? "visible" : "hidden", - tooltip: isDisplayed - ? createTooltip("visible", "category.displayedThroughCategorySelector") - : createTooltip("hidden", "category.hiddenThroughCategorySelector"), - }; - } - - protected async getElementGroupingNodeDisplayStatus(_id: string, key: ECClassGroupingNodeKey): Promise { - const { modelId, categoryId, elementIds } = await this.getGroupedElementIds(key); - - if (!modelId || !this._props.viewport.view.viewsModel(modelId)) - return { isDisabled: true, state: "hidden", tooltip: createTooltip("disabled", "element.modelNotDisplayed") }; - - if (this._props.viewport.alwaysDrawn !== undefined && this._props.viewport.alwaysDrawn.size > 0) { - let atLeastOneElementForceDisplayed = false; - for await (const elementId of elementIds.getElementIds()) { - if (this._props.viewport.alwaysDrawn.has(elementId)) { - atLeastOneElementForceDisplayed = true; - break; - } - } - if (atLeastOneElementForceDisplayed) - return { state: "visible", tooltip: createTooltip("visible", "element.displayedThroughAlwaysDrawnList") }; - } - - if (this._props.viewport.alwaysDrawn !== undefined && this._props.viewport.alwaysDrawn.size !== 0 && this._props.viewport.isAlwaysDrawnExclusive) - return { state: "hidden", tooltip: createTooltip("hidden", "element.hiddenDueToOtherElementsExclusivelyAlwaysDrawn") }; - - if (this._props.viewport.neverDrawn !== undefined && this._props.viewport.neverDrawn.size > 0) { - let allElementsForceHidden = true; - for await (const elementId of elementIds.getElementIds()) { - if (!this._props.viewport.neverDrawn.has(elementId)) { - allElementsForceHidden = false; - break; - } - } - if (allElementsForceHidden) - return { state: "hidden", tooltip: createTooltip("visible", "element.hiddenThroughNeverDrawnList") }; - } - - if (categoryId && this.getCategoryDisplayStatus(categoryId, modelId).state === "visible") - return { state: "visible", tooltip: createTooltip("visible", undefined) }; - - return { state: "hidden", tooltip: createTooltip("hidden", "element.hiddenThroughCategory") }; - } - - protected getElementDisplayStatus(elementId: Id64String, modelId: Id64String | undefined, categoryId: Id64String | undefined): VisibilityStatus { - if (!modelId || !this._props.viewport.view.viewsModel(modelId)) - return { isDisabled: true, state: "hidden", tooltip: createTooltip("disabled", "element.modelNotDisplayed") }; - if (this._props.viewport.neverDrawn !== undefined && this._props.viewport.neverDrawn.has(elementId)) - return { state: "hidden", tooltip: createTooltip("hidden", "element.hiddenThroughNeverDrawnList") }; - if (this._props.viewport.alwaysDrawn !== undefined) { - if (this._props.viewport.alwaysDrawn.has(elementId)) - return { state: "visible", tooltip: createTooltip("visible", "element.displayedThroughAlwaysDrawnList") }; - if (this._props.viewport.alwaysDrawn.size !== 0 && this._props.viewport.isAlwaysDrawnExclusive) - return { state: "hidden", tooltip: createTooltip("hidden", "element.hiddenDueToOtherElementsExclusivelyAlwaysDrawn") }; - } - if (categoryId && this.getCategoryDisplayStatus(categoryId, modelId).state === "visible") - return { state: "visible", tooltip: createTooltip("visible", undefined) }; - return { state: "hidden", tooltip: createTooltip("hidden", "element.hiddenThroughCategory") }; - } - - protected async changeSubjectNodeState(ids: Id64String[], node: TreeNodeItem, on: boolean) { - if (!this._props.viewport.view.isSpatialView()) - return; - - if (this._filteredDataProvider) - return this.changeFilteredSubjectState(this._filteredDataProvider, ids, node, on); - - return this.changeSubjectState(ids, on); - } - - private async changeFilteredSubjectState(provider: IFilteredPresentationTreeDataProvider, ids: Id64String[], node: TreeNodeItem, on: boolean) { - if (provider.nodeMatchesFilter(node)) - return this.changeSubjectState(ids, on); - - const children = await provider.getNodes(node); - return Promise.all(children.map(async (childNode) => this.changeVisibility(childNode, provider.getNodeKey(childNode), on))); - } - - private async changeSubjectState(ids: Id64String[], on: boolean) { - const modelIds = await this.getSubjectModelIds(ids); - return this.changeModelsVisibility(modelIds, on); - } - - protected async changeModelState(id: Id64String, on: boolean) { - if (!this._props.viewport.view.isSpatialView()) - return; - - return this.changeModelsVisibility([id], on); - } - - protected async changeModelsVisibility(ids: Id64String[], visible: boolean) { - if (visible) - return this._props.viewport.addViewedModels(ids); - else - this._props.viewport.changeModelDisplay(ids, false); - } - - protected changeCategoryState(categoryId: Id64String, parentModelId: Id64String | undefined, on: boolean) { - if (parentModelId) { - const isDisplayedInSelector = this._props.viewport.view.viewsCategory(categoryId); - const ovr = (on === isDisplayedInSelector) ? PerModelCategoryVisibility.Override.None - : on ? PerModelCategoryVisibility.Override.Show : PerModelCategoryVisibility.Override.Hide; - this._props.viewport.perModelCategoryVisibility.setOverride(parentModelId, categoryId, ovr); - if (ovr === PerModelCategoryVisibility.Override.None && on) { - // we took off the override which means the category is displayed in selector, but - // doesn't mean all its subcategories are displayed - this call ensures that - this._props.viewport.changeCategoryDisplay([categoryId], true, true); - } - return; - } - this._props.viewport.changeCategoryDisplay([categoryId], on, on ? true : false); - } - - protected async changeElementGroupingNodeState(key: ECClassGroupingNodeKey, on: boolean) { - const { modelId, categoryId, elementIds } = await this.getGroupedElementIds(key); - await this.changeElementsState(modelId, categoryId, elementIds.getElementIds(), on); - } - - protected async changeElementState(id: Id64String, modelId: Id64String | undefined, categoryId: Id64String | undefined, on: boolean) { - const childIdsContainer = this.getAssemblyElementIds(id); - async function* elementIds() { - yield id; - for await (const childId of childIdsContainer.getElementIds()) - yield childId; - } - await this.changeElementsState(modelId, categoryId, elementIds(), on); - } - - protected async changeElementsState(modelId: Id64String | undefined, categoryId: Id64String | undefined, elementIds: AsyncGenerator, on: boolean) { - const isDisplayedByDefault = modelId && this.getModelDisplayStatus(modelId).state === "visible" - && categoryId && this.getCategoryDisplayStatus(categoryId, modelId).state === "visible"; - const isHiddenDueToExclusiveAlwaysDrawnElements = this._props.viewport.isAlwaysDrawnExclusive && this._props.viewport.alwaysDrawn && 0 !== this._props.viewport.alwaysDrawn.size; - const currNeverDrawn = new Set(this._props.viewport.neverDrawn ? this._props.viewport.neverDrawn : []); - const currAlwaysDrawn = new Set(this._props.viewport.alwaysDrawn ? - this._props.viewport.alwaysDrawn : /* istanbul ignore next */[], - ); - for await (const elementId of elementIds) { - if (on) { - currNeverDrawn.delete(elementId); - if (!isDisplayedByDefault || isHiddenDueToExclusiveAlwaysDrawnElements) - currAlwaysDrawn.add(elementId); - } else { - currAlwaysDrawn.delete(elementId); - if (isDisplayedByDefault && !isHiddenDueToExclusiveAlwaysDrawnElements) - currNeverDrawn.add(elementId); - } - } - this._props.viewport.setNeverDrawn(currNeverDrawn); - this._props.viewport.setAlwaysDrawn(currAlwaysDrawn, this._props.viewport.isAlwaysDrawnExclusive); - } - - private onVisibilityChangeInternal() { - if (this._pendingVisibilityChange) - return; - - this._pendingVisibilityChange = setTimeout(() => { - this.onVisibilityChange.raiseEvent(); - this._pendingVisibilityChange = undefined; - }, 0); - } - - // eslint-disable-next-line @typescript-eslint/naming-convention - private onViewChanged = (_vp: Viewport) => { - this.onVisibilityChangeInternal(); - }; - - // eslint-disable-next-line @typescript-eslint/naming-convention - private onElementAlwaysDrawnChanged = () => { - this.onVisibilityChangeInternal(); - }; - - // eslint-disable-next-line @typescript-eslint/naming-convention - private onElementNeverDrawnChanged = () => { - this.onVisibilityChangeInternal(); - }; - - private getCategoryParentModelId(categoryNode: TreeNodeItem): Id64String | undefined { - return categoryNode.extendedData ? categoryNode.extendedData.modelId : /* istanbul ignore next */ undefined; - } - - private getElementModelId(elementNode: TreeNodeItem): Id64String | undefined { - return elementNode.extendedData ? elementNode.extendedData.modelId : /* istanbul ignore next */ undefined; - } - - private getElementCategoryId(elementNode: TreeNodeItem): Id64String | undefined { - return elementNode.extendedData ? elementNode.extendedData.categoryId : /* istanbul ignore next */ undefined; - } - - private async getSubjectModelIds(subjectIds: Id64String[]) { - return (await Promise.all(subjectIds.map(async (id) => this._subjectModelIdsCache.getSubjectModelIds(id)))) - .reduce((allModelIds: Id64String[], curr: Id64String[]) => [...allModelIds, ...curr], []); - } - - // istanbul ignore next - private getAssemblyElementIds(assemblyId: Id64String) { - return this._elementIdsCache.getAssemblyElementIds(assemblyId); - } - - // istanbul ignore next - private async getGroupedElementIds(groupingNodeKey: GroupingNodeKey) { - return this._elementIdsCache.getGroupedElementIds(groupingNodeKey); - } -} - -interface ModelInfo { - id: Id64String; - isHidden: boolean; -} - -class SubjectModelIdsCache { - private _imodel: IModelConnection; - private _subjectsHierarchy: Map | undefined; - private _subjectModels: Map | undefined; - private _init: Promise | undefined; - - constructor(imodel: IModelConnection) { - this._imodel = imodel; - } - - private async initSubjectModels() { - const querySubjects = (): AsyncIterableIterator<{ id: Id64String, parentId?: Id64String, targetPartitionId?: Id64String }> => { - const subjectsQuery = ` - SELECT ECInstanceId id, Parent.Id parentId, json_extract(JsonProperties, '$.Subject.Model.TargetPartition') targetPartitionId - FROM bis.Subject - `; - return this._imodel.query(subjectsQuery, undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames }); - }; - const queryModels = (): AsyncIterableIterator<{ id: Id64String, parentId: Id64String, content?: string }> => { - const modelsQuery = ` - SELECT p.ECInstanceId id, p.Parent.Id parentId, json_extract(p.JsonProperties, '$.PhysicalPartition.Model.Content') content - FROM bis.InformationPartitionElement p - INNER JOIN bis.GeometricModel3d m ON m.ModeledElement.Id = p.ECInstanceId - WHERE NOT m.IsPrivate - `; - return this._imodel.query(modelsQuery, undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames }); - }; - - function pushToMap(map: Map, key: TKey, value: TValue) { - let list = map.get(key); - if (!list) { - list = []; - map.set(key, list); - } - list.push(value); - } - - this._subjectsHierarchy = new Map(); - const targetPartitionSubjects = new Map(); - for await (const subject of querySubjects()) { - if (subject.parentId) - pushToMap(this._subjectsHierarchy, subject.parentId, subject.id); - if (subject.targetPartitionId) - pushToMap(targetPartitionSubjects, subject.targetPartitionId, subject.id); - } - - this._subjectModels = new Map(); - for await (const model of queryModels()) { - const subjectIds = targetPartitionSubjects.get(model.id) ?? []; - if (!subjectIds.includes(model.parentId)) - subjectIds.push(model.parentId); - - const v = { id: model.id, isHidden: (model.content !== undefined) }; - subjectIds.forEach((subjectId) => { - pushToMap(this._subjectModels!, subjectId, v); - }); - } - } - - private async initCache() { - if (!this._init) { - this._init = this.initSubjectModels().then(() => { }); - } - return this._init; - } - - private appendSubjectModelsRecursively(modelIds: Id64String[], subjectId: Id64String) { - const subjectModelIds = this._subjectModels!.get(subjectId); - if (subjectModelIds) - modelIds.push(...subjectModelIds.map((info) => info.id)); - - const childSubjectIds = this._subjectsHierarchy!.get(subjectId); - if (childSubjectIds) - childSubjectIds.forEach((cs) => this.appendSubjectModelsRecursively(modelIds, cs)); - } - - public async getSubjectModelIds(subjectId: Id64String): Promise { - await this.initCache(); - const modelIds = new Array(); - this.appendSubjectModelsRecursively(modelIds, subjectId); - return modelIds; - } -} - -interface GroupedElementIds { - modelId?: string; - categoryId?: string; - elementIds: CachingElementIdsContainer; -} - -// istanbul ignore next -class ElementIdsCache { - private _assemblyElementIdsCache = new Map(); - private _groupedElementIdsCache = new Map(); - - constructor(private _imodel: IModelConnection, private _rulesetId: string) { - } - - public clear() { - this._assemblyElementIdsCache.clear(); - this._groupedElementIdsCache.clear(); - } - - public getAssemblyElementIds(assemblyId: Id64String) { - const ids = this._assemblyElementIdsCache.get(assemblyId); - if (ids) - return ids; - - const container = createAssemblyElementIdsContainer(this._imodel, this._rulesetId, assemblyId); - this._assemblyElementIdsCache.set(assemblyId, container); - return container; - } - - public async getGroupedElementIds(groupingNodeKey: GroupingNodeKey): Promise { - const keyString = JSON.stringify(groupingNodeKey); - const ids = this._groupedElementIdsCache.get(keyString); - if (ids) - return ids; - const info = await createGroupedElementsInfo(this._imodel, this._rulesetId, groupingNodeKey); - this._groupedElementIdsCache.set(keyString, info); - return info; - } -} - -async function* createInstanceIdsGenerator(imodel: IModelConnection, rulesetId: string, displayType: string, inputKeys: Keys) { - const res = await Presentation.presentation.getContentInstanceKeys({ - imodel, - rulesetOrId: rulesetId, - displayType, - keys: new KeySet(inputKeys), - }); - for await (const key of res.items()) { - yield key.id; - } -} - -// istanbul ignore next -class CachingElementIdsContainer { - private _generator; - private _ids; - constructor(generator: AsyncGenerator) { - this._generator = generator; - this._ids = new Array(); - } - public async* getElementIds() { - for (const id of this._ids) { - yield id; - } - for await (const id of this._generator) { - this._ids.push(id); - yield id; - } - } -} - -function createAssemblyElementIdsContainer(imodel: IModelConnection, rulesetId: string, assemblyId: Id64String) { - return new CachingElementIdsContainer(createInstanceIdsGenerator(imodel, rulesetId, "AssemblyElementsRequest", [{ className: "BisCore:Element", id: assemblyId }])); -} - -async function createGroupedElementsInfo(imodel: IModelConnection, rulesetId: string, groupingNodeKey: GroupingNodeKey) { - const groupedElementIdsContainer = new CachingElementIdsContainer(createInstanceIdsGenerator(imodel, rulesetId, "AssemblyElementsRequest", [groupingNodeKey])); - const elementId = await groupedElementIdsContainer.getElementIds().next(); - if (elementId.done) - throw new Error("Invalid grouping node key"); - - let modelId, categoryId; - const query = `SELECT Model.Id AS modelId, Category.Id AS categoryId FROM bis.GeometricElement3d WHERE ECInstanceId = ? LIMIT 1`; - for await (const modelAndCategoryIds of imodel.query(query, QueryBinder.from([elementId.value]), { rowFormat: QueryRowFormat.UseJsPropertyNames })) { - modelId = modelAndCategoryIds.modelId; - categoryId = modelAndCategoryIds.categoryId; - break; - } - return { modelId, categoryId, elementIds: groupedElementIdsContainer }; -} - -const createTooltip = (status: "visible" | "hidden" | "disabled", tooltipStringId: string | undefined): string => { - const statusStringId = `UiFramework:modelTree.status.${status}`; - const statusString = UiFramework.localization.getLocalizedString(statusStringId); - if (!tooltipStringId) - return statusString; - - tooltipStringId = `UiFramework:modelTree.tooltips.${tooltipStringId}`; - const tooltipString = UiFramework.localization.getLocalizedString(tooltipStringId); - return `${statusString}: ${tooltipString}`; -}; diff --git a/ui/appui-react/src/test/imodel-components/models-tree/ModelsVisibilityHandler.test.ts b/ui/appui-react/src/test/imodel-components/models-tree/ModelsVisibilityHandler.test.ts deleted file mode 100644 index 28497818ec0a..000000000000 --- a/ui/appui-react/src/test/imodel-components/models-tree/ModelsVisibilityHandler.test.ts +++ /dev/null @@ -1,1647 +0,0 @@ -/*--------------------------------------------------------------------------------------------- -* Copyright (c) Bentley Systems, Incorporated. All rights reserved. -* See LICENSE.md in the project root for license terms and full copyright notice. -*--------------------------------------------------------------------------------------------*/ - -import { expect } from "chai"; -import * as sinon from "sinon"; -import * as moq from "typemoq"; -import { PropertyRecord } from "@itwin/appui-abstract"; -import { BeEvent, Id64String, using } from "@itwin/core-bentley"; -import { QueryRowFormat } from "@itwin/core-common"; -import { - IModelApp, IModelConnection, NoRenderApp, PerModelCategoryVisibility, SpatialViewState, Viewport, ViewState, ViewState3d, -} from "@itwin/core-frontend"; -import { isPromiseLike } from "@itwin/core-react"; -import { createRandomId } from "@itwin/presentation-common/lib/cjs/test"; -import { FilteredPresentationTreeDataProvider } from "@itwin/presentation-components"; -import { IModelHierarchyChangeEventArgs, Presentation, PresentationManager } from "@itwin/presentation-frontend"; -import { ModelsVisibilityHandler, ModelsVisibilityHandlerProps } from "../../../appui-react/imodel-components/models-tree/ModelsVisibilityHandler"; -import { TestUtils } from "../../TestUtils"; -import { createCategoryNode, createElementClassGroupingNode, createElementNode, createModelNode, createSubjectNode } from "../Common"; - -describe("ModelsVisibilityHandler", () => { - - before(async () => { - await TestUtils.initializeUiFramework(); - await NoRenderApp.startup(); - }); - - after(async () => { - TestUtils.terminateUiFramework(); - await IModelApp.shutdown(); - }); - - const imodelMock = moq.Mock.ofType(); - - beforeEach(() => { - imodelMock.reset(); - }); - - interface ViewportMockProps { - viewState?: ViewState; - perModelCategoryVisibility?: PerModelCategoryVisibility.Overrides; - onViewedCategoriesPerModelChanged?: BeEvent<(vp: Viewport) => void>; - onViewedCategoriesChanged?: BeEvent<(vp: Viewport) => void>; - onViewedModelsChanged?: BeEvent<(vp: Viewport) => void>; - onAlwaysDrawnChanged?: BeEvent<() => void>; - onNeverDrawnChanged?: BeEvent<() => void>; - } - - const mockViewport = (props?: ViewportMockProps) => { - if (!props) - props = {}; - if (!props.viewState) - props.viewState = moq.Mock.ofType().object; - if (!props.perModelCategoryVisibility) - props.perModelCategoryVisibility = moq.Mock.ofType().object; - if (!props.onViewedCategoriesPerModelChanged) - props.onViewedCategoriesPerModelChanged = new BeEvent<(vp: Viewport) => void>(); - if (!props.onViewedCategoriesChanged) - props.onViewedCategoriesChanged = new BeEvent<(vp: Viewport) => void>(); - if (!props.onViewedModelsChanged) - props.onViewedModelsChanged = new BeEvent<(vp: Viewport) => void>(); - if (!props.onAlwaysDrawnChanged) - props.onAlwaysDrawnChanged = new BeEvent<() => void>(); - if (!props.onNeverDrawnChanged) - props.onNeverDrawnChanged = new BeEvent<() => void>(); - const vpMock = moq.Mock.ofType(); - vpMock.setup((x) => x.iModel).returns(() => imodelMock.object); - vpMock.setup((x) => x.view).returns(() => props!.viewState!); - vpMock.setup((x) => x.perModelCategoryVisibility).returns(() => props!.perModelCategoryVisibility!); - vpMock.setup((x) => x.onViewedCategoriesPerModelChanged).returns(() => props!.onViewedCategoriesPerModelChanged!); - vpMock.setup((x) => x.onViewedCategoriesChanged).returns(() => props!.onViewedCategoriesChanged!); - vpMock.setup((x) => x.onViewedModelsChanged).returns(() => props!.onViewedModelsChanged!); - vpMock.setup((x) => x.onAlwaysDrawnChanged).returns(() => props!.onAlwaysDrawnChanged!); - vpMock.setup((x) => x.onNeverDrawnChanged).returns(() => props!.onNeverDrawnChanged!); - return vpMock; - }; - - const createHandler = (partialProps?: Partial): ModelsVisibilityHandler => { - if (!partialProps) - partialProps = {}; - const props: ModelsVisibilityHandlerProps = { - rulesetId: "test", - viewport: partialProps.viewport || mockViewport().object, - hierarchyAutoUpdateEnabled: partialProps.hierarchyAutoUpdateEnabled, - }; - return new ModelsVisibilityHandler(props); - }; - - interface SubjectModelIdsMockProps { - imodelMock: moq.IMock; - subjectsHierarchy: Map; - subjectModels: Map>; - } - - const mockSubjectModelIds = (props: SubjectModelIdsMockProps) => { - props.imodelMock.setup((x) => x.query(moq.It.is((q: string) => (-1 !== q.indexOf("FROM bis.Subject"))), undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames })) - .returns(async function* () { - const list = new Array<{ id: Id64String, parentId: Id64String }>(); - props.subjectsHierarchy.forEach((ids, parentId) => ids.forEach((id) => list.push({ id, parentId }))); - while (list.length) - yield list.shift(); - }); - props.imodelMock.setup((x) => x.query(moq.It.is((q: string) => (-1 !== q.indexOf("FROM bis.InformationPartitionElement"))), undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames })) - .returns(async function* () { - const list = new Array<{ id: Id64String, parentId: Id64String, content?: string }>(); - props.subjectModels.forEach((modelInfos, subjectId) => modelInfos.forEach((modelInfo) => list.push({ id: modelInfo.id, parentId: subjectId, content: modelInfo.content }))); - while (list.length) - yield list.shift(); - }); - }; - - describe("constructor", () => { - - it("should subscribe for viewport change events", () => { - const vpMock = mockViewport(); - createHandler({ viewport: vpMock.object }); - expect(vpMock.object.onViewedCategoriesPerModelChanged.numberOfListeners).to.eq(1); - expect(vpMock.object.onViewedCategoriesChanged.numberOfListeners).to.eq(1); - expect(vpMock.object.onViewedModelsChanged.numberOfListeners).to.eq(1); - expect(vpMock.object.onAlwaysDrawnChanged.numberOfListeners).to.eq(1); - expect(vpMock.object.onNeverDrawnChanged.numberOfListeners).to.eq(1); - }); - - it("should subscribe for 'onIModelHierarchyChanged' event if hierarchy auto update is enabled", () => { - const presentationManagerMock = moq.Mock.ofType(); - const changeEvent = new BeEvent<(args: IModelHierarchyChangeEventArgs) => void>(); - presentationManagerMock.setup((x) => x.onIModelHierarchyChanged).returns(() => changeEvent); - Presentation.setPresentationManager(presentationManagerMock.object); - createHandler({ viewport: mockViewport().object, hierarchyAutoUpdateEnabled: true }); - expect(changeEvent.numberOfListeners).to.eq(1); - }); - - }); - - describe("dispose", () => { - - it("should unsubscribe from viewport change events", () => { - const vpMock = mockViewport(); - using(createHandler({ viewport: vpMock.object }), (_) => { }); - expect(vpMock.object.onViewedCategoriesPerModelChanged.numberOfListeners).to.eq(0); - expect(vpMock.object.onViewedCategoriesChanged.numberOfListeners).to.eq(0); - expect(vpMock.object.onViewedModelsChanged.numberOfListeners).to.eq(0); - expect(vpMock.object.onAlwaysDrawnChanged.numberOfListeners).to.eq(0); - expect(vpMock.object.onNeverDrawnChanged.numberOfListeners).to.eq(0); - }); - - it("should unsubscribe from 'onIModelHierarchyChanged' event", () => { - const presentationManagerMock = moq.Mock.ofType(); - const changeEvent = new BeEvent<(args: IModelHierarchyChangeEventArgs) => void>(); - presentationManagerMock.setup((x) => x.onIModelHierarchyChanged).returns(() => changeEvent); - Presentation.setPresentationManager(presentationManagerMock.object); - using(createHandler({ viewport: mockViewport().object, hierarchyAutoUpdateEnabled: true }), (_) => { }); - expect(changeEvent.numberOfListeners).to.eq(0); - }); - - }); - - describe("getDisplayStatus", () => { - - it("returns disabled when node is not an instance node", async () => { - const node = { - __key: { - type: "custom", - version: 0, - pathFromRoot: [], - }, - id: "custom", - label: PropertyRecord.fromString("custom"), - }; - - const vpMock = mockViewport(); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.false; - expect(result).to.include({ state: "hidden", isDisabled: true }); - }); - }); - - describe("subject", () => { - - it("return disabled when active view is not spatial", async () => { - const node = createSubjectNode(); - const vpMock = mockViewport(); - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.true; - if (isPromiseLike(result)) - expect(await result).to.include({ state: "hidden", isDisabled: true }); - }); - }); - - it("return 'hidden' when all models are not displayed", async () => { - const subjectIds = ["0x1", "0x2"]; - const node = createSubjectNode(subjectIds); - mockSubjectModelIds({ - imodelMock, - subjectsHierarchy: new Map([["0x0", subjectIds]]), - subjectModels: new Map([ - [subjectIds[0], [{ id: "0x3" }, { id: "0x4" }]], - [subjectIds[1], [{ id: "0x5" }, { id: "0x6" }]], - ]), - }); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x3")).returns(() => false); - viewStateMock.setup((x) => x.viewsModel("0x4")).returns(() => false); - viewStateMock.setup((x) => x.viewsModel("0x5")).returns(() => false); - viewStateMock.setup((x) => x.viewsModel("0x6")).returns(() => false); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.true; - if (isPromiseLike(result)) - expect(await result).to.include({ state: "hidden" }); - }); - }); - - it("return 'visible' when at least one direct model is displayed", async () => { - const subjectIds = ["0x1", "0x2"]; - const node = createSubjectNode(subjectIds); - mockSubjectModelIds({ - imodelMock, - subjectsHierarchy: new Map([["0x0", subjectIds]]), - subjectModels: new Map([ - [subjectIds[0], [{ id: "0x3" }, { id: "0x4" }]], - [subjectIds[1], [{ id: "0x5" }, { id: "0x6" }]], - ]), - }); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x3")).returns(() => false); - viewStateMock.setup((x) => x.viewsModel("0x4")).returns(() => false); - viewStateMock.setup((x) => x.viewsModel("0x5")).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x6")).returns(() => false); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.true; - if (isPromiseLike(result)) - expect(await result).to.include({ state: "visible" }); - }); - }); - - it("return 'visible' when at least one nested model is displayed", async () => { - const subjectIds = ["0x1", "0x2"]; - const node = createSubjectNode(subjectIds); - mockSubjectModelIds({ - imodelMock, - subjectsHierarchy: new Map([ - [subjectIds[0], ["0x3"]], - [subjectIds[1], ["0x4"]], - ["0x3", ["0x5", "0x6"]], - ["0x7", ["0x8"]], - ]), - subjectModels: new Map([ - ["0x6", [{ id: "0x10" }, { id: "0x11" }]], - ]), - }); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x10")).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x11")).returns(() => false); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.true; - if (isPromiseLike(result)) - expect(await result).to.include({ state: "visible" }); - }); - }); - - it("initializes subject models cache only once", async () => { - const node = createSubjectNode(); - const key = node.__key.instanceKeys[0]; - - mockSubjectModelIds({ - imodelMock, - subjectsHierarchy: new Map([["0x0", [key.id]]]), - subjectModels: new Map([[key.id, [{ id: "0x1" }, { id: "0x2" }]]]), - }); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel(moq.It.isAny())).returns(() => false); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - await Promise.all([handler.getVisibilityStatus(node, node.__key), handler.getVisibilityStatus(node, node.__key)]); - // expect the `query` to be called only twice (once for subjects and once for models) - imodelMock.verify((x) => x.query(moq.It.isAnyString(), undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames }), moq.Times.exactly(2)); - }); - }); - - describe("filtered", () => { - - it("return 'visible' when subject node matches filter and at least one model is visible", async () => { - const node = createSubjectNode(); - const key = node.__key.instanceKeys[0]; - - const filteredProvider = moq.Mock.ofType(); - filteredProvider.setup((x) => x.nodeMatchesFilter(node)).returns(() => true); - - mockSubjectModelIds({ - imodelMock, - subjectsHierarchy: new Map([["0x0", [key.id]]]), - subjectModels: new Map([[key.id, [{ id: "0x10" }, { id: "0x20" }]]]), - }); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x10")).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x20")).returns(() => false); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - handler.setFilteredDataProvider(filteredProvider.object); - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.true; - if (isPromiseLike(result)) - expect(await result).to.include({ state: "visible" }); - filteredProvider.verifyAll(); - }); - }); - - it("return 'visible' when subject node with children matches filter and at least one model is visible", async () => { - const parentSubjectId = "0x1"; - const childSubjectId = "0x2"; - const node = createSubjectNode(parentSubjectId); - const childNode = createSubjectNode(childSubjectId); - - const filteredProvider = moq.Mock.ofType(); - filteredProvider.setup(async (x) => x.getNodes(node)).returns(async () => [childNode]).verifiable(moq.Times.never()); - filteredProvider.setup(async (x) => x.getNodes(childNode)).returns(async () => []).verifiable(moq.Times.never()); - filteredProvider.setup((x) => x.nodeMatchesFilter(moq.It.isAny())).returns(() => true); - - mockSubjectModelIds({ - imodelMock, - subjectsHierarchy: new Map([ - [parentSubjectId, [childSubjectId]], - ]), - subjectModels: new Map([ - [parentSubjectId, [{ id: "0x10" }, { id: "0x11" }]], - [childSubjectId, [{ id: "0x20" }]], - ]), - }); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x10")).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x11")).returns(() => false); - viewStateMock.setup((x) => x.viewsModel("0x20")).returns(() => false); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - handler.setFilteredDataProvider(filteredProvider.object); - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.true; - if (isPromiseLike(result)) - expect(await result).to.include({ state: "visible" }); - filteredProvider.verifyAll(); - }); - }); - - it("return 'visible' when subject node with children does not match filter and at least one child has visible models", async () => { - const parentSubjectId = "0x1"; - const childSubjectIds = ["0x2", "0x3"]; - const node = createSubjectNode(parentSubjectId); - const childNodes = [createSubjectNode(childSubjectIds[0]), createSubjectNode(childSubjectIds[1])]; - - const filteredProvider = moq.Mock.ofType(); - filteredProvider.setup(async (x) => x.getNodes(node)).returns(async () => childNodes).verifiable(moq.Times.once()); - filteredProvider.setup(async (x) => x.getNodes(childNodes[0])).returns(async () => []).verifiable(moq.Times.never()); - filteredProvider.setup(async (x) => x.getNodes(childNodes[1])).returns(async () => []).verifiable(moq.Times.never()); - filteredProvider.setup((x) => x.getNodeKey(childNodes[0])).returns(() => childNodes[0].__key).verifiable(moq.Times.once()); - filteredProvider.setup((x) => x.getNodeKey(childNodes[1])).returns(() => childNodes[1].__key).verifiable(moq.Times.once()); - filteredProvider.setup((x) => x.nodeMatchesFilter(node)).returns(() => false); - filteredProvider.setup((x) => x.nodeMatchesFilter(childNodes[0])).returns(() => true); - filteredProvider.setup((x) => x.nodeMatchesFilter(childNodes[1])).returns(() => true); - - mockSubjectModelIds({ - imodelMock, - subjectsHierarchy: new Map([ - [parentSubjectId, childSubjectIds], - ]), - subjectModels: new Map([ - [parentSubjectId, [{ id: "0x10" }]], - [childSubjectIds[0], [{ id: "0x20" }]], - [childSubjectIds[1], [{ id: "0x30" }]], - ]), - }); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x10")).returns(() => false); - viewStateMock.setup((x) => x.viewsModel("0x20")).returns(() => false); - viewStateMock.setup((x) => x.viewsModel("0x30")).returns(() => true); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - handler.setFilteredDataProvider(filteredProvider.object); - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.true; - if (isPromiseLike(result)) - expect(await result).to.include({ state: "visible" }); - filteredProvider.verifyAll(); - }); - }); - - it("return 'hidden' when subject node with children does not match filter and children models are not visible", async () => { - const parentSubjectIds = ["0x1", "0x2"]; - const childSubjectId = "0x3"; - const node = createSubjectNode(parentSubjectIds); - const childNode = createSubjectNode(childSubjectId); - - const filteredProvider = moq.Mock.ofType(); - filteredProvider.setup(async (x) => x.getNodes(node)).returns(async () => [childNode]).verifiable(moq.Times.once()); - filteredProvider.setup(async (x) => x.getNodes(childNode)).returns(async () => []).verifiable(moq.Times.never()); - filteredProvider.setup((x) => x.getNodeKey(childNode)).returns(() => childNode.__key).verifiable(moq.Times.once()); - filteredProvider.setup((x) => x.nodeMatchesFilter(node)).returns(() => false); - filteredProvider.setup((x) => x.nodeMatchesFilter(childNode)).returns(() => true); - - mockSubjectModelIds({ - imodelMock, - subjectsHierarchy: new Map([ - [parentSubjectIds[0], [childSubjectId]], - [parentSubjectIds[1], [childSubjectId]], - ]), - subjectModels: new Map([ - [parentSubjectIds[0], [{ id: "0x10" }]], - [parentSubjectIds[1], [{ id: "0x20" }]], - [childSubjectId, [{ id: "0x30" }]], - ]), - }); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x10")).returns(() => false); - viewStateMock.setup((x) => x.viewsModel("0x20")).returns(() => false); - viewStateMock.setup((x) => x.viewsModel("0x30")).returns(() => false); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - handler.setFilteredDataProvider(filteredProvider.object); - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.true; - if (isPromiseLike(result)) - expect(await result).to.include({ state: "hidden" }); - filteredProvider.verifyAll(); - }); - }); - - }); - - }); - - describe("model", () => { - - it("return disabled when active view is not spatial", async () => { - const node = createModelNode(); - const vpMock = mockViewport(); - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.false; - expect(result).to.include({ state: "hidden", isDisabled: true }); - }); - }); - - it("return 'visible' when displayed", async () => { - const node = createModelNode(); - const key = node.__key.instanceKeys[0]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel(key.id)).returns(() => true); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.false; - expect(result).to.include({ state: "visible" }); - }); - }); - - it("returns 'hidden' when not displayed", async () => { - const node = createModelNode(); - const key = node.__key.instanceKeys[0]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel(key.id)).returns(() => false); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.false; - expect(result).to.include({ state: "hidden" }); - }); - }); - - }); - - describe("category", () => { - - it("return disabled when model not displayed", async () => { - const parentModelNode = createModelNode(); - const parentModelKey = parentModelNode.__key.instanceKeys[0]; - const categoryNode = createCategoryNode(parentModelKey); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel(parentModelKey.id)).returns(() => false); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(categoryNode, categoryNode.__key); - expect(isPromiseLike(result)).to.be.false; - expect(result).to.include({ state: "hidden", isDisabled: true }); - }); - }); - - it("return 'visible' when model displayed, category not displayed but per-model override says it's displayed", async () => { - const parentModelNode = createModelNode(); - const parentModelKey = parentModelNode.__key.instanceKeys[0]; - const categoryNode = createCategoryNode(parentModelKey); - const categoryKey = categoryNode.__key.instanceKeys[0]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory(categoryKey.id)).returns(() => false); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel(parentModelKey.id)).returns(() => true); - - const perModelCategoryVisibilityMock = moq.Mock.ofType(); - perModelCategoryVisibilityMock.setup((x) => x.getOverride(parentModelKey.id, categoryKey.id)).returns(() => PerModelCategoryVisibility.Override.Show); - - const vpMock = mockViewport({ viewState: viewStateMock.object, perModelCategoryVisibility: perModelCategoryVisibilityMock.object }); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(categoryNode, categoryNode.__key); - expect(isPromiseLike(result)).to.be.false; - expect(result).to.include({ state: "visible" }); - }); - }); - - it("return 'visible' when model displayed, category displayed and there're no per-model overrides", async () => { - const parentModelNode = createModelNode(); - const parentModelKey = parentModelNode.__key.instanceKeys[0]; - const categoryNode = createCategoryNode(parentModelKey); - const key = categoryNode.__key.instanceKeys[0]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory(key.id)).returns(() => true); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel(parentModelKey.id)).returns(() => true); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(categoryNode, categoryNode.__key); - expect(isPromiseLike(result)).to.be.false; - expect(result).to.include({ state: "visible" }); - }); - }); - - it("return 'hidden' when model displayed, category displayed but per-model override says it's not displayed", async () => { - const parentModelNode = createModelNode(); - const parentModelKey = parentModelNode.__key.instanceKeys[0]; - const categoryNode = createCategoryNode(parentModelKey); - const categoryKey = categoryNode.__key.instanceKeys[0]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory(categoryKey.id)).returns(() => true); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel(parentModelKey.id)).returns(() => true); - - const perModelCategoryVisibilityMock = moq.Mock.ofType(); - perModelCategoryVisibilityMock.setup((x) => x.getOverride(parentModelKey.id, categoryKey.id)).returns(() => PerModelCategoryVisibility.Override.Hide); - - const vpMock = mockViewport({ viewState: viewStateMock.object, perModelCategoryVisibility: perModelCategoryVisibilityMock.object }); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(categoryNode, categoryNode.__key); - expect(isPromiseLike(result)).to.be.false; - expect(result).to.include({ state: "hidden" }); - }); - }); - - it("return 'hidden' when model displayed, category not displayed and there're no per-model overrides", async () => { - const parentModelNode = createModelNode(); - const parentModelKey = parentModelNode.__key.instanceKeys[0]; - const categoryNode = createCategoryNode(parentModelKey); - const key = categoryNode.__key.instanceKeys[0]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory(key.id)).returns(() => false); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel(parentModelKey.id)).returns(() => true); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(categoryNode, categoryNode.__key); - expect(isPromiseLike(result)).to.be.false; - expect(result).to.include({ state: "hidden" }); - }); - }); - - it("return 'hidden' when category has no parent model and category is not displayed", async () => { - const categoryNode = createCategoryNode(); - const categoryKey = categoryNode.__key.instanceKeys[0]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory(categoryKey.id)).returns(() => false); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(categoryNode, categoryNode.__key); - expect(isPromiseLike(result)).to.be.false; - expect(result).to.include({ state: "hidden" }); - }); - }); - - }); - - describe("element class grouping", () => { - - it("returns disabled when model not displayed", async () => { - const groupedElementIds = ["0x11", "0x12", "0x13"]; - const node = createElementClassGroupingNode(groupedElementIds); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => false); - const vpMock = mockViewport({ viewState: viewStateMock.object }); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - // note: need to override to avoid running queries on the imodel - (handler as any).getGroupedElementIds = async () => ({ - categoryId: "0x1", - modelId: "0x2", - elementIds: { - async* getElementIds() { - for (const id of groupedElementIds) - yield id; - }, - }, - }); - - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.true; - expect(await result).to.include({ state: "hidden", isDisabled: true }); - }); - }); - - it("returns 'visible' when model displayed and at least one element is in always displayed list", async () => { - const groupedElementIds = ["0x11", "0x12", "0x13"]; - const node = createElementClassGroupingNode(groupedElementIds); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => false); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); - const vpMock = mockViewport({ viewState: viewStateMock.object }); - const alwaysDrawn = new Set([groupedElementIds[1]]); - vpMock.setup((x) => x.neverDrawn).returns(() => undefined); - vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDrawn); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - // note: need to override to avoid running queries on the imodel - (handler as any).getGroupedElementIds = async () => ({ - categoryId: "0x1", - modelId: "0x2", - elementIds: { - async* getElementIds() { - for (const id of groupedElementIds) - yield id; - }, - }, - }); - - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.true; - expect(await result).to.include({ state: "visible" }); - }); - }); - - it("returns 'hidden' when model displayed and there's at least one element in always exclusive displayed list that's not grouped under node", async () => { - const groupedElementIds = ["0x11", "0x12", "0x13"]; - const node = createElementClassGroupingNode(groupedElementIds); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => true); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); - const vpMock = mockViewport({ viewState: viewStateMock.object }); - const alwaysDrawn = new Set(["0x4"]); - vpMock.setup((x) => x.neverDrawn).returns(() => undefined); - vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDrawn); - vpMock.setup((x) => x.isAlwaysDrawnExclusive).returns(() => true); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - // note: need to override to avoid running queries on the imodel - (handler as any).getGroupedElementIds = async () => ({ - categoryId: "0x1", - modelId: "0x2", - elementIds: { - async* getElementIds() { - for (const id of groupedElementIds) - yield id; - }, - }, - }); - - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.true; - expect(await result).to.include({ state: "hidden" }); - }); - }); - - it("returns 'hidden' when model displayed and all elements are in never displayed list", async () => { - const groupedElementIds = ["0x11", "0x12", "0x13"]; - const node = createElementClassGroupingNode(groupedElementIds); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => true); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); - const vpMock = mockViewport({ viewState: viewStateMock.object }); - const neverDrawn = new Set(groupedElementIds); - vpMock.setup((x) => x.neverDrawn).returns(() => neverDrawn); - vpMock.setup((x) => x.alwaysDrawn).returns(() => new Set()); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - // note: need to override to avoid running queries on the imodel - (handler as any).getGroupedElementIds = async () => ({ - categoryId: "0x1", - modelId: "0x2", - elementIds: { - async* getElementIds() { - for (const id of groupedElementIds) - yield id; - }, - }, - }); - - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.true; - expect(await result).to.include({ state: "hidden" }); - }); - }); - - it("returns 'hidden' when model displayed and category not displayed", async () => { - const groupedElementIds = ["0x11", "0x12", "0x13"]; - const node = createElementClassGroupingNode(groupedElementIds); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => false); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); - const vpMock = mockViewport({ viewState: viewStateMock.object }); - const neverDrawn = new Set(["0x11"]); - vpMock.setup((x) => x.neverDrawn).returns(() => neverDrawn); - vpMock.setup((x) => x.alwaysDrawn).returns(() => new Set()); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - // note: need to override to avoid running queries on the imodel - (handler as any).getGroupedElementIds = async () => ({ - categoryId: "0x1", - modelId: "0x2", - elementIds: { - async* getElementIds() { - for (const id of groupedElementIds) - yield id; - }, - }, - }); - - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.true; - expect(await result).to.include({ state: "hidden" }); - }); - }); - - it("returns 'visible' when model displayed and category displayed", async () => { - const groupedElementIds = ["0x11", "0x12", "0x13"]; - const node = createElementClassGroupingNode(groupedElementIds); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => true); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); - const vpMock = mockViewport({ viewState: viewStateMock.object }); - const neverDrawn = new Set(["0x11"]); - vpMock.setup((x) => x.neverDrawn).returns(() => neverDrawn); - vpMock.setup((x) => x.alwaysDrawn).returns(() => new Set()); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - // note: need to override to avoid running queries on the imodel - (handler as any).getGroupedElementIds = async () => ({ - categoryId: "0x1", - modelId: "0x2", - elementIds: { - async* getElementIds() { - for (const id of groupedElementIds) - yield id; - }, - }, - }); - - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.true; - expect(await result).to.include({ state: "visible" }); - }); - }); - - }); - - describe("element", () => { - - it("returns disabled when modelId not set", async () => { - const node = createElementNode(undefined, "0x1"); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel(moq.It.isAny())).returns(() => true); - const vpMock = mockViewport({ viewState: viewStateMock.object }); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.false; - expect(result).to.include({ state: "hidden", isDisabled: true }); - }); - }); - - it("returns disabled when model not displayed", async () => { - const node = createElementNode("0x2", "0x1"); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => false); - const vpMock = mockViewport({ viewState: viewStateMock.object }); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.false; - expect(result).to.include({ state: "hidden", isDisabled: true }); - }); - }); - - it("returns 'hidden' when model displayed, category displayed, but element is in never displayed list", async () => { - const node = createElementNode("0x2", "0x1"); - const key = node.__key.instanceKeys[0]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => true); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); - const vpMock = mockViewport({ viewState: viewStateMock.object }); - const neverDrawn = new Set([key.id]); - vpMock.setup((x) => x.neverDrawn).returns(() => neverDrawn); - vpMock.setup((x) => x.alwaysDrawn).returns(() => undefined); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.false; - expect(result).to.include({ state: "hidden" }); - }); - }); - - it("returns 'visible' when model displayed and element is in always displayed list", async () => { - const node = createElementNode("0x2", "0x1"); - const key = node.__key.instanceKeys[0]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => false); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); - const vpMock = mockViewport({ viewState: viewStateMock.object }); - const alwaysDrawn = new Set([key.id]); - vpMock.setup((x) => x.neverDrawn).returns(() => undefined); - vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDrawn); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.false; - expect(result).to.include({ state: "visible" }); - }); - }); - - it("returns 'visible' when model displayed, category displayed and element is in neither 'never' nor 'always' displayed", async () => { - const node = createElementNode("0x2", "0x1"); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => true); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); - const vpMock = mockViewport({ viewState: viewStateMock.object }); - vpMock.setup((x) => x.alwaysDrawn).returns(() => undefined); - vpMock.setup((x) => x.neverDrawn).returns(() => undefined); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.false; - expect(result).to.include({ state: "visible" }); - }); - }); - - it("returns 'hidden' when model displayed, category not displayed and element is in neither 'never' nor 'always' displayed", async () => { - const node = createElementNode("0x2", "0x1"); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => false); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); - const vpMock = mockViewport({ viewState: viewStateMock.object }); - vpMock.setup((x) => x.alwaysDrawn).returns(() => undefined); - vpMock.setup((x) => x.neverDrawn).returns(() => undefined); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.false; - expect(result).to.include({ state: "hidden" }); - }); - }); - - it("returns 'hidden' when model displayed, category displayed and some other element is exclusively 'always' displayed", async () => { - const node = createElementNode("0x2", "0x1"); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => true); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); - const vpMock = mockViewport({ viewState: viewStateMock.object }); - vpMock.setup((x) => x.isAlwaysDrawnExclusive).returns(() => true); - vpMock.setup((x) => x.alwaysDrawn).returns(() => new Set([createRandomId()])); - vpMock.setup((x) => x.neverDrawn).returns(() => undefined); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.false; - expect(result).to.include({ state: "hidden" }); - }); - }); - - it("returns 'hidden' when model displayed, categoryId not set and element is in neither 'never' nor 'always' displayed", async () => { - const node = createElementNode("0x2", undefined); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory(moq.It.isAny())).returns(() => true); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => true); - const vpMock = mockViewport({ viewState: viewStateMock.object }); - vpMock.setup((x) => x.alwaysDrawn).returns(() => new Set()); - vpMock.setup((x) => x.neverDrawn).returns(() => new Set()); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const result = handler.getVisibilityStatus(node, node.__key); - expect(isPromiseLike(result)).to.be.false; - expect(result).to.include({ state: "hidden" }); - }); - }); - - }); - - }); - - describe("changeVisibility", () => { - - it("does nothing when node is not an instance node", async () => { - const node = { - __key: { - type: "custom", - version: 0, - pathFromRoot: [], - }, - id: "custom", - label: PropertyRecord.fromString("custom"), - }; - - const vpMock = mockViewport(); - vpMock.setup(async (x) => x.addViewedModels(moq.It.isAny())).verifiable(moq.Times.never()); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - await handler.changeVisibility(node, node.__key, true); - vpMock.verifyAll(); - }); - }); - - describe("subject", () => { - - it("does nothing for non-spatial views", async () => { - const node = createSubjectNode(); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => false); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - vpMock.setup(async (x) => x.addViewedModels(moq.It.isAny())).verifiable(moq.Times.never()); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - // note: need to override to avoid running a query on the imodel - (handler as any).getSubjectModelIds = async () => ["0x1", "0x2"]; - - await handler.changeVisibility(node, node.__key, true); - vpMock.verifyAll(); - }); - }); - - it("makes all subject models visible", async () => { - const node = createSubjectNode(); - const subjectModelIds = ["0x1", "0x2"]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - vpMock.setup(async (x) => x.addViewedModels(subjectModelIds)).verifiable(); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - // note: need to override to avoid running a query on the imodel - (handler as any).getSubjectModelIds = async () => subjectModelIds; - - await handler.changeVisibility(node, node.__key, true); - vpMock.verifyAll(); - }); - }); - - it("makes all subject models hidden", async () => { - const node = createSubjectNode(); - const subjectModelIds = ["0x1", "0x2"]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - vpMock.setup((x) => x.changeModelDisplay(subjectModelIds, false)).verifiable(); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - // note: need to override to avoid running a query on the imodel - (handler as any).getSubjectModelIds = async () => subjectModelIds; - - await handler.changeVisibility(node, node.__key, false); - vpMock.verifyAll(); - }); - }); - - describe("filtered", () => { - - ["visible", "hidden"].map((mode) => { - it(`makes all subject models ${mode} when subject node does not have children`, async () => { - const node = createSubjectNode(); - const key = node.__key.instanceKeys[0]; - const subjectModelIds = ["0x1", "0x2"]; - - const filteredDataProvider = moq.Mock.ofType(); - filteredDataProvider.setup(async (x) => x.getNodes(node)).returns(async () => []).verifiable(moq.Times.never()); - filteredDataProvider.setup((x) => x.nodeMatchesFilter(node)).returns(() => true); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - - mockSubjectModelIds({ - imodelMock, - subjectsHierarchy: new Map([]), - subjectModels: new Map([ - [key.id, [{ id: subjectModelIds[0], content: "reference" }, { id: subjectModelIds[1] }]], - ]), - }); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - if (mode === "visible") { - vpMock.setup(async (x) => x.addViewedModels(subjectModelIds)).verifiable(); - } else { - vpMock.setup((x) => x.changeModelDisplay(subjectModelIds, false)).verifiable(); - } - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - handler.setFilteredDataProvider(filteredDataProvider.object); - await handler.changeVisibility(node, node.__key, mode === "visible"); - vpMock.verifyAll(); - filteredDataProvider.verifyAll(); - }); - }); - - it(`makes only children ${mode} if parent node does not match filter`, async () => { - const node = createSubjectNode("0x1"); - const childNode = createSubjectNode("0x2"); - const parentSubjectModelIds = ["0x10", "0x11"]; - const childSubjectModelIds = ["0x20"]; - - const filteredDataProvider = moq.Mock.ofType(); - filteredDataProvider.setup(async (x) => x.getNodes(node)).returns(async () => [childNode]).verifiable(moq.Times.once()); - filteredDataProvider.setup(async (x) => x.getNodes(childNode)).returns(async () => []).verifiable(moq.Times.never()); - filteredDataProvider.setup((x) => x.getNodeKey(childNode)).returns(() => childNode.__key).verifiable(moq.Times.once()); - filteredDataProvider.setup((x) => x.nodeMatchesFilter(node)).returns(() => false); - filteredDataProvider.setup((x) => x.nodeMatchesFilter(childNode)).returns(() => true); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - - mockSubjectModelIds({ - imodelMock, - subjectsHierarchy: new Map([ - ["0x1", ["0x2"]], - ]), - subjectModels: new Map([ - ["0x1", [{ id: parentSubjectModelIds[0] }, { id: parentSubjectModelIds[1] }]], - ["0x2", [{ id: childSubjectModelIds[0] }]], - ]), - }); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - if (mode === "visible") { - vpMock.setup(async (x) => x.addViewedModels(childSubjectModelIds)).verifiable(); - } else { - vpMock.setup((x) => x.changeModelDisplay(childSubjectModelIds, false)).verifiable(); - } - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - handler.setFilteredDataProvider(filteredDataProvider.object); - await handler.changeVisibility(node, node.__key, mode === "visible"); - vpMock.verifyAll(); - filteredDataProvider.verifyAll(); - }); - - }); - - }); - - }); - - }); - - describe("model", () => { - - it("does nothing for non-spatial views", async () => { - const node = createModelNode(); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => false); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - vpMock.setup(async (x) => x.addViewedModels(moq.It.isAny())).verifiable(moq.Times.never()); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - await handler.changeVisibility(node, node.__key, true); - vpMock.verifyAll(); - }); - }); - - it("makes model visible", async () => { - const node = createModelNode(); - const key = node.__key.instanceKeys[0]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - vpMock.setup(async (x) => x.addViewedModels([key.id])).verifiable(); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - await handler.changeVisibility(node, node.__key, true); - vpMock.verifyAll(); - }); - }); - - it("makes model hidden", async () => { - const node = createModelNode(); - const key = node.__key.instanceKeys[0]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - vpMock.setup((x) => x.changeModelDisplay([key.id], false)).verifiable(); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - await handler.changeVisibility(node, node.__key, false); - vpMock.verifyAll(); - }); - }); - - }); - - describe("category", () => { - - it("makes category visible through per-model override when it's not visible through category selector", async () => { - const parentModelNode = createModelNode(); - const parentModelKey = parentModelNode.__key.instanceKeys[0]; - const categoryNode = createCategoryNode(parentModelKey); - const categoryKey = categoryNode.__key.instanceKeys[0]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory(categoryKey.id)).returns(() => false); - - const perModelCategoryVisibilityMock = moq.Mock.ofType(); - - const vpMock = mockViewport({ viewState: viewStateMock.object, perModelCategoryVisibility: perModelCategoryVisibilityMock.object }); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - await handler.changeVisibility(categoryNode, categoryNode.__key, true); - perModelCategoryVisibilityMock.verify((x) => x.setOverride(parentModelKey.id, categoryKey.id, PerModelCategoryVisibility.Override.Show), moq.Times.once()); - vpMock.verify((x) => x.changeCategoryDisplay(moq.It.isAny(), moq.It.isAny(), moq.It.isAny()), moq.Times.never()); - }); - }); - - it("makes category hidden through override when it's visible through category selector", async () => { - const parentModelNode = createModelNode(); - const parentModelKey = parentModelNode.__key.instanceKeys[0]; - const categoryNode = createCategoryNode(parentModelKey); - const categoryKey = categoryNode.__key.instanceKeys[0]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory(categoryKey.id)).returns(() => true); - - const perModelCategoryVisibilityMock = moq.Mock.ofType(); - - const vpMock = mockViewport({ viewState: viewStateMock.object, perModelCategoryVisibility: perModelCategoryVisibilityMock.object }); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - await handler.changeVisibility(categoryNode, categoryNode.__key, false); - perModelCategoryVisibilityMock.verify((x) => x.setOverride(parentModelKey.id, categoryKey.id, PerModelCategoryVisibility.Override.Hide), moq.Times.once()); - vpMock.verify((x) => x.changeCategoryDisplay(moq.It.isAny(), moq.It.isAny(), moq.It.isAny()), moq.Times.never()); - }); - }); - - it("removes category override and enables all sub-categories when making visible and it's visible through category selector", async () => { - const parentModelNode = createModelNode(); - const parentModelKey = parentModelNode.__key.instanceKeys[0]; - const categoryNode = createCategoryNode(parentModelKey); - const categoryKey = categoryNode.__key.instanceKeys[0]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory(categoryKey.id)).returns(() => true); - - const perModelCategoryVisibilityMock = moq.Mock.ofType(); - - const vpMock = mockViewport({ viewState: viewStateMock.object, perModelCategoryVisibility: perModelCategoryVisibilityMock.object }); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - await handler.changeVisibility(categoryNode, categoryNode.__key, true); - perModelCategoryVisibilityMock.verify((x) => x.setOverride(parentModelKey.id, categoryKey.id, PerModelCategoryVisibility.Override.None), moq.Times.once()); - vpMock.verify((x) => x.changeCategoryDisplay([categoryKey.id], true, true), moq.Times.once()); - }); - }); - - it("removes category override when making hidden and it's hidden through category selector", async () => { - const parentModelNode = createModelNode(); - const parentModelKey = parentModelNode.__key.instanceKeys[0]; - const categoryNode = createCategoryNode(parentModelKey); - const categoryKey = categoryNode.__key.instanceKeys[0]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory(categoryKey.id)).returns(() => false); - - const perModelCategoryVisibilityMock = moq.Mock.ofType(); - - const vpMock = mockViewport({ viewState: viewStateMock.object, perModelCategoryVisibility: perModelCategoryVisibilityMock.object }); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - await handler.changeVisibility(categoryNode, categoryNode.__key, false); - perModelCategoryVisibilityMock.verify((x) => x.setOverride(parentModelKey.id, categoryKey.id, PerModelCategoryVisibility.Override.None), moq.Times.once()); - vpMock.verify((x) => x.changeCategoryDisplay(moq.It.isAny(), moq.It.isAny(), moq.It.isAny()), moq.Times.never()); - }); - }); - - it("makes category visible in selector and enables all sub-categories when category has no parent model", async () => { - const categoryNode = createCategoryNode(); - const categoryKey = categoryNode.__key.instanceKeys[0]; - - const vpMock = mockViewport(); - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - await handler.changeVisibility(categoryNode, categoryNode.__key, true); - vpMock.verify((x) => x.changeCategoryDisplay([categoryKey.id], true, true), moq.Times.once()); - }); - }); - - it("makes category hidden in selector when category has no parent model", async () => { - const categoryNode = createCategoryNode(); - const categoryKey = categoryNode.__key.instanceKeys[0]; - - const vpMock = mockViewport(); - vpMock.setup((x) => x.changeCategoryDisplay([categoryKey.id], false)).verifiable(); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - await handler.changeVisibility(categoryNode, categoryNode.__key, false); - vpMock.verify((x) => x.changeCategoryDisplay([categoryKey.id], false, false), moq.Times.once()); - }); - }); - - }); - - describe("element class grouping", () => { - - it("makes elements visible by removing from never displayed list and adding to always displayed list when category is not displayed", async () => { - const groupedElementIds = ["0x11", "0x12", "0x13"]; - const node = createElementClassGroupingNode(groupedElementIds); - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory("0x1")).returns(() => false); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x2")).returns(() => false); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - - const alwaysDisplayed = new Set(); - const neverDisplayed = new Set([groupedElementIds[0]]); - vpMock.setup((x) => x.isAlwaysDrawnExclusive).returns(() => false); - vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDisplayed); - vpMock.setup((x) => x.neverDrawn).returns(() => neverDisplayed); - vpMock.setup((x) => x.setAlwaysDrawn(moq.It.is((set) => { - return set.size === 3 - && groupedElementIds.reduce((result, id) => (result && set.has(id)), true); - }), false)).verifiable(); - vpMock.setup((x) => x.setNeverDrawn(moq.It.is((set) => (set.size === 0)))).verifiable(); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - // note: need to override to avoid running queries on the imodel - (handler as any).getGroupedElementIds = async () => ({ - categoryId: "0x1", - modelId: "0x2", - elementIds: { - async* getElementIds() { - for (const id of groupedElementIds) - yield id; - }, - }, - }); - - await handler.changeVisibility(node, node.__key, true); - vpMock.verifyAll(); - }); - }); - - }); - - describe("element", () => { - - it("makes element visible by only removing from never displayed list when element's category is displayed", async () => { - const node = createElementNode("0x4", "0x3"); - const key = node.__key.instanceKeys[0]; - const assemblyChildrenIds = ["0x1", "0x2"]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory("0x3")).returns(() => true); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x4")).returns(() => true); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - - const alwaysDisplayed = new Set(); - const neverDisplayed = new Set([key.id]); - vpMock.setup((x) => x.isAlwaysDrawnExclusive).returns(() => false); - vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDisplayed); - vpMock.setup((x) => x.neverDrawn).returns(() => neverDisplayed); - vpMock.setup((x) => x.setNeverDrawn(moq.It.is((set) => (set.size === 0)))).verifiable(); - vpMock.setup((x) => x.setAlwaysDrawn(moq.It.is((set) => (set.size === 0)), false)).verifiable(); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - // note: need to override to avoid running queries on the imodel - (handler as any).getAssemblyElementIds = () => ({ - async* getElementIds() { - for (const id of assemblyChildrenIds) - yield id; - }, - }); - - await handler.changeVisibility(node, node.__key, true); - vpMock.verifyAll(); - }); - }); - - it("makes element visible by removing from never displayed list and adding to always displayed list when category is not displayed", async () => { - const node = createElementNode("0x4", "0x3"); - const key = node.__key.instanceKeys[0]; - const assemblyChildrenIds = ["0x1", "0x2"]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory("0x4")).returns(() => false); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x3")).returns(() => false); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - - const alwaysDisplayed = new Set(); - const neverDisplayed = new Set([key.id]); - vpMock.setup((x) => x.isAlwaysDrawnExclusive).returns(() => false); - vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDisplayed); - vpMock.setup((x) => x.neverDrawn).returns(() => neverDisplayed); - vpMock.setup((x) => x.setAlwaysDrawn(moq.It.is((set) => { - return set.size === 3 - && set.has(key.id) - && assemblyChildrenIds.reduce((result, id) => (result && set.has(id)), true); - }), false)).verifiable(); - vpMock.setup((x) => x.setNeverDrawn(moq.It.is((set) => (set.size === 0)))).verifiable(); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - // note: need to override to avoid running a query on the imodel - (handler as any).getAssemblyElementIds = () => ({ - async* getElementIds() { - for (const id of assemblyChildrenIds) - yield id; - }, - }); - - await handler.changeVisibility(node, node.__key, true); - vpMock.verifyAll(); - }); - }); - - it("makes element visible by adding to always displayed list when category is displayed, but element is hidden due to other elements exclusively always drawn", async () => { - const node = createElementNode("0x4", "0x3"); - const key = node.__key.instanceKeys[0]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory("0x4")).returns(() => true); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x3")).returns(() => true); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - - const alwaysDisplayed = new Set([createRandomId()]); - vpMock.setup((x) => x.isAlwaysDrawnExclusive).returns(() => true); - vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDisplayed); - vpMock.setup((x) => x.neverDrawn).returns(() => undefined); - vpMock.setup((x) => x.setAlwaysDrawn(moq.It.is((set) => { - return set.size === 2 && set.has(key.id); - }), true)).verifiable(); - vpMock.setup((x) => x.setNeverDrawn(moq.It.is((set) => (set.size === 0)))).verifiable(); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - // note: need to override to avoid running a query on the imodel - (handler as any).getAssemblyElementIds = () => ({ - async* getElementIds() { }, - }); - - await handler.changeVisibility(node, node.__key, true); - vpMock.verifyAll(); - }); - }); - - it("makes element hidden by only removing from always displayed list when element's category is not displayed", async () => { - const node = createElementNode("0x4", "0x3"); - const key = node.__key.instanceKeys[0]; - const assemblyChildrenIds = ["0x1", "0x2"]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory("0x3")).returns(() => false); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x4")).returns(() => true); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - - const alwaysDisplayed = new Set([key.id]); - const neverDisplayed = new Set(); - vpMock.setup((x) => x.isAlwaysDrawnExclusive).returns(() => false); - vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDisplayed); - vpMock.setup((x) => x.neverDrawn).returns(() => neverDisplayed); - vpMock.setup((x) => x.setNeverDrawn(moq.It.is((set) => (set.size === 0)))).verifiable(); - vpMock.setup((x) => x.setAlwaysDrawn(moq.It.is((set) => (set.size === 0)), false)).verifiable(); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - // note: need to override to avoid running queries on the imodel - (handler as any).getAssemblyElementIds = () => ({ - async* getElementIds() { - for (const id of assemblyChildrenIds) - yield id; - }, - }); - - await handler.changeVisibility(node, node.__key, false); - vpMock.verifyAll(); - }); - }); - - it("makes element hidden by removing from always displayed list and adding to never displayed list when category is displayed", async () => { - const node = createElementNode("0x4", "0x3"); - const key = node.__key.instanceKeys[0]; - const assemblyChildrenIds = ["0x1", "0x2"]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory("0x3")).returns(() => true); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x4")).returns(() => true); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - - const alwaysDisplayed = new Set([key.id]); - const neverDisplayed = new Set(); - vpMock.setup((x) => x.isAlwaysDrawnExclusive).returns(() => false); - vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDisplayed); - vpMock.setup((x) => x.neverDrawn).returns(() => neverDisplayed); - vpMock.setup((x) => x.setAlwaysDrawn(moq.It.is((set) => (set.size === 0)), false)).verifiable(); - vpMock.setup((x) => x.setNeverDrawn(moq.It.is((set) => { - return set.size === 3 - && set.has(key.id) - && assemblyChildrenIds.reduce((result, id) => (result && set.has(id)), true); - }))).verifiable(); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - // note: need to override to avoid running a query on the imodel - (handler as any).getAssemblyElementIds = () => ({ - async* getElementIds() { - for (const id of assemblyChildrenIds) - yield id; - }, - }); - - await handler.changeVisibility(node, node.__key, false); - vpMock.verifyAll(); - }); - }); - - it("makes element hidden by removing from always displayed list when category is displayed and there are exclusively always drawn elements", async () => { - const node = createElementNode("0x4", "0x3"); - const key = node.__key.instanceKeys[0]; - - const viewStateMock = moq.Mock.ofType(); - viewStateMock.setup((x) => x.viewsCategory("0x3")).returns(() => true); - viewStateMock.setup((x) => x.isSpatialView()).returns(() => true); - viewStateMock.setup((x) => x.viewsModel("0x4")).returns(() => true); - - const vpMock = mockViewport({ viewState: viewStateMock.object }); - - const alwaysDisplayed = new Set([key.id, createRandomId()]); - vpMock.setup((x) => x.isAlwaysDrawnExclusive).returns(() => true); - vpMock.setup((x) => x.alwaysDrawn).returns(() => alwaysDisplayed); - vpMock.setup((x) => x.neverDrawn).returns(() => undefined); - vpMock.setup((x) => x.setAlwaysDrawn(moq.It.is((set) => (set.size === 1 && !set.has(key.id))), true)).verifiable(); - vpMock.setup((x) => x.setNeverDrawn(moq.It.is((set) => (set.size === 0)))).verifiable(); - - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - // note: need to override to avoid running a query on the imodel - (handler as any).getAssemblyElementIds = () => ({ - async* getElementIds() { }, - }); - - await handler.changeVisibility(node, node.__key, false); - vpMock.verifyAll(); - }); - }); - - }); - - }); - - describe("visibility change event", () => { - - it("raises event on `onAlwaysDrawnChanged` event", async () => { - const evt = new BeEvent(); - const vpMock = mockViewport({ onAlwaysDrawnChanged: evt }); - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const spy = sinon.spy(); - handler.onVisibilityChange.addListener(spy); - evt.raiseEvent(vpMock.object); - await new Promise((resolve) => setTimeout(resolve)); - expect(spy).to.be.calledOnce; - }); - }); - - it("raises event on `onNeverDrawnChanged` event", async () => { - const evt = new BeEvent(); - const vpMock = mockViewport({ onNeverDrawnChanged: evt }); - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const spy = sinon.spy(); - handler.onVisibilityChange.addListener(spy); - evt.raiseEvent(vpMock.object); - await new Promise((resolve) => setTimeout(resolve)); - expect(spy).to.be.calledOnce; - }); - }); - - it("raises event on `onViewedCategoriesChanged` event", async () => { - const evt = new BeEvent(); - const vpMock = mockViewport({ onViewedCategoriesChanged: evt }); - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const spy = sinon.spy(); - handler.onVisibilityChange.addListener(spy); - evt.raiseEvent(vpMock.object); - await new Promise((resolve) => setTimeout(resolve)); - expect(spy).to.be.calledOnce; - }); - }); - - it("raises event on `onViewedModelsChanged` event", async () => { - const evt = new BeEvent(); - const vpMock = mockViewport({ onViewedModelsChanged: evt }); - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const spy = sinon.spy(); - handler.onVisibilityChange.addListener(spy); - evt.raiseEvent(vpMock.object); - await new Promise((resolve) => setTimeout(resolve)); - expect(spy).to.be.calledOnce; - }); - }); - - it("raises event on `onViewedCategoriesPerModelChanged` event", async () => { - const evt = new BeEvent(); - const vpMock = mockViewport({ onViewedCategoriesPerModelChanged: evt }); - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const spy = sinon.spy(); - handler.onVisibilityChange.addListener(spy); - evt.raiseEvent(vpMock.object); - await new Promise((resolve) => setTimeout(resolve)); - expect(spy).to.be.calledOnce; - }); - }); - - it("raises event once when multiple affecting events are fired", async () => { - const evts = { - onViewedCategoriesPerModelChanged: new BeEvent<(vp: Viewport) => void>(), - onViewedCategoriesChanged: new BeEvent<(vp: Viewport) => void>(), - onViewedModelsChanged: new BeEvent<(vp: Viewport) => void>(), - onAlwaysDrawnChanged: new BeEvent<() => void>(), - onNeverDrawnChanged: new BeEvent<() => void>(), - }; - const vpMock = mockViewport({ ...evts }); - await using(createHandler({ viewport: vpMock.object }), async (handler) => { - const spy = sinon.spy(); - handler.onVisibilityChange.addListener(spy); - evts.onViewedCategoriesPerModelChanged.raiseEvent(vpMock.object); - evts.onViewedCategoriesChanged.raiseEvent(vpMock.object); - evts.onViewedModelsChanged.raiseEvent(vpMock.object); - evts.onAlwaysDrawnChanged.raiseEvent(); - evts.onNeverDrawnChanged.raiseEvent(); - await new Promise((resolve) => setTimeout(resolve)); - expect(spy).to.be.calledOnce; - }); - }); - - }); - -}); diff --git a/ui/framework/src/test/imodel-components/models-tree/ModelsVisibilityHandler.test.ts b/ui/framework/src/test/imodel-components/models-tree/ModelsVisibilityHandler.test.ts index 228fff6aafed..8863b6d2db73 100644 --- a/ui/framework/src/test/imodel-components/models-tree/ModelsVisibilityHandler.test.ts +++ b/ui/framework/src/test/imodel-components/models-tree/ModelsVisibilityHandler.test.ts @@ -99,8 +99,8 @@ describe("ModelsVisibilityHandler", () => { }); props.imodelMock.setup((x) => x.query(moq.It.is((q: string) => (-1 !== q.indexOf("FROM bis.InformationPartitionElement"))))) .returns(async function* () { - const list = new Array<{ id: Id64String, subjectId: Id64String, content?: string }>(); - props.subjectModels.forEach((modelInfos, subjectId) => modelInfos.forEach((modelInfo) => list.push({ id: modelInfo.id, subjectId, content: modelInfo.content }))); + const list = new Array<{ id: Id64String, parentId: Id64String, content?: string }>(); + props.subjectModels.forEach((modelInfos, subjectId) => modelInfos.forEach((modelInfo) => list.push({ id: modelInfo.id, parentId: subjectId, content: modelInfo.content }))); while (list.length) yield list.shift(); }); diff --git a/ui/framework/src/ui-framework/imodel-components/models-tree/ModelsVisibilityHandler.ts b/ui/framework/src/ui-framework/imodel-components/models-tree/ModelsVisibilityHandler.ts index d060bb802ec1..2c2770be989d 100644 --- a/ui/framework/src/ui-framework/imodel-components/models-tree/ModelsVisibilityHandler.ts +++ b/ui/framework/src/ui-framework/imodel-components/models-tree/ModelsVisibilityHandler.ts @@ -398,43 +398,62 @@ class SubjectModelIdsCache { this._imodel = imodel; } - private async initSubjectsHierarchy() { - this._subjectsHierarchy = new Map(); - const ecsql = `SELECT ECInstanceId id, Parent.Id parentId FROM bis.Subject WHERE Parent IS NOT NULL`; - const result = this._imodel.query(ecsql); - for await (const row of result) { - let list = this._subjectsHierarchy.get(row.parentId); + private async initSubjectModels() { + const querySubjects = (): AsyncIterableIterator<{ id: Id64String, parentId?: Id64String, targetPartitionId?: Id64String }> => { + const subjectsQuery = ` + SELECT ECInstanceId id, Parent.Id parentId, json_extract(JsonProperties, '$.Subject.Model.TargetPartition') targetPartitionId + FROM bis.Subject + `; + return this._imodel.query(subjectsQuery); + }; + const queryModels = (): AsyncIterableIterator<{ id: Id64String, parentId: Id64String, content?: string }> => { + const modelsQuery = ` + SELECT p.ECInstanceId id, p.Parent.Id parentId, json_extract(p.JsonProperties, '$.PhysicalPartition.Model.Content') content + FROM bis.InformationPartitionElement p + INNER JOIN bis.GeometricModel3d m ON m.ModeledElement.Id = p.ECInstanceId + WHERE NOT m.IsPrivate + `; + return this._imodel.query(modelsQuery); + }; + + function pushToMap(map: Map, key: TKey, value: TValue) { + let list = map.get(key); if (!list) { list = []; - this._subjectsHierarchy.set(row.parentId, list); + map.set(key, list); } - list.push(row.id); + list.push(value); + } + + this._subjectsHierarchy = new Map(); + const targetPartitionSubjects = new Map(); + for await (const subject of querySubjects()) { + // istanbul ignore else + if (subject.parentId) + pushToMap(this._subjectsHierarchy, subject.parentId, subject.id); + // istanbul ignore if + if (subject.targetPartitionId) + pushToMap(targetPartitionSubjects, subject.targetPartitionId, subject.id); } - } - private async initSubjectModels() { this._subjectModels = new Map(); - const ecsql = ` - SELECT p.ECInstanceId id, s.ECInstanceId subjectId, json_extract(p.JsonProperties, '$.PhysicalPartition.Model.Content') content - FROM bis.InformationPartitionElement p - INNER JOIN bis.GeometricModel3d m ON m.ModeledElement.Id = p.ECInstanceId - INNER JOIN bis.Subject s ON (s.ECInstanceId = p.Parent.Id OR json_extract(s.JsonProperties, '$.Subject.Model.TargetPartition') = printf('0x%x', p.ECInstanceId)) - WHERE NOT m.IsPrivate`; - const result = this._imodel.query(ecsql); - for await (const row of result) { - let list = this._subjectModels.get(row.subjectId); - if (!list) { - list = []; - this._subjectModels.set(row.subjectId, list); - } - const isHidden = row.content !== undefined; - list.push({ id: row.id, isHidden }); + for await (const model of queryModels()) { + // istanbul ignore next + const subjectIds = targetPartitionSubjects.get(model.id) ?? []; + // istanbul ignore else + if (!subjectIds.includes(model.parentId)) + subjectIds.push(model.parentId); + + const v = { id: model.id, isHidden: (model.content !== undefined) }; + subjectIds.forEach((subjectId) => { + pushToMap(this._subjectModels!, subjectId, v); + }); } } private async initCache() { if (!this._init) { - this._init = Promise.all([this.initSubjectModels(), this.initSubjectsHierarchy()]).then(() => { }); + this._init = this.initSubjectModels().then(() => { }); } return this._init; }