Skip to content

Commit

Permalink
Merge branch 'master' into JonasD/categories-tree-with-definition-con…
Browse files Browse the repository at this point in the history
…tainers
  • Loading branch information
JonasDov authored Feb 13, 2025
2 parents 7bf0153 + 9d9f47d commit 9ca55f0
Show file tree
Hide file tree
Showing 7 changed files with 510 additions and 54 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Adjusted modeled element / sub-model visibility controls. Now, if visibility of modeled element is changed, visibility of sub-model is adjusted accordingly and vice versa.",
"packageName": "@itwin/tree-widget-react",
"email": "100586436+JonasDov@users.noreply.github.com",
"dependentChangeType": "patch"
}
19 changes: 15 additions & 4 deletions packages/itwin/tree-widget/public/locales/en/TreeWidget.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"hiddenThroughModelSelector": "Model display is hidden through model selector",
"someCategoriesHidden": "Some categories are visible and some are hidden",
"allCategoriesVisible": "All categories visible",
"allCategoriesHidden": "All categories hidden"
"allCategoriesHidden": "All categories hidden",
"someSubModelsVisible": "Some sub-models are visible"
},
"category": {
"displayedThroughPerModelOverride": "Per-model category override set to 'Show'",
Expand All @@ -50,21 +51,31 @@
"allElementsHidden": "All category elements are in the never drawn list or none of the category elements are in the exclusive always drawn list",
"someElementsAreHidden": "There are both visible and hidden elements in this category",
"allElementsVisible": "All category elements are in the always drawn list",
"hiddenThroughModel": "Model is hidden"
"allElementsAndSubModelsHidden": "All elements are hidden",
"someElementsOrSubModelsHidden": "Some elements are hidden and some are visible",
"hiddenThroughModel": "Model is hidden",
"allModeledElementsHidden": "All elements in the category are hidden",
"someModeledElementsHidden": "There are both visible and hidden elements in this category"
},
"element": {
"hiddenThroughNeverDrawnList": "Element(s) in \"never drawn\" list",
"displayedThroughAlwaysDrawnList": "Element(s) in \"always drawn\" list",
"hiddenDueToOtherElementsExclusivelyAlwaysDrawn": "Other elements in \"exclusively always drawn\" list",
"hiddenThroughModel": "Model is not displayed",
"hiddenThroughCategory": "Category is not displayed"
"hiddenThroughCategory": "Category is not displayed",
"someElementsAreHidden": "Modeled elements subModel has partial state",
"partialThroughSubModel": "Modeled element is hidden, but its subModel is visible",
"partialThroughElement": "Modeled element is visible due to override, but its subModel is hidden",
"partialThroughCategory": "Modeled element is visible due to category visibility, but its subModel is hidden"
},
"groupingNode": {
"allElementsHidden": "All elements are in the never drawn list or none of the elements are in the exclusive always drawn list",
"someElementsAreHidden": "There are both visible and hidden elements",
"allElementsVisible": "All elements are in the always drawn list",
"visibleThroughCategory": "All elements are visible through category",
"hiddenThroughCategory": "All elements are hidden through category"
"hiddenThroughCategory": "All elements are hidden through category",
"allElementsAndSubModelsHidden": "All elements are hidden",
"someElementsOrSubModelsHidden": "There are both visible and hidden elements"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ export function createFakeIdsCache(props?: IdsCacheMockProps): ModelsTreeIdsCach
getCategoryElementsCount: sinon.stub<[Id64String, Id64String], Promise<number>>().callsFake(async (_, categoryId) => {
return props?.categoryElements?.get(categoryId)?.length ?? 0;
}),
hasSubModel: sinon.stub<[Id64String], Promise<boolean>>().callsFake(async () => false),
getCategoriesModeledElements: sinon.stub<[Id64String, Id64Array], Promise<Id64Array>>().callsFake(async () => []),
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import type { HierarchyVisibilityHandler } from "../../../../tree-widget-react/c
import type { ModelsTreeVisibilityHandlerProps } from "../../../../tree-widget-react/components/trees/models-tree/internal/ModelsTreeVisibilityHandler.js";
import type { IModelConnection, Viewport } from "@itwin/core-frontend";
import type { GeometricElement3dProps, QueryBinder } from "@itwin/core-common";
import type { HierarchyNodeIdentifiersPath, HierarchyProvider } from "@itwin/presentation-hierarchies";
import type { GroupingHierarchyNode, HierarchyNodeIdentifiersPath, HierarchyProvider, NonGroupingHierarchyNode } from "@itwin/presentation-hierarchies";
import type { Id64String } from "@itwin/core-bentley";
import type { ValidateNodeProps } from "./VisibilityValidation.js";

Expand Down Expand Up @@ -1763,6 +1763,214 @@ describe("HierarchyBasedVisibilityHandler", () => {
};
}

describe("with modeled elements", () => {
let iModel: IModelConnection;
let createdIds: {
subjectId: Id64String;
modeledElementId: Id64String;
modelId: Id64String;
categoryId: Id64String;
subModelCategoryId: Id64String;
subModelElementId: Id64String;
};

before(async function () {
const { imodel, ...ids } = await buildIModel(this, async (builder, testSchema) => {
const rootSubject: InstanceKey = { className: "BisCore.Subject", id: IModel.rootSubjectId };
const partition = insertPhysicalPartition({ builder, codeValue: "model", parentId: rootSubject.id });
const model = insertPhysicalSubModel({ builder, modeledElementId: partition.id });
const category = insertSpatialCategory({ builder, codeValue: "category" });
const modeledElement = insertPhysicalElement({
builder,
userLabel: `element`,
modelId: model.id,
categoryId: category.id,
classFullName: testSchema.items.SubModelablePhysicalObject.fullName,
});
const subModel = insertPhysicalSubModel({ builder, modeledElementId: modeledElement.id });
const subModelCategory = insertSpatialCategory({ builder, codeValue: "category2" });
const subModelElement = insertPhysicalElement({ builder, userLabel: `element2`, modelId: subModel.id, categoryId: subModelCategory.id });
return {
subjectId: rootSubject.id,
modeledElementId: modeledElement.id,
modelId: model.id,
categoryId: category.id,
subModelCategoryId: subModelCategory.id,
subModelElementId: subModelElement.id,
};
});
iModel = imodel;
createdIds = ids;
});

const testCases: Array<{
name: string;
getTargetNode: (ids: {
subjectId: Id64String;
modelId: Id64String;
categoryId: Id64String;
modeledElementId: Id64String;
subModelCategoryId: Id64String;
subModelElementId: Id64String;
}) => NonGroupingHierarchyNode | GroupingHierarchyNode;
expectations: (ids: {
modelId: Id64String;
categoryId: Id64String;
modeledElementId: Id64String;
subModelCategoryId: Id64String;
subModelElementId: Id64String;
}) => ReturnType<typeof VisibilityExpectations.all>;
}> = [
{
name: "modeled element's children display is turned on when its subject display is turned on",
getTargetNode: (ids) => createSubjectHierarchyNode(ids.subjectId),
expectations: () => VisibilityExpectations.all("visible"),
},
{
name: "modeled element's children display is turned on when its model display is turned on",
getTargetNode: (ids) => createModelHierarchyNode(ids.modelId, true),
expectations: () => VisibilityExpectations.all("visible"),
},
{
name: "modeled element's children display is turned on when its category display is turned on",
getTargetNode: (ids) => createCategoryHierarchyNode(ids.modelId, ids.categoryId, true),
expectations: () => VisibilityExpectations.all("visible"),
},
{
name: "modeled element's children display is turned on when its class grouping node display is turned on",
getTargetNode: (ids) => createClassGroupingHierarchyNode({ modelId: ids.modelId, categoryId: ids.categoryId, elements: [ids.modeledElementId] }),
expectations: () => VisibilityExpectations.all("visible"),
},
{
name: "modeled element's children display is turned on when its display is turned on",
getTargetNode: (ids) =>
createElementHierarchyNode({
modelId: ids.modelId,
categoryId: ids.categoryId,
elementId: ids.modeledElementId,
hasChildren: true,
}),
expectations: () => VisibilityExpectations.all("visible"),
},
{
name: "modeled element's children display is turned on when its sub-model display is turned on",
getTargetNode: (ids) => createModelHierarchyNode(ids.modeledElementId, true),
expectations: (ids) => ({
subject: () => "partial",
model: (modelId) => (modelId === ids.modelId ? "partial" : "visible"),
category: ({ categoryId }) => {
if (categoryId === ids.subModelCategoryId) {
return "visible";
}
return "partial";
},
groupingNode: ({ elementIds }) => {
if (elementIds.includes(ids.modeledElementId)) {
return "partial";
}
return "visible";
},
element: ({ elementId }) => {
if (elementId === ids.modeledElementId) {
return "partial";
}
return "visible";
},
}),
},
{
name: "modeled element, its model and category have partial visibility when its sub-model element's category display is turned on",
getTargetNode: (ids) => createCategoryHierarchyNode(ids.modeledElementId, ids.subModelCategoryId, true),
expectations: (ids) => ({
subject: () => "partial",
model: () => "partial",
category: ({ categoryId }) => {
if (categoryId === ids.subModelCategoryId) {
return "visible";
}
return "partial";
},
groupingNode: ({ elementIds }) => {
if (elementIds.includes(ids.modeledElementId)) {
return "partial";
}
return "visible";
},
element: ({ elementId }) => {
if (elementId === ids.subModelElementId) {
return "visible";
}
return "partial";
},
}),
},
{
name: "modeled element, its model and category have partial visibility when its sub-model element's display is turned on",
getTargetNode: (ids) =>
createElementHierarchyNode({
modelId: ids.modeledElementId,
categoryId: ids.subModelCategoryId,
elementId: ids.subModelElementId,
}),
expectations: (ids) => ({
subject: () => "partial",
model: () => "partial",
category: ({ categoryId }) => {
if (categoryId === ids.subModelCategoryId) {
return "visible";
}
return "partial";
},
groupingNode: ({ elementIds }) => {
if (elementIds.includes(ids.modeledElementId)) {
return "partial";
}
return "visible";
},
element: ({ elementId }) => {
if (elementId === ids.subModelElementId) {
return "visible";
}
return "partial";
},
}),
},
];

testCases.forEach(({ name, getTargetNode, expectations }) => {
it(name, async function () {
using visibilityTestData = createVisibilityTestData({ imodel: iModel });
const { handler, provider, viewport } = visibilityTestData;

await using(handler, async (_) => {
const nodeToChangeVisibility = getTargetNode(createdIds);
await validateHierarchyVisibility({
provider,
handler,
viewport,
visibilityExpectations: VisibilityExpectations.all("hidden"),
});
await handler.changeVisibility(nodeToChangeVisibility, true);
viewport.renderFrame();
await validateHierarchyVisibility({
provider,
handler,
viewport,
visibilityExpectations: expectations(createdIds),
});
await handler.changeVisibility(nodeToChangeVisibility, false);
viewport.renderFrame();
await validateHierarchyVisibility({
provider,
handler,
viewport,
visibilityExpectations: VisibilityExpectations.all("hidden"),
});
});
});
});
});

it("by default everything is hidden", async function () {
const { imodel } = await buildIModel(this, async (builder) => {
const categoryId = insertSpatialCategory({ builder, codeValue: "category" }).id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { HierarchyVisibilityHandler } from "../../../../tree-widget-react/c

interface VisibilityExpectations {
subject(id: string): Visibility;
element(props: { modelId: Id64String; categoryId: Id64String; elementId: Id64String }): "visible" | "hidden";
element(props: { modelId: Id64String; categoryId: Id64String; elementId: Id64String }): Visibility;
groupingNode(props: { modelId: Id64String; categoryId: Id64String; elementIds: Id64Array }): Visibility;
category(props: { modelId: Id64String; categoryId: Id64String }):
| Visibility
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class ModelsTreeIdsCache {
private _subjectInfos: Promise<Map<Id64String, SubjectInfo>> | undefined;
private _parentSubjectIds: Promise<Id64Array> | undefined; // the list should contain a subject id if its node should be shown as having children
private _modelInfos: Promise<Map<Id64String, ModelInfo>> | undefined;
private _modelWithCategoryModeledElements: Promise<Map<Id64String, Id64Set>> | undefined;
private _modelKeyPaths: Map<Id64String, Promise<HierarchyNodeIdentifiersPath[]>>;
private _subjectKeyPaths: Map<Id64String, Promise<HierarchyNodeIdentifiersPath>>;
private _categoryKeyPaths: Map<Id64String, Promise<HierarchyNodeIdentifiersPath[]>>;
Expand Down Expand Up @@ -266,6 +267,37 @@ export class ModelsTreeIdsCache {
}
}

private async *queryModeledElements() {
const query = `
SELECT
pe.ECInstanceId modeledElementId,
pe.Category.Id categoryId,
pe.Model.Id modelId
FROM BisCore.Model m
JOIN ${this._hierarchyConfig.elementClassSpecification} pe ON pe.ECInstanceId = m.ModeledElement.Id
`;
for await (const row of this._queryExecutor.createQueryReader({ ecsql: query }, { rowFormat: "ECSqlPropertyNames", limit: "unbounded" })) {
yield { modelId: row.modelId, categoryId: row.categoryId, modeledElementId: row.modeledElementId };
}
}

private async getModelWithCategoryModeledElements() {
this._modelWithCategoryModeledElements ??= (async () => {
const modelWithCategoryModeledElements = new Map<Id64String, Id64Set>();
for await (const { modelId, categoryId, modeledElementId } of this.queryModeledElements()) {
const key = `${modelId}-${categoryId}`;
const entry = modelWithCategoryModeledElements.get(key);
if (entry === undefined) {
modelWithCategoryModeledElements.set(key, new Set([modeledElementId]));
} else {
entry.add(modeledElementId);
}
}
return modelWithCategoryModeledElements;
})();
return this._modelWithCategoryModeledElements;
}

private async getModelInfos() {
this._modelInfos ??= (async () => {
const modelInfos = new Map<Id64String, { categories: Id64Set; elementCount: number }>();
Expand Down Expand Up @@ -307,6 +339,23 @@ export class ModelsTreeIdsCache {
return modelInfos.get(modelId)?.elementCount ?? 0;
}

public async hasSubModel(elementId: Id64String): Promise<boolean> {
const modelInfos = await this.getModelInfos();
return modelInfos.has(elementId);
}

public async getCategoriesModeledElements(modelId: Id64String, categoryIds: Id64Array): Promise<Id64Array> {
const modelWithCategoryModeledElements = await this.getModelWithCategoryModeledElements();
const result = new Array<Id64String>();
for (const categoryId of categoryIds) {
const entry = modelWithCategoryModeledElements.get(`${modelId}-${categoryId}`);
if (entry !== undefined) {
result.push(...entry);
}
}
return result;
}

public async createModelInstanceKeyPaths(modelId: Id64String): Promise<HierarchyNodeIdentifiersPath[]> {
let entry = this._modelKeyPaths.get(modelId);
if (!entry) {
Expand Down
Loading

0 comments on commit 9ca55f0

Please sign in to comment.