From e95560cab520e49c80ebc4e4a5e9a23180e3bda3 Mon Sep 17 00:00:00 2001 From: Hoang Nam Le <50554904+hl662@users.noreply.github.com> Date: Thu, 20 Jun 2024 10:37:47 -0400 Subject: [PATCH 01/11] bump imodel read rpc interface version --- core/common/src/rpc/IModelReadRpcInterface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/src/rpc/IModelReadRpcInterface.ts b/core/common/src/rpc/IModelReadRpcInterface.ts index fc13b45cb937..556aa6b5efce 100644 --- a/core/common/src/rpc/IModelReadRpcInterface.ts +++ b/core/common/src/rpc/IModelReadRpcInterface.ts @@ -84,7 +84,7 @@ export abstract class IModelReadRpcInterface extends RpcInterface { // eslint-di public static readonly interfaceName = "IModelReadRpcInterface"; /** The semantic version of the interface. */ - public static interfaceVersion = "3.6.0"; + public static interfaceVersion = "3.7.0"; /*=========================================================================================== NOTE: Any add/remove/change to the methods below requires an update of the interface version. From 89ecbe12222dea579f9842184b56e5503bc2b8fc Mon Sep 17 00:00:00 2001 From: Hoang Nam Le <50554904+hl662@users.noreply.github.com> Date: Tue, 18 Jun 2024 11:14:25 -0400 Subject: [PATCH 02/11] add a loadAllSubcategories method in cache --- core/frontend/src/SubCategoriesCache.ts | 7 ++++++- core/frontend/src/ViewCreator3d.ts | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/frontend/src/SubCategoriesCache.ts b/core/frontend/src/SubCategoriesCache.ts index 199438bf9aad..d4e04e1e63e0 100644 --- a/core/frontend/src/SubCategoriesCache.ts +++ b/core/frontend/src/SubCategoriesCache.ts @@ -43,6 +43,7 @@ export class SubCategoriesCache { /** Request that the subcategory information for all of the specified categories is loaded. * If all such information has already been loaded, returns undefined. + * If no categories are specified, dispatches an asynchronous request to load all subcategories and returns a cancellable request object * Otherwise, dispatches an asynchronous request to load those categories which are not already loaded and returns a cancellable request object * containing the corresponding promise and the set of categories still to be loaded. */ @@ -58,7 +59,6 @@ export class SubCategoriesCache { return !request.wasCanceled; }); - return { missingCategoryIds: missing, promise, @@ -66,6 +66,11 @@ export class SubCategoriesCache { }; } + public async loadAllSubCategories(): Promise { + const results = await this._imodel.querySubCategories(); + if (undefined !== results) + this.processResults(results, new Set()); + } /** Given categoryIds, return which of these are not cached. */ private getMissing(categoryIds: Id64Arg): Id64Set | undefined { let missing: Id64Set | undefined; diff --git a/core/frontend/src/ViewCreator3d.ts b/core/frontend/src/ViewCreator3d.ts index 8d3b70efa0f4..fad4a86e0830 100644 --- a/core/frontend/src/ViewCreator3d.ts +++ b/core/frontend/src/ViewCreator3d.ts @@ -90,6 +90,7 @@ export class ViewCreator3d { const viewState = SpatialViewState.createFromProps(props, this._imodel); try { + await viewState.iModel.subcategories.loadAllSubCategories(); await viewState.load(); } catch { } From 723632a95cd3c54631631a7a371bf4ac31420d51 Mon Sep 17 00:00:00 2001 From: Hoang Nam Le <50554904+hl662@users.noreply.github.com> Date: Tue, 11 Jun 2024 10:50:01 -0400 Subject: [PATCH 03/11] refactor querySubcategories --- core/backend/src/IModelDb.ts | 14 ++++++++++---- core/common/src/rpc/IModelReadRpcInterface.ts | 2 +- core/frontend/src/IModelConnection.ts | 3 ++- .../src/frontend/IModelConnection.test.ts | 6 ++++++ 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/core/backend/src/IModelDb.ts b/core/backend/src/IModelDb.ts index 9c7f1ba26759..329aa9c094d3 100644 --- a/core/backend/src/IModelDb.ts +++ b/core/backend/src/IModelDb.ts @@ -706,16 +706,22 @@ export abstract class IModelDb extends IModel { } /** - * queries the BisCore.SubCategory table for the entries that are children of the passed categoryIds + * queries the BisCore.SubCategory table for the entries that are children of the passed categoryIds. + * If no iterable is passed, all subcategories are returned. If an empty iterable is passed, no subcategories are returned. * @param categoryIds categoryIds to query * @returns array of SubCategoryResultRow * @internal */ - public async querySubCategories(categoryIds: Iterable): Promise { + public async querySubCategories(categoryIds?: Iterable): Promise { const result: SubCategoryResultRow[] = []; + let query: string; + if (!categoryIds) { + query = `SELECT ECInstanceId as id, Parent.Id as parentId, Properties as appearance FROM BisCore.SubCategory`; + } else { + const where = [...categoryIds].join(","); + query = `SELECT ECInstanceId as id, Parent.Id as parentId, Properties as appearance FROM BisCore.SubCategory WHERE Parent.Id IN (${where})`; + } - const where = [...categoryIds].join(","); - const query = `SELECT ECInstanceId as id, Parent.Id as parentId, Properties as appearance FROM BisCore.SubCategory WHERE Parent.Id IN (${where})`; try { for await (const row of this.createQueryReader(query, undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames })) { result.push(row.toRow() as SubCategoryResultRow); diff --git a/core/common/src/rpc/IModelReadRpcInterface.ts b/core/common/src/rpc/IModelReadRpcInterface.ts index 556aa6b5efce..23f963722c76 100644 --- a/core/common/src/rpc/IModelReadRpcInterface.ts +++ b/core/common/src/rpc/IModelReadRpcInterface.ts @@ -94,7 +94,7 @@ export abstract class IModelReadRpcInterface extends RpcInterface { // eslint-di public async getConnectionProps(_iModelToken: IModelRpcOpenProps): Promise { return this.forward(arguments); } public async queryRows(_iModelToken: IModelRpcProps, _request: DbQueryRequest): Promise { return this.forward(arguments); } @RpcOperation.allowResponseCaching(RpcResponseCacheControl.Immutable) // eslint-disable-line deprecation/deprecation - public async querySubCategories(_iModelToken: IModelRpcProps, _categoryIds: CompressedId64Set): Promise { return this.forward(arguments); } + public async querySubCategories(_iModelToken: IModelRpcProps, _categoryIds?: CompressedId64Set): Promise { return this.forward(arguments); } public async queryBlob(_iModelToken: IModelRpcProps, _request: DbBlobRequest): Promise { return this.forward(arguments); } @RpcOperation.allowResponseCaching(RpcResponseCacheControl.Immutable) // eslint-disable-line deprecation/deprecation public async getModelProps(_iModelToken: IModelRpcProps, _modelIds: Id64String[]): Promise { return this.forward(arguments); } diff --git a/core/frontend/src/IModelConnection.ts b/core/frontend/src/IModelConnection.ts index d43524b03a3d..bcb48149f3f4 100644 --- a/core/frontend/src/IModelConnection.ts +++ b/core/frontend/src/IModelConnection.ts @@ -277,11 +277,12 @@ export abstract class IModelConnection extends IModel { /** * queries the BisCore.SubCategory table for the entries that are children of the passed categoryIds + * If no iterable is passed, all subcategories are returned. If an empty iterable is passed, no subcategories are returned. * @param compressedCategoryIds compressed category Ids * @returns array of SubCategoryResultRow * @internal */ - public async querySubCategories(compressedCategoryIds: CompressedId64Set): Promise { + public async querySubCategories(compressedCategoryIds?: CompressedId64Set): Promise { return IModelReadRpcInterface.getClientForRouting(this.routingContext.token).querySubCategories(this.getRpcProps(), compressedCategoryIds); } diff --git a/full-stack-tests/rpc-interface/src/frontend/IModelConnection.test.ts b/full-stack-tests/rpc-interface/src/frontend/IModelConnection.test.ts index e89046aea40f..3f8906bde717 100644 --- a/full-stack-tests/rpc-interface/src/frontend/IModelConnection.test.ts +++ b/full-stack-tests/rpc-interface/src/frontend/IModelConnection.test.ts @@ -346,6 +346,12 @@ describe("IModelReadRpcInterface Methods from an IModelConnection", () => { expect(candidate1Result).to.deep.eq({ ...expectedCandidate1Result, candidate: candidates[0] }); expect(candidate2Result).to.deep.eq({ ...expectedCandidate2Result, candidate: candidates[1] }); }); + + it("querySubCategories should get all subcategories when no argument is passed", async () => { + const result = await iModel.querySubCategories(); + expect(result).to.not.be.null; + expect(result.length).to.not.be.equal(0); + }); }); describe("Snapping", () => { From 187f93854951f4b216c10448387bf1031ae417b4 Mon Sep 17 00:00:00 2001 From: Hoang Nam Le <50554904+hl662@users.noreply.github.com> Date: Thu, 20 Jun 2024 10:58:34 -0400 Subject: [PATCH 04/11] Run rush change --- .../nam-default-subcategories_2024-06-20-14-56.json | 10 ++++++++++ .../nam-default-subcategories_2024-06-20-14-56.json | 10 ++++++++++ .../nam-default-subcategories_2024-06-20-14-56.json | 10 ++++++++++ .../nam-default-subcategories_2024-06-20-14-56.json | 10 ++++++++++ core/frontend/src/SubCategoriesCache.ts | 1 + 5 files changed, 41 insertions(+) create mode 100644 common/changes/@itwin/core-backend/nam-default-subcategories_2024-06-20-14-56.json create mode 100644 common/changes/@itwin/core-common/nam-default-subcategories_2024-06-20-14-56.json create mode 100644 common/changes/@itwin/core-frontend/nam-default-subcategories_2024-06-20-14-56.json create mode 100644 common/changes/@itwin/rpcinterface-full-stack-tests/nam-default-subcategories_2024-06-20-14-56.json diff --git a/common/changes/@itwin/core-backend/nam-default-subcategories_2024-06-20-14-56.json b/common/changes/@itwin/core-backend/nam-default-subcategories_2024-06-20-14-56.json new file mode 100644 index 000000000000..c2d4dda5bf35 --- /dev/null +++ b/common/changes/@itwin/core-backend/nam-default-subcategories_2024-06-20-14-56.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-backend", + "comment": "Update querySubCategories to fetch all subcategories when no categoryId iterable is passed in.", + "type": "none" + } + ], + "packageName": "@itwin/core-backend" +} \ No newline at end of file diff --git a/common/changes/@itwin/core-common/nam-default-subcategories_2024-06-20-14-56.json b/common/changes/@itwin/core-common/nam-default-subcategories_2024-06-20-14-56.json new file mode 100644 index 000000000000..2b9ae46e1553 --- /dev/null +++ b/common/changes/@itwin/core-common/nam-default-subcategories_2024-06-20-14-56.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-common", + "comment": "Update querySubCategories to fetch all subcategories when no categoryId iterable is passed in.", + "type": "none" + } + ], + "packageName": "@itwin/core-common" +} \ No newline at end of file diff --git a/common/changes/@itwin/core-frontend/nam-default-subcategories_2024-06-20-14-56.json b/common/changes/@itwin/core-frontend/nam-default-subcategories_2024-06-20-14-56.json new file mode 100644 index 000000000000..a919cedf2b20 --- /dev/null +++ b/common/changes/@itwin/core-frontend/nam-default-subcategories_2024-06-20-14-56.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-frontend", + "comment": "Load all subcategories when creating a default view.", + "type": "none" + } + ], + "packageName": "@itwin/core-frontend" +} \ No newline at end of file diff --git a/common/changes/@itwin/rpcinterface-full-stack-tests/nam-default-subcategories_2024-06-20-14-56.json b/common/changes/@itwin/rpcinterface-full-stack-tests/nam-default-subcategories_2024-06-20-14-56.json new file mode 100644 index 000000000000..f4ac0f33aed4 --- /dev/null +++ b/common/changes/@itwin/rpcinterface-full-stack-tests/nam-default-subcategories_2024-06-20-14-56.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/rpcinterface-full-stack-tests", + "comment": "", + "type": "none" + } + ], + "packageName": "@itwin/rpcinterface-full-stack-tests" +} \ No newline at end of file diff --git a/core/frontend/src/SubCategoriesCache.ts b/core/frontend/src/SubCategoriesCache.ts index d4e04e1e63e0..517e7c814bab 100644 --- a/core/frontend/src/SubCategoriesCache.ts +++ b/core/frontend/src/SubCategoriesCache.ts @@ -66,6 +66,7 @@ export class SubCategoriesCache { }; } + /** Load all subcategories of the iModel into the cache. */ public async loadAllSubCategories(): Promise { const results = await this._imodel.querySubCategories(); if (undefined !== results) From 7f3136c9d722115ae06d205c87b358036a73e85b Mon Sep 17 00:00:00 2001 From: Hoang Nam Le <50554904+hl662@users.noreply.github.com> Date: Thu, 20 Jun 2024 11:03:20 -0400 Subject: [PATCH 05/11] rush extract-api --- common/api/core-backend.api.md | 2 +- common/api/core-common.api.md | 2 +- common/api/core-frontend.api.md | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/common/api/core-backend.api.md b/common/api/core-backend.api.md index 12a2a68cf69e..1f523d05d9df 100644 --- a/common/api/core-backend.api.md +++ b/common/api/core-backend.api.md @@ -3187,7 +3187,7 @@ export abstract class IModelDb extends IModel { queryRowCount(ecsql: string, params?: QueryBinder): Promise; querySchemaVersion(schemaName: string): string | undefined; // @internal - querySubCategories(categoryIds: Iterable): Promise; + querySubCategories(categoryIds?: Iterable): Promise; // @alpha queryTextureData(props: TextureLoadProps): Promise; // @internal (undocumented) diff --git a/common/api/core-common.api.md b/common/api/core-common.api.md index e225dcd610cb..c169a393549b 100644 --- a/common/api/core-common.api.md +++ b/common/api/core-common.api.md @@ -4966,7 +4966,7 @@ export abstract class IModelReadRpcInterface extends RpcInterface { // (undocumented) queryRows(_iModelToken: IModelRpcProps, _request: DbQueryRequest): Promise; // (undocumented) - querySubCategories(_iModelToken: IModelRpcProps, _categoryIds: CompressedId64Set): Promise; + querySubCategories(_iModelToken: IModelRpcProps, _categoryIds?: CompressedId64Set): Promise; // (undocumented) queryTextureData(_iModelToken: IModelRpcProps, _textureLoadProps: TextureLoadProps): Promise; // (undocumented) diff --git a/common/api/core-frontend.api.md b/common/api/core-frontend.api.md index 677c8f8fa07a..360be6d9886b 100644 --- a/common/api/core-frontend.api.md +++ b/common/api/core-frontend.api.md @@ -7047,7 +7047,7 @@ export abstract class IModelConnection extends IModel { // @deprecated queryRowCount(ecsql: string, params?: QueryBinder): Promise; // @internal - querySubCategories(compressedCategoryIds: CompressedId64Set): Promise; + querySubCategories(compressedCategoryIds?: CompressedId64Set): Promise; queryTextureData(textureLoadProps: TextureLoadProps): Promise; // @internal requestSnap(props: SnapRequestProps): Promise; @@ -13414,6 +13414,7 @@ export class SubCategoriesCache { // (undocumented) getSubCategoryInfo(categoryId: Id64String, inputSubCategoryIds: Id64String | Iterable): Promise>; load(categoryIds: Id64Arg): SubCategoriesRequest | undefined; + loadAllSubCategories(): Promise; // (undocumented) onIModelConnectionClose(): void; } From 90441bbd4193821793ce05653cf0d96922daca89 Mon Sep 17 00:00:00 2001 From: Hoang Nam Le <50554904+hl662@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:27:40 -0400 Subject: [PATCH 06/11] add try/catch error, filter parent categories out --- core/backend/src/IModelDb.ts | 15 +++++++++------ core/frontend/src/SubCategoriesCache.ts | 11 ++++++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/core/backend/src/IModelDb.ts b/core/backend/src/IModelDb.ts index 270fb5ea4bd2..dd4f27833275 100644 --- a/core/backend/src/IModelDb.ts +++ b/core/backend/src/IModelDb.ts @@ -708,20 +708,23 @@ export abstract class IModelDb extends IModel { /** * queries the BisCore.SubCategory table for the entries that are children of the passed categoryIds. - * If no iterable is passed, all subcategories are returned. If an empty iterable is passed, no subcategories are returned. + * If no iterable is passed, all subcategories that are part of parent categories containing an element are returned. If an empty iterable is passed, no subcategories are returned. * @param categoryIds categoryIds to query * @returns array of SubCategoryResultRow * @internal */ public async querySubCategories(categoryIds?: Iterable): Promise { const result: SubCategoryResultRow[] = []; - let query: string; if (!categoryIds) { - query = `SELECT ECInstanceId as id, Parent.Id as parentId, Properties as appearance FROM BisCore.SubCategory`; - } else { - const where = [...categoryIds].join(","); - query = `SELECT ECInstanceId as id, Parent.Id as parentId, Properties as appearance FROM BisCore.SubCategory WHERE Parent.Id IN (${where})`; + const parentCategoriesQuery = `SELECT DISTINCT Category.Id AS id FROM BisCore.GeometricElement3d WHERE Category.Id IN (SELECT ECInstanceId FROM BisCore.SpatialCategory)`; + const parentCategories: Id64Array = []; + for await (const row of this.createQueryReader(parentCategoriesQuery)) { + parentCategories.push(row.id); + }; + categoryIds = parentCategories; } + const where = [...categoryIds].join(","); + const query = `SELECT ECInstanceId as id, Parent.Id as parentId, Properties as appearance FROM BisCore.SubCategory WHERE Parent.Id IN (${where})`; try { for await (const row of this.createQueryReader(query, undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames })) { diff --git a/core/frontend/src/SubCategoriesCache.ts b/core/frontend/src/SubCategoriesCache.ts index 517e7c814bab..cb6501fd7831 100644 --- a/core/frontend/src/SubCategoriesCache.ts +++ b/core/frontend/src/SubCategoriesCache.ts @@ -68,9 +68,14 @@ export class SubCategoriesCache { /** Load all subcategories of the iModel into the cache. */ public async loadAllSubCategories(): Promise { - const results = await this._imodel.querySubCategories(); - if (undefined !== results) - this.processResults(results, new Set()); + try { + const results = await this._imodel.querySubCategories(); + if (undefined !== results) + this.processResults(results, new Set()); + } catch { + // In case of a truncated response, gracefully handle the error and exit. + } + } /** Given categoryIds, return which of these are not cached. */ private getMissing(categoryIds: Id64Arg): Id64Set | undefined { From 40fe65eab16a2ccea89eea0188e677cde3169a39 Mon Sep 17 00:00:00 2001 From: Hoang Nam Le <50554904+hl662@users.noreply.github.com> Date: Fri, 28 Jun 2024 12:08:13 -0400 Subject: [PATCH 07/11] add test for fallback behavior --- .../backend/src/rpc-impl/IModelReadRpcImpl.ts | 5 ++- core/frontend/src/SubCategoriesCache.ts | 19 +++++----- .../frontend/standalone/ViewCreator3d.test.ts | 35 ++++++++++++++++--- 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/core/backend/src/rpc-impl/IModelReadRpcImpl.ts b/core/backend/src/rpc-impl/IModelReadRpcImpl.ts index fce5fb0253ba..67594409837c 100644 --- a/core/backend/src/rpc-impl/IModelReadRpcImpl.ts +++ b/core/backend/src/rpc-impl/IModelReadRpcImpl.ts @@ -118,8 +118,11 @@ export class IModelReadRpcImpl extends RpcInterface implements IModelReadRpcInte return viewHydrater.getHydrateResponseProps(options); } - public async querySubCategories(tokenProps: IModelRpcProps, compressedCategoryIds: CompressedId64Set): Promise { + public async querySubCategories(tokenProps: IModelRpcProps, compressedCategoryIds?: CompressedId64Set): Promise { const iModelDb = await getIModelForRpc(tokenProps); + if (!compressedCategoryIds){ + return iModelDb.querySubCategories(); + } const decompressedIds = CompressedId64Set.decompressArray(compressedCategoryIds); return iModelDb.querySubCategories(decompressedIds); } diff --git a/core/frontend/src/SubCategoriesCache.ts b/core/frontend/src/SubCategoriesCache.ts index cb6501fd7831..7daae45e98f1 100644 --- a/core/frontend/src/SubCategoriesCache.ts +++ b/core/frontend/src/SubCategoriesCache.ts @@ -70,9 +70,10 @@ export class SubCategoriesCache { public async loadAllSubCategories(): Promise { try { const results = await this._imodel.querySubCategories(); - if (undefined !== results) - this.processResults(results, new Set()); - } catch { + if (undefined !== results){ + this.processResults(results, new Set(), false); + } + } catch (e) { // In case of a truncated response, gracefully handle the error and exit. } @@ -109,9 +110,10 @@ export class SubCategoriesCache { return new SubCategoryAppearance(props); } - private processResults(result: SubCategoriesCache.Result, missing: Id64Set): void { - for (const row of result) - this.add(row.parentId, row.id, SubCategoriesCache.createSubCategoryAppearance(row.appearance)); + private processResults(result: SubCategoriesCache.Result, missing: Id64Set, override: boolean = true): void { + for (const row of result){ + this.add(row.parentId, row.id, SubCategoriesCache.createSubCategoryAppearance(row.appearance), override); + } // Ensure that any category Ids which returned no results (e.g., non-existent category, invalid Id, etc) are still recorded so they are not repeatedly re-requested for (const id of missing) @@ -122,13 +124,14 @@ export class SubCategoriesCache { /** Exposed strictly for tests. * @internal */ - public add(categoryId: string, subCategoryId: string, appearance: SubCategoryAppearance) { + public add(categoryId: string, subCategoryId: string, appearance: SubCategoryAppearance, override: boolean) { let set = this._byCategoryId.get(categoryId); if (undefined === set) this._byCategoryId.set(categoryId, set = new Set()); set.add(subCategoryId); - this._appearances.set(subCategoryId, appearance); + if (override) + this._appearances.set(subCategoryId, appearance); } public async getCategoryInfo(inputCategoryIds: Id64String | Iterable): Promise> { diff --git a/full-stack-tests/core/src/frontend/standalone/ViewCreator3d.test.ts b/full-stack-tests/core/src/frontend/standalone/ViewCreator3d.test.ts index 2cfaeec3a529..23035d486759 100644 --- a/full-stack-tests/core/src/frontend/standalone/ViewCreator3d.test.ts +++ b/full-stack-tests/core/src/frontend/standalone/ViewCreator3d.test.ts @@ -6,6 +6,7 @@ import { expect } from "chai"; import { SubCategoryAppearance } from "@itwin/core-common"; import { IModelConnection, ScreenViewport, SnapshotConnection, ViewCreator3d} from "@itwin/core-frontend"; import { TestUtility } from "../TestUtility"; +import sinon = require("sinon"); describe("ViewCreator3d", async () => { let imodel: IModelConnection; @@ -41,8 +42,8 @@ describe("ViewCreator3d", async () => { // In a real scenario the visibility would be obtained from the persistent subcategory appearance - our test iModels don't contain any // invisible subcategories to test with. // The iModel contains one spatial category 0x17 with one subcategory 0x18. We're adding a second pretend subcategory 0x20. - imodel.subcategories.add("0x17", "0x18", new SubCategoryAppearance()); - imodel.subcategories.add("0x17", "0x20", new SubCategoryAppearance()); + imodel.subcategories.add("0x17", "0x18", new SubCategoryAppearance(), true); + imodel.subcategories.add("0x17", "0x20", new SubCategoryAppearance(), true); const creator = new ViewCreator3d(imodel); let view = await creator.createDefaultView(); @@ -55,7 +56,7 @@ describe("ViewCreator3d", async () => { expectVisible(true, true); const invisibleAppearance = new SubCategoryAppearance({ invisible: true }); - imodel.subcategories.add("0x17", "0x18", invisibleAppearance); + imodel.subcategories.add("0x17", "0x18", invisibleAppearance, true); view = await creator.createDefaultView(); expectVisible(false, true); @@ -66,12 +67,38 @@ describe("ViewCreator3d", async () => { view = await creator.createDefaultView({ allSubCategoriesVisible: true }); expectVisible(true, true); - imodel.subcategories.add("0x17", "0x20", invisibleAppearance); + imodel.subcategories.add("0x17", "0x20", invisibleAppearance, true); view = await creator.createDefaultView({ allSubCategoriesVisible: true }); expectVisible(true, true); view = await creator.createDefaultView(); expectVisible(false, false); }); + + it("when internal logic of loadAllSubCategories throws, should fall back to loading all subcategories through standard paging", async () => { + imodel.subcategories.add("0x17", "0x18", new SubCategoryAppearance(), true); + imodel.subcategories.add("0x17", "0x20", new SubCategoryAppearance(), true); + + const loadSpy = sinon.spy(imodel.subcategories, "load"); + const queryStub = sinon.stub(imodel, "querySubCategories"); + queryStub.callsFake(function () { + // Immediately restore the stub to ensure only the first call is thrown. + queryStub.restore(); + // Throw an error for the first call + throw new Error("Internal Server Error"); + }); + + const creator = new ViewCreator3d(imodel); + const view = await creator.createDefaultView(); + function expectVisible(subcat18Vis: boolean, subcat20Vis: boolean): void { + expect(view.isSubCategoryVisible("0x18")).to.equal(subcat18Vis); + expect(view.isSubCategoryVisible("0x20")).to.equal(subcat20Vis); + } + + expect(Array.from(view.categorySelector.categories)).to.deep.equal(["0x17"]); + expectVisible(true, true); + expect(loadSpy).to.be.calledOnce; + loadSpy.restore(); + }); }); From 2792b926f7ae4882a23fb104d60b2f18b0e0ad11 Mon Sep 17 00:00:00 2001 From: Hoang Nam Le <50554904+hl662@users.noreply.github.com> Date: Fri, 28 Jun 2024 12:36:16 -0400 Subject: [PATCH 08/11] run rush extract api --- common/api/core-frontend.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/api/core-frontend.api.md b/common/api/core-frontend.api.md index 92b6f946e0da..b2ceb83fd6d3 100644 --- a/common/api/core-frontend.api.md +++ b/common/api/core-frontend.api.md @@ -13406,7 +13406,7 @@ export class StrokesPrimitivePointLists extends Array // @internal export class SubCategoriesCache { constructor(imodel: IModelConnection); - add(categoryId: string, subCategoryId: string, appearance: SubCategoryAppearance): void; + add(categoryId: string, subCategoryId: string, appearance: SubCategoryAppearance, override: boolean): void; // (undocumented) clear(): void; // (undocumented) From 4fc1a660752e5062c483b7280f440d5c380c418c Mon Sep 17 00:00:00 2001 From: Hoang Nam Le <50554904+hl662@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:30:54 -0400 Subject: [PATCH 09/11] Address requested changes by adding new rpc method instead --- common/api/core-backend.api.md | 4 +- common/api/core-common.api.md | 4 +- common/api/core-frontend.api.md | 4 +- ...efault-subcategories_2024-06-20-14-56.json | 2 +- ...efault-subcategories_2024-06-20-14-56.json | 2 +- ...efault-subcategories_2024-06-20-14-56.json | 2 +- core/backend/src/IModelDb.ts | 38 +++++++++++++------ .../backend/src/rpc-impl/IModelReadRpcImpl.ts | 10 +++-- core/common/src/rpc/IModelReadRpcInterface.ts | 4 +- core/frontend/src/IModelConnection.ts | 12 +++++- core/frontend/src/SubCategoriesCache.ts | 5 +-- .../frontend/standalone/ViewCreator3d.test.ts | 9 +---- .../src/frontend/IModelConnection.test.ts | 4 +- 13 files changed, 64 insertions(+), 36 deletions(-) diff --git a/common/api/core-backend.api.md b/common/api/core-backend.api.md index e93f454335c6..e4b43ab59c9b 100644 --- a/common/api/core-backend.api.md +++ b/common/api/core-backend.api.md @@ -3180,6 +3180,8 @@ export abstract class IModelDb extends IModel { prepareStatement(sql: string, logErrors?: boolean): ECSqlStatement; // @deprecated query(ecsql: string, params?: QueryBinder, options?: QueryOptions): AsyncIterableIterator; + // @internal + queryAllUsedSpatialSubCategories(): Promise; queryEntityIds(params: EntityQueryParams): Id64Set; queryFilePropertyBlob(prop: FilePropertyProps): Uint8Array | undefined; queryFilePropertyString(prop: FilePropertyProps): string | undefined; @@ -3188,7 +3190,7 @@ export abstract class IModelDb extends IModel { queryRowCount(ecsql: string, params?: QueryBinder): Promise; querySchemaVersion(schemaName: string): string | undefined; // @internal - querySubCategories(categoryIds?: Iterable): Promise; + querySubCategories(categoryIds: Iterable): Promise; // @alpha queryTextureData(props: TextureLoadProps): Promise; // @internal (undocumented) diff --git a/common/api/core-common.api.md b/common/api/core-common.api.md index b509e9b4ecaf..84314fe99e7d 100644 --- a/common/api/core-common.api.md +++ b/common/api/core-common.api.md @@ -4966,6 +4966,8 @@ export abstract class IModelReadRpcInterface extends RpcInterface { // (undocumented) loadElementProps(_iModelToken: IModelRpcProps, _elementIdentifier: Id64String | GuidString | CodeProps, _options?: ElementLoadOptions): Promise; // (undocumented) + queryAllUsedSpatialSubCategories(_iModelToken: IModelRpcProps): Promise; + // (undocumented) queryBlob(_iModelToken: IModelRpcProps, _request: DbBlobRequest): Promise; // (undocumented) queryElementProps(_iModelToken: IModelRpcProps, _params: EntityQueryParams): Promise; @@ -4980,7 +4982,7 @@ export abstract class IModelReadRpcInterface extends RpcInterface { // (undocumented) queryRows(_iModelToken: IModelRpcProps, _request: DbQueryRequest): Promise; // (undocumented) - querySubCategories(_iModelToken: IModelRpcProps, _categoryIds?: CompressedId64Set): Promise; + querySubCategories(_iModelToken: IModelRpcProps, _categoryIds: CompressedId64Set): Promise; // (undocumented) queryTextureData(_iModelToken: IModelRpcProps, _textureLoadProps: TextureLoadProps): Promise; // (undocumented) diff --git a/common/api/core-frontend.api.md b/common/api/core-frontend.api.md index b2ceb83fd6d3..0b69a25c8e42 100644 --- a/common/api/core-frontend.api.md +++ b/common/api/core-frontend.api.md @@ -7043,11 +7043,13 @@ export abstract class IModelConnection extends IModel { get projectCenterAltitude(): number | undefined; // @deprecated query(ecsql: string, params?: QueryBinder, options?: QueryOptions): AsyncIterableIterator; + // @internal + queryAllUsedSpatialSubCategories(): Promise; queryEntityIds(params: EntityQueryParams): Promise; // @deprecated queryRowCount(ecsql: string, params?: QueryBinder): Promise; // @internal - querySubCategories(compressedCategoryIds?: CompressedId64Set): Promise; + querySubCategories(compressedCategoryIds: CompressedId64Set): Promise; queryTextureData(textureLoadProps: TextureLoadProps): Promise; // @internal requestSnap(props: SnapRequestProps): Promise; diff --git a/common/changes/@itwin/core-backend/nam-default-subcategories_2024-06-20-14-56.json b/common/changes/@itwin/core-backend/nam-default-subcategories_2024-06-20-14-56.json index c2d4dda5bf35..2ce0717eaf11 100644 --- a/common/changes/@itwin/core-backend/nam-default-subcategories_2024-06-20-14-56.json +++ b/common/changes/@itwin/core-backend/nam-default-subcategories_2024-06-20-14-56.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@itwin/core-backend", - "comment": "Update querySubCategories to fetch all subcategories when no categoryId iterable is passed in.", + "comment": "Add RPC method queryAllUsedSpatialSubCategories() to fetch all subcategories of used spatial categories and 3D elements.", "type": "none" } ], diff --git a/common/changes/@itwin/core-common/nam-default-subcategories_2024-06-20-14-56.json b/common/changes/@itwin/core-common/nam-default-subcategories_2024-06-20-14-56.json index 2b9ae46e1553..bed686be7013 100644 --- a/common/changes/@itwin/core-common/nam-default-subcategories_2024-06-20-14-56.json +++ b/common/changes/@itwin/core-common/nam-default-subcategories_2024-06-20-14-56.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@itwin/core-common", - "comment": "Update querySubCategories to fetch all subcategories when no categoryId iterable is passed in.", + "comment": "Add RPC method queryAllUsedSpatialSubCategories() to fetch all subcategories of used spatial categories and 3D elements.", "type": "none" } ], diff --git a/common/changes/@itwin/core-frontend/nam-default-subcategories_2024-06-20-14-56.json b/common/changes/@itwin/core-frontend/nam-default-subcategories_2024-06-20-14-56.json index a919cedf2b20..a681c378fc7f 100644 --- a/common/changes/@itwin/core-frontend/nam-default-subcategories_2024-06-20-14-56.json +++ b/common/changes/@itwin/core-frontend/nam-default-subcategories_2024-06-20-14-56.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@itwin/core-frontend", - "comment": "Load all subcategories when creating a default view.", + "comment": "Load up front all subcategories of used spatial categories and 3D elements when creating a default view.", "type": "none" } ], diff --git a/core/backend/src/IModelDb.ts b/core/backend/src/IModelDb.ts index dd4f27833275..11ef6359ab8a 100644 --- a/core/backend/src/IModelDb.ts +++ b/core/backend/src/IModelDb.ts @@ -706,26 +706,42 @@ export abstract class IModelDb extends IModel { return stmt; } + /** + * queries the BisCore.SubCategory table for entries that are children of used spatial categories and 3D elements. + * @returns array of SubCategoryResultRow + * @internal + */ + public async queryAllUsedSpatialSubCategories(): Promise { + const result: SubCategoryResultRow[] = []; + const parentCategoriesQuery = `SELECT DISTINCT Category.Id AS id FROM BisCore.GeometricElement3d WHERE Category.Id IN (SELECT ECInstanceId FROM BisCore.SpatialCategory)`; + const parentCategories: Id64Array = []; + for await (const row of this.createQueryReader(parentCategoriesQuery)) { + parentCategories.push(row.id); + }; + const where = [...parentCategories].join(","); + const query = `SELECT ECInstanceId as id, Parent.Id as parentId, Properties as appearance FROM BisCore.SubCategory WHERE Parent.Id IN (${where})`; + + try { + for await (const row of this.createQueryReader(query, undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames })) { + result.push(row.toRow() as SubCategoryResultRow); + } + } catch { + // We can ignore the error here, and just return whatever we were able to query. + } + return result; + } + /** * queries the BisCore.SubCategory table for the entries that are children of the passed categoryIds. - * If no iterable is passed, all subcategories that are part of parent categories containing an element are returned. If an empty iterable is passed, no subcategories are returned. * @param categoryIds categoryIds to query * @returns array of SubCategoryResultRow * @internal */ - public async querySubCategories(categoryIds?: Iterable): Promise { + public async querySubCategories(categoryIds: Iterable): Promise { const result: SubCategoryResultRow[] = []; - if (!categoryIds) { - const parentCategoriesQuery = `SELECT DISTINCT Category.Id AS id FROM BisCore.GeometricElement3d WHERE Category.Id IN (SELECT ECInstanceId FROM BisCore.SpatialCategory)`; - const parentCategories: Id64Array = []; - for await (const row of this.createQueryReader(parentCategoriesQuery)) { - parentCategories.push(row.id); - }; - categoryIds = parentCategories; - } + const where = [...categoryIds].join(","); const query = `SELECT ECInstanceId as id, Parent.Id as parentId, Properties as appearance FROM BisCore.SubCategory WHERE Parent.Id IN (${where})`; - try { for await (const row of this.createQueryReader(query, undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames })) { result.push(row.toRow() as SubCategoryResultRow); diff --git a/core/backend/src/rpc-impl/IModelReadRpcImpl.ts b/core/backend/src/rpc-impl/IModelReadRpcImpl.ts index 67594409837c..0d2ff37322f3 100644 --- a/core/backend/src/rpc-impl/IModelReadRpcImpl.ts +++ b/core/backend/src/rpc-impl/IModelReadRpcImpl.ts @@ -118,11 +118,13 @@ export class IModelReadRpcImpl extends RpcInterface implements IModelReadRpcInte return viewHydrater.getHydrateResponseProps(options); } - public async querySubCategories(tokenProps: IModelRpcProps, compressedCategoryIds?: CompressedId64Set): Promise { + public async queryAllUsedSpatialSubCategories(tokenProps: IModelRpcProps): Promise { + const iModelDb = await getIModelForRpc(tokenProps); + return iModelDb.queryAllUsedSpatialSubCategories(); + } + + public async querySubCategories(tokenProps: IModelRpcProps, compressedCategoryIds: CompressedId64Set): Promise { const iModelDb = await getIModelForRpc(tokenProps); - if (!compressedCategoryIds){ - return iModelDb.querySubCategories(); - } const decompressedIds = CompressedId64Set.decompressArray(compressedCategoryIds); return iModelDb.querySubCategories(decompressedIds); } diff --git a/core/common/src/rpc/IModelReadRpcInterface.ts b/core/common/src/rpc/IModelReadRpcInterface.ts index 23f963722c76..4e4259bff574 100644 --- a/core/common/src/rpc/IModelReadRpcInterface.ts +++ b/core/common/src/rpc/IModelReadRpcInterface.ts @@ -94,7 +94,9 @@ export abstract class IModelReadRpcInterface extends RpcInterface { // eslint-di public async getConnectionProps(_iModelToken: IModelRpcOpenProps): Promise { return this.forward(arguments); } public async queryRows(_iModelToken: IModelRpcProps, _request: DbQueryRequest): Promise { return this.forward(arguments); } @RpcOperation.allowResponseCaching(RpcResponseCacheControl.Immutable) // eslint-disable-line deprecation/deprecation - public async querySubCategories(_iModelToken: IModelRpcProps, _categoryIds?: CompressedId64Set): Promise { return this.forward(arguments); } + public async querySubCategories(_iModelToken: IModelRpcProps, _categoryIds: CompressedId64Set): Promise { return this.forward(arguments); } + @RpcOperation.allowResponseCaching(RpcResponseCacheControl.Immutable) // eslint-disable-line deprecation/deprecation + public async queryAllUsedSpatialSubCategories(_iModelToken: IModelRpcProps): Promise { return this.forward(arguments); } public async queryBlob(_iModelToken: IModelRpcProps, _request: DbBlobRequest): Promise { return this.forward(arguments); } @RpcOperation.allowResponseCaching(RpcResponseCacheControl.Immutable) // eslint-disable-line deprecation/deprecation public async getModelProps(_iModelToken: IModelRpcProps, _modelIds: Id64String[]): Promise { return this.forward(arguments); } diff --git a/core/frontend/src/IModelConnection.ts b/core/frontend/src/IModelConnection.ts index bcb48149f3f4..94c001de219c 100644 --- a/core/frontend/src/IModelConnection.ts +++ b/core/frontend/src/IModelConnection.ts @@ -277,15 +277,23 @@ export abstract class IModelConnection extends IModel { /** * queries the BisCore.SubCategory table for the entries that are children of the passed categoryIds - * If no iterable is passed, all subcategories are returned. If an empty iterable is passed, no subcategories are returned. * @param compressedCategoryIds compressed category Ids * @returns array of SubCategoryResultRow * @internal */ - public async querySubCategories(compressedCategoryIds?: CompressedId64Set): Promise { + public async querySubCategories(compressedCategoryIds: CompressedId64Set): Promise { return IModelReadRpcInterface.getClientForRouting(this.routingContext.token).querySubCategories(this.getRpcProps(), compressedCategoryIds); } + /** + * queries the BisCore.SubCategory table for entries that are children of used spatial categories and 3D elements. + * @returns array of SubCategoryResultRow + * @internal + */ + public async queryAllUsedSpatialSubCategories(): Promise { + return IModelReadRpcInterface.getClientForRouting(this.routingContext.token).queryAllUsedSpatialSubCategories(this.getRpcProps()); + } + /** Execute a query and stream its results * The result of the query is async iterator over the rows. The iterator will get next page automatically once rows in current page has been read. * [ECSQL row]($docs/learning/ECSQLRowFormat). diff --git a/core/frontend/src/SubCategoriesCache.ts b/core/frontend/src/SubCategoriesCache.ts index 7daae45e98f1..71675a1b19f8 100644 --- a/core/frontend/src/SubCategoriesCache.ts +++ b/core/frontend/src/SubCategoriesCache.ts @@ -43,7 +43,6 @@ export class SubCategoriesCache { /** Request that the subcategory information for all of the specified categories is loaded. * If all such information has already been loaded, returns undefined. - * If no categories are specified, dispatches an asynchronous request to load all subcategories and returns a cancellable request object * Otherwise, dispatches an asynchronous request to load those categories which are not already loaded and returns a cancellable request object * containing the corresponding promise and the set of categories still to be loaded. */ @@ -66,10 +65,10 @@ export class SubCategoriesCache { }; } - /** Load all subcategories of the iModel into the cache. */ + /** Load all subcategories that come from used spatial categories of the iModel into the cache. */ public async loadAllSubCategories(): Promise { try { - const results = await this._imodel.querySubCategories(); + const results = await this._imodel.queryAllUsedSpatialSubCategories(); if (undefined !== results){ this.processResults(results, new Set(), false); } diff --git a/full-stack-tests/core/src/frontend/standalone/ViewCreator3d.test.ts b/full-stack-tests/core/src/frontend/standalone/ViewCreator3d.test.ts index 23035d486759..6e6f9a349220 100644 --- a/full-stack-tests/core/src/frontend/standalone/ViewCreator3d.test.ts +++ b/full-stack-tests/core/src/frontend/standalone/ViewCreator3d.test.ts @@ -80,13 +80,7 @@ describe("ViewCreator3d", async () => { imodel.subcategories.add("0x17", "0x20", new SubCategoryAppearance(), true); const loadSpy = sinon.spy(imodel.subcategories, "load"); - const queryStub = sinon.stub(imodel, "querySubCategories"); - queryStub.callsFake(function () { - // Immediately restore the stub to ensure only the first call is thrown. - queryStub.restore(); - // Throw an error for the first call - throw new Error("Internal Server Error"); - }); + const queryStub = sinon.stub(imodel, "queryAllUsedSpatialSubCategories").rejects(new Error("Internal Server Error")); const creator = new ViewCreator3d(imodel); const view = await creator.createDefaultView(); @@ -99,6 +93,7 @@ describe("ViewCreator3d", async () => { expectVisible(true, true); expect(loadSpy).to.be.calledOnce; loadSpy.restore(); + queryStub.restore(); }); }); diff --git a/full-stack-tests/rpc-interface/src/frontend/IModelConnection.test.ts b/full-stack-tests/rpc-interface/src/frontend/IModelConnection.test.ts index 3f8906bde717..75038b69891f 100644 --- a/full-stack-tests/rpc-interface/src/frontend/IModelConnection.test.ts +++ b/full-stack-tests/rpc-interface/src/frontend/IModelConnection.test.ts @@ -347,8 +347,8 @@ describe("IModelReadRpcInterface Methods from an IModelConnection", () => { expect(candidate2Result).to.deep.eq({ ...expectedCandidate2Result, candidate: candidates[1] }); }); - it("querySubCategories should get all subcategories when no argument is passed", async () => { - const result = await iModel.querySubCategories(); + it("queryAllUsedSpatialSubCategories should find subcategories coming from spatial categories of 3d Elements", async () => { + const result = await iModel.queryAllUsedSpatialSubCategories(); expect(result).to.not.be.null; expect(result.length).to.not.be.equal(0); }); From d8f249a31d6d2325aa99e8c135d34aec6ac7c43f Mon Sep 17 00:00:00 2001 From: Nam Le <50554904+hl662@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:39:49 -0400 Subject: [PATCH 10/11] Update function name to be more accurate Co-authored-by: Paul Connelly <22944042+pmconne@users.noreply.github.com> --- core/frontend/src/SubCategoriesCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/frontend/src/SubCategoriesCache.ts b/core/frontend/src/SubCategoriesCache.ts index 71675a1b19f8..ee1978848979 100644 --- a/core/frontend/src/SubCategoriesCache.ts +++ b/core/frontend/src/SubCategoriesCache.ts @@ -66,7 +66,7 @@ export class SubCategoriesCache { } /** Load all subcategories that come from used spatial categories of the iModel into the cache. */ - public async loadAllSubCategories(): Promise { + public async loadAllUsedSpatialSubCategories(): Promise { try { const results = await this._imodel.queryAllUsedSpatialSubCategories(); if (undefined !== results){ From 8ded715ed2a5e53cf8742bccdc9d5ec441448c6a Mon Sep 17 00:00:00 2001 From: Hoang Nam Le <50554904+hl662@users.noreply.github.com> Date: Mon, 1 Jul 2024 16:06:53 -0400 Subject: [PATCH 11/11] run rush extract api --- common/api/core-frontend.api.md | 2 +- core/frontend/src/ViewCreator3d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/api/core-frontend.api.md b/common/api/core-frontend.api.md index 0b69a25c8e42..524357d7d14c 100644 --- a/common/api/core-frontend.api.md +++ b/common/api/core-frontend.api.md @@ -13418,7 +13418,7 @@ export class SubCategoriesCache { // (undocumented) getSubCategoryInfo(categoryId: Id64String, inputSubCategoryIds: Id64String | Iterable): Promise>; load(categoryIds: Id64Arg): SubCategoriesRequest | undefined; - loadAllSubCategories(): Promise; + loadAllUsedSpatialSubCategories(): Promise; // (undocumented) onIModelConnectionClose(): void; } diff --git a/core/frontend/src/ViewCreator3d.ts b/core/frontend/src/ViewCreator3d.ts index fad4a86e0830..fbcd44eeaa31 100644 --- a/core/frontend/src/ViewCreator3d.ts +++ b/core/frontend/src/ViewCreator3d.ts @@ -90,7 +90,7 @@ export class ViewCreator3d { const viewState = SpatialViewState.createFromProps(props, this._imodel); try { - await viewState.iModel.subcategories.loadAllSubCategories(); + await viewState.iModel.subcategories.loadAllUsedSpatialSubCategories(); await viewState.load(); } catch { }