Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adjust modeled element / subModel visibility #1183

Merged
merged 31 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
58e7a6c
Fix modeled element visibility
JonasDov Feb 7, 2025
f58a41d
Add changeset
JonasDov Feb 7, 2025
cc6612e
Merge branch 'master' into JonasD/modeled-element-visibility-fix
JonasDov Feb 7, 2025
1b36456
Run extract api
JonasDov Feb 7, 2025
2f7a647
Update packages/itwin/tree-widget/public/locales/en/TreeWidget.json
JonasDov Feb 11, 2025
c04478a
Update packages/itwin/tree-widget/src/tree-widget-react/components/tr…
JonasDov Feb 11, 2025
6d62669
Update packages/itwin/tree-widget/src/tree-widget-react/components/tr…
JonasDov Feb 11, 2025
783efeb
Update change/@itwin-tree-widget-react-d7414fdf-e366-4667-8371-cb9c44…
JonasDov Feb 11, 2025
317e819
Update packages/itwin/tree-widget/src/test/trees/models-tree/internal…
JonasDov Feb 11, 2025
e5094da
Remove unnecessary promise.all and create keyy once
JonasDov Feb 11, 2025
6592eee
Rename modelId to modeledElementId
JonasDov Feb 11, 2025
7904ec9
Rename getDoesSubModelExist -> hasSubModel
JonasDov Feb 11, 2025
83d305b
Rename shouldntChangeSubModels to skipSubModels
JonasDov Feb 11, 2025
9265d75
Adress comments
JonasDov Feb 11, 2025
c19077a
Remove unused locale
JonasDov Feb 11, 2025
1c73c42
Update packages/itwin/tree-widget/src/test/trees/models-tree/internal…
JonasDov Feb 12, 2025
1db957b
Update packages/itwin/tree-widget/src/test/trees/models-tree/internal…
JonasDov Feb 12, 2025
fdf180c
Update packages/itwin/tree-widget/src/test/trees/models-tree/internal…
JonasDov Feb 12, 2025
b32a434
Update packages/itwin/tree-widget/src/test/trees/models-tree/internal…
JonasDov Feb 12, 2025
040a23f
Update packages/itwin/tree-widget/src/test/trees/models-tree/internal…
JonasDov Feb 12, 2025
cf96631
Update packages/itwin/tree-widget/src/test/trees/models-tree/internal…
JonasDov Feb 12, 2025
166b65d
Update packages/itwin/tree-widget/src/test/trees/models-tree/internal…
JonasDov Feb 12, 2025
d5d3d9c
Update packages/itwin/tree-widget/src/test/trees/models-tree/internal…
JonasDov Feb 12, 2025
f49030b
Adress comments
JonasDov Feb 12, 2025
afb8768
Merge branch 'master' into JonasD/modeled-element-visibility-fix
JonasDov Feb 12, 2025
bc5d354
Update packages/itwin/tree-widget/src/tree-widget-react/components/tr…
JonasDov Feb 13, 2025
7c15883
Update packages/itwin/tree-widget/public/locales/en/TreeWidget.json
JonasDov Feb 13, 2025
a6f7e14
Adress comments
JonasDov Feb 13, 2025
999b33a
Rename getPotentialSubModelVisibilityStatus -> getPotentialSubModelsV…
JonasDov Feb 13, 2025
7b65ba2
Adress comments
JonasDov Feb 13, 2025
709f9f2
Change from() toArray() -> of()
JonasDov Feb 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 subModels are visible"
JonasDov marked this conversation as resolved.
Show resolved Hide resolved
},
"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,220 @@ 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;
nodeCreatorFunc: (ids: {
grigasp marked this conversation as resolved.
Show resolved Hide resolved
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 elements children display is turned on / off when subject display is turned on / off",
JonasDov marked this conversation as resolved.
Show resolved Hide resolved
nodeCreatorFunc: (ids) => createSubjectHierarchyNode(ids.subjectId),
expectations: () => VisibilityExpectations.all("visible"),
},
{
name: "modeled elements children display is turned on / off when model display is turned on / off",
JonasDov marked this conversation as resolved.
Show resolved Hide resolved
nodeCreatorFunc: (ids) => createModelHierarchyNode(ids.modelId, true),
expectations: () => VisibilityExpectations.all("visible"),
},
{
name: "modeled elements children display is turned on / off when modeled elements parent categories display is turned on / off",
JonasDov marked this conversation as resolved.
Show resolved Hide resolved
nodeCreatorFunc: (ids) => createCategoryHierarchyNode(ids.modelId, ids.categoryId, true),
expectations: () => VisibilityExpectations.all("visible"),
},
{
name: "modeled elements children display is turned on / off when modeled elements class grouping node display is turned on / off",
JonasDov marked this conversation as resolved.
Show resolved Hide resolved
nodeCreatorFunc: (ids) => createClassGroupingHierarchyNode({ modelId: ids.modelId, categoryId: ids.categoryId, elements: [ids.modeledElementId] }),
expectations: () => VisibilityExpectations.all("visible"),
},
{
name: "modeled elements children display is turned on / off when modeled element display is turned on / off",
JonasDov marked this conversation as resolved.
Show resolved Hide resolved
nodeCreatorFunc: (ids) =>
createElementHierarchyNode({
modelId: ids.modelId,
categoryId: ids.categoryId,
elementId: ids.modeledElementId,
hasChildren: true,
}),
expectations: () => VisibilityExpectations.all("visible"),
},
{
name: "modeled elements children display is turned on / off when subModel display is turned on / off",
JonasDov marked this conversation as resolved.
Show resolved Hide resolved
nodeCreatorFunc: (ids) => createModelHierarchyNode(ids.modeledElementId, true),
expectations: (ids) => ({
subject: () => "partial",
model: (modelId) => (modelId === ids.modelId ? "partial" : "visible"),
category: ({ categoryId, modelId }) => {
if (categoryId === ids.subModelCategoryId) {
return "visible";
}
if (modelId === ids.modeledElementId) {
return "hidden";
}
return "partial";
grigasp marked this conversation as resolved.
Show resolved Hide resolved
},
groupingNode: ({ elementIds }) => {
if (elementIds.includes(ids.modeledElementId)) {
return "partial";
}
return "visible";
},
element: ({ elementId }) => {
if (elementId === ids.modeledElementId) {
return "partial";
}
return "visible";
},
}),
},
{
name: "model, category and modeled element have partial / off visibility when subModels category display is turned on / off",
JonasDov marked this conversation as resolved.
Show resolved Hide resolved
nodeCreatorFunc: (ids) => createCategoryHierarchyNode(ids.modeledElementId, ids.subModelCategoryId, true),
expectations: (ids) => ({
subject: () => "partial",
model: () => "partial",
category: ({ categoryId, modelId }) => {
if (categoryId === ids.subModelCategoryId) {
return "visible";
}
if (modelId === ids.modeledElementId) {
return "hidden";
}
return "partial";
grigasp marked this conversation as resolved.
Show resolved Hide resolved
},
groupingNode: ({ elementIds }) => {
if (elementIds.includes(ids.modeledElementId)) {
return "partial";
}
return "visible";
},
element: ({ elementId }) => {
if (elementId === ids.subModelElementId) {
return "visible";
}
return "partial";
},
}),
},
{
name: "model, category and modeled element have partial / off visibility when subModels elements display is turned on / off",
JonasDov marked this conversation as resolved.
Show resolved Hide resolved
nodeCreatorFunc: (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, nodeCreatorFunc, expectations }) => {
grigasp marked this conversation as resolved.
Show resolved Hide resolved
it(name, async function () {
using visibilityTestData = createVisibilityTestData({ imodel: iModel });
const { handler, provider, viewport } = visibilityTestData;

await using(handler, async (_) => {
const nodeToChangeVisibility = nodeCreatorFunc(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
Loading