From 2f8a6e557d13f4bc1d5bade7ac96535bd415b2d7 Mon Sep 17 00:00:00 2001 From: rveenstra Date: Wed, 30 Jul 2025 11:30:44 +1000 Subject: [PATCH 1/8] Add support for optional changesets when loading iModels --- packages/engine/Source/Core/ITwinPlatform.js | 8 +++++++- packages/engine/Source/Scene/ITwinData.js | 21 ++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/engine/Source/Core/ITwinPlatform.js b/packages/engine/Source/Core/ITwinPlatform.js index 269cb192c1fb..3fa28c6083d3 100644 --- a/packages/engine/Source/Core/ITwinPlatform.js +++ b/packages/engine/Source/Core/ITwinPlatform.js @@ -157,9 +157,12 @@ ITwinPlatform.apiEndpoint = new Resource({ * * @throws {RuntimeError} If the iTwin API request is not successful */ -ITwinPlatform.getExports = async function (iModelId) { +ITwinPlatform.getExports = async function (iModelId, changesetId) { //>>includeStart('debug', pragmas.debug); Check.typeOf.string("iModelId", iModelId); + if (defined(changesetId)) { + Check.typeOf.string("changesetId", changesetId); + } if ( !defined(ITwinPlatform.defaultAccessToken) && !defined(ITwinPlatform.defaultShareKey) @@ -191,6 +194,9 @@ ITwinPlatform.getExports = async function (iModelId) { if (typeof CESIUM_VERSION !== "undefined") { resource.appendQueryParameters({ clientVersion: CESIUM_VERSION }); } + if (defined(changesetId)) { + resource.appendQueryParameters({ changesetId: changesetId }); + } try { const response = await resource.fetchJson(); diff --git a/packages/engine/Source/Scene/ITwinData.js b/packages/engine/Source/Scene/ITwinData.js index ff2681ad9e99..f6c1e64c9c73 100644 --- a/packages/engine/Source/Scene/ITwinData.js +++ b/packages/engine/Source/Scene/ITwinData.js @@ -34,17 +34,30 @@ const ITwinData = {}; * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * * @param {string} iModelId The id of the iModel to load + * @param {string} [changesetId] The id of the changeset to load, if not provided the latest changesets will be used * @param {Cesium3DTileset.ConstructorOptions} [options] Object containing options to pass to the internally created {@link Cesium3DTileset}. * @returns {Promise} A promise that will resolve to the created 3D tileset or undefined if there is no completed export for the given iModel id * + * @throws {RuntimeError} If no exports for the given iModel are found * @throws {RuntimeError} If all exports for the given iModel are Invalid * @throws {RuntimeError} If the iTwin API request is not successful */ -ITwinData.createTilesetFromIModelId = async function (iModelId, options) { - const { exports } = await ITwinPlatform.getExports(iModelId); +ITwinData.createTilesetFromIModelId = async function ( + iModelId, + changesetId, + options, +) { + const { exports } = await ITwinPlatform.getExports(iModelId, changesetId); - if ( - exports.length > 0 && + if (exports.length === 0) { + if (defined(changesetId)) { + throw new RuntimeError( + `No exports found for this iModel: ${iModelId} and changeset: ${changesetId}`, + ); + } else { + throw new RuntimeError(`No exports found for this iModel: ${iModelId}`); + } + } else if ( exports.every((exportObj) => { return exportObj.status === ITwinPlatform.ExportStatus.Invalid; }) From 993f73a4922b44957df6bf5589d23bce8e31d265 Mon Sep 17 00:00:00 2001 From: rveenstra Date: Wed, 30 Jul 2025 12:04:57 +1000 Subject: [PATCH 2/8] order of parameters, tests --- packages/engine/Source/Scene/ITwinData.js | 15 ++++----------- packages/engine/Specs/Core/ITwinPlatformSpec.js | 11 +++++++++++ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/engine/Source/Scene/ITwinData.js b/packages/engine/Source/Scene/ITwinData.js index f6c1e64c9c73..f01c2c66baa7 100644 --- a/packages/engine/Source/Scene/ITwinData.js +++ b/packages/engine/Source/Scene/ITwinData.js @@ -34,8 +34,8 @@ const ITwinData = {}; * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * * @param {string} iModelId The id of the iModel to load - * @param {string} [changesetId] The id of the changeset to load, if not provided the latest changesets will be used * @param {Cesium3DTileset.ConstructorOptions} [options] Object containing options to pass to the internally created {@link Cesium3DTileset}. + * @param {string} [changesetId] The id of the changeset to load, if not provided the latest changesets will be used * @returns {Promise} A promise that will resolve to the created 3D tileset or undefined if there is no completed export for the given iModel id * * @throws {RuntimeError} If no exports for the given iModel are found @@ -44,20 +44,13 @@ const ITwinData = {}; */ ITwinData.createTilesetFromIModelId = async function ( iModelId, - changesetId, options, + changesetId, ) { const { exports } = await ITwinPlatform.getExports(iModelId, changesetId); - if (exports.length === 0) { - if (defined(changesetId)) { - throw new RuntimeError( - `No exports found for this iModel: ${iModelId} and changeset: ${changesetId}`, - ); - } else { - throw new RuntimeError(`No exports found for this iModel: ${iModelId}`); - } - } else if ( + if ( + exports.length > 0 && exports.every((exportObj) => { return exportObj.status === ITwinPlatform.ExportStatus.Invalid; }) diff --git a/packages/engine/Specs/Core/ITwinPlatformSpec.js b/packages/engine/Specs/Core/ITwinPlatformSpec.js index 6e41e9e9b01c..281be834588a 100644 --- a/packages/engine/Specs/Core/ITwinPlatformSpec.js +++ b/packages/engine/Specs/Core/ITwinPlatformSpec.js @@ -156,6 +156,17 @@ describe("ITwinPlatform", () => { expect(resource).toBeDefined(); expect(resource.url).toContain("imodel-id-1"); }); + + it("uses the changeset in the API request", async () => { + let resource; + requestSpy.and.callFake(function () { + resource = this; + return JSON.stringify({ exports: [] }); + }); + await ITwinPlatform.getExports("imodel-id-1", "changeset-id-1"); + expect(resource).toBeDefined(); + expect(resource.url).toContain("changeset-id-1"); + }); }); describe("getRealityDataMetadata", () => { From 70099616c6cf4121cf07a09f62c541572028664c Mon Sep 17 00:00:00 2001 From: rveenstra Date: Thu, 31 Jul 2025 10:08:33 +1000 Subject: [PATCH 3/8] update docs --- packages/engine/Source/Core/ITwinPlatform.js | 1 + packages/engine/Source/Scene/ITwinData.js | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/engine/Source/Core/ITwinPlatform.js b/packages/engine/Source/Core/ITwinPlatform.js index 3fa28c6083d3..18f7aacf2bd3 100644 --- a/packages/engine/Source/Core/ITwinPlatform.js +++ b/packages/engine/Source/Core/ITwinPlatform.js @@ -153,6 +153,7 @@ ITwinPlatform.apiEndpoint = new Resource({ * @private * * @param {string} iModelId iModel id + * @param {string} [changesetId] The id of the changeset to filter results by. If not provided, exports from the latest available changesets will be returned. * @returns {Promise} * * @throws {RuntimeError} If the iTwin API request is not successful diff --git a/packages/engine/Source/Scene/ITwinData.js b/packages/engine/Source/Scene/ITwinData.js index f01c2c66baa7..03c012a55af3 100644 --- a/packages/engine/Source/Scene/ITwinData.js +++ b/packages/engine/Source/Scene/ITwinData.js @@ -38,7 +38,6 @@ const ITwinData = {}; * @param {string} [changesetId] The id of the changeset to load, if not provided the latest changesets will be used * @returns {Promise} A promise that will resolve to the created 3D tileset or undefined if there is no completed export for the given iModel id * - * @throws {RuntimeError} If no exports for the given iModel are found * @throws {RuntimeError} If all exports for the given iModel are Invalid * @throws {RuntimeError} If the iTwin API request is not successful */ From e8cad4332103b1f761521326ad8be2c385d2a754 Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:20:49 -0400 Subject: [PATCH 4/8] update ITwinData to use options arguments --- .../gallery/Drape Imagery on 3D Tiles.html | 10 +- .../gallery/iModel Mesh Export Service.html | 22 +- .../gallery/iTwin Feature Service.html | 42 ++-- CHANGES.md | 5 + packages/engine/Source/Scene/ITwinData.js | 132 ++++++++--- packages/engine/Specs/Scene/ITwinDataSpec.js | 217 ++++++++++++------ 6 files changed, 286 insertions(+), 142 deletions(-) diff --git a/Apps/Sandcastle/gallery/Drape Imagery on 3D Tiles.html b/Apps/Sandcastle/gallery/Drape Imagery on 3D Tiles.html index d1ed0eaf9eb1..12383fc49bab 100644 --- a/Apps/Sandcastle/gallery/Drape Imagery on 3D Tiles.html +++ b/Apps/Sandcastle/gallery/Drape Imagery on 3D Tiles.html @@ -40,12 +40,10 @@ }); viewer.scene.skyAtmosphere.show = true; - const iTwinId = "535a24a3-9b29-4e23-bb5d-9cedb524c743"; - const realityMeshId = "85897090-3bcc-470b-bec7-20bb639cc1b9"; - const tileset = await Cesium.ITwinData.createTilesetForRealityDataId( - iTwinId, - realityMeshId, - ); + const tileset = await Cesium.ITwinData.createTilesetForRealityDataId({ + iTwinId: "535a24a3-9b29-4e23-bb5d-9cedb524c743", + realityDataId: "85897090-3bcc-470b-bec7-20bb639cc1b9", + }); viewer.scene.primitives.add(tileset); tileset.maximumScreenSpaceError = 2; diff --git a/Apps/Sandcastle/gallery/iModel Mesh Export Service.html b/Apps/Sandcastle/gallery/iModel Mesh Export Service.html index 5675c660ee22..b00e73cec4a1 100644 --- a/Apps/Sandcastle/gallery/iModel Mesh Export Service.html +++ b/Apps/Sandcastle/gallery/iModel Mesh Export Service.html @@ -110,12 +110,12 @@ } // Create tilesets using the iModel ids - const surroundingArea = await Cesium.ITwinData.createTilesetFromIModelId( - "f856f57d-3d28-4265-9c4f-5e60c0662c15", - ); - const station = await Cesium.ITwinData.createTilesetFromIModelId( - "669dde67-eb69-4e0b-bcf2-f722eee94746", - ); + const surroundingArea = await Cesium.ITwinData.createTilesetFromIModelId({ + iModelId: "f856f57d-3d28-4265-9c4f-5e60c0662c15", + }); + const station = await Cesium.ITwinData.createTilesetFromIModelId({ + iModelId: "669dde67-eb69-4e0b-bcf2-f722eee94746", + }); // Change how highlighting with the feature selection changes the color surroundingArea.colorBlendMode = Cesium.Cesium3DTileColorBlendMode.REPLACE; station.colorBlendMode = Cesium.Cesium3DTileColorBlendMode.REPLACE; @@ -124,12 +124,10 @@ scene.primitives.add(station); // Create tileset of the reality data mesh - const iTwinId = "535a24a3-9b29-4e23-bb5d-9cedb524c743"; - const realityMeshId = "85897090-3bcc-470b-bec7-20bb639cc1b9"; - const realityMesh = await Cesium.ITwinData.createTilesetForRealityDataId( - iTwinId, - realityMeshId, - ); + const realityMesh = await Cesium.ITwinData.createTilesetForRealityDataId({ + iTwinId: "535a24a3-9b29-4e23-bb5d-9cedb524c743", + realityDataId: "85897090-3bcc-470b-bec7-20bb639cc1b9", + }); scene.primitives.add(realityMesh); Sandcastle.addToolbarButton( diff --git a/Apps/Sandcastle/gallery/iTwin Feature Service.html b/Apps/Sandcastle/gallery/iTwin Feature Service.html index bac66a0b1279..93441dfc9975 100644 --- a/Apps/Sandcastle/gallery/iTwin Feature Service.html +++ b/Apps/Sandcastle/gallery/iTwin Feature Service.html @@ -66,18 +66,18 @@ viewer.scene.camera.flyTo(birdsEyeView); // Load feature service geojson files - const points = await Cesium.ITwinData.loadGeospatialFeatures( - iTwinId, - "2380dc1b-1dac-4709-aa5c-f6cb38c4e9f5", - ); - const lines = await Cesium.ITwinData.loadGeospatialFeatures( - iTwinId, - "613d2310-4d01-43b7-bc92-873a2ca4a4a0", - ); - const areas = await Cesium.ITwinData.loadGeospatialFeatures( - iTwinId, - "93e7ef51-5210-49f2-92a3-c7f6685e102f", - ); + const points = await Cesium.ITwinData.loadGeospatialFeatures({ + iTwinId: iTwinId, + collectionId: "2380dc1b-1dac-4709-aa5c-f6cb38c4e9f5", + }); + const lines = await Cesium.ITwinData.loadGeospatialFeatures({ + iTwinId: iTwinId, + collectionId: "613d2310-4d01-43b7-bc92-873a2ca4a4a0", + }); + const areas = await Cesium.ITwinData.loadGeospatialFeatures({ + iTwinId: iTwinId, + collectionId: "93e7ef51-5210-49f2-92a3-c7f6685e102f", + }); // Add some styling to the lines and points to differentiate types const pinBuilder = new Cesium.PinBuilder(); @@ -118,17 +118,15 @@ viewer.dataSources.add(areas); // Create tileset of the reality data mesh and pointcloud - const realityMeshId = "62e4432d-621d-489a-87ff-1fc56a2b5369"; - const realityMesh = await Cesium.ITwinData.createTilesetForRealityDataId( - iTwinId, - realityMeshId, - ); + const realityMesh = await Cesium.ITwinData.createTilesetForRealityDataId({ + iTwinId: iTwinId, + realityDataId: "62e4432d-621d-489a-87ff-1fc56a2b5369", + }); viewer.scene.primitives.add(realityMesh); - const pointcloudId = "ebf2ee74-f0de-4cd6-a311-19a169c55fdc"; - const pointcloud = await Cesium.ITwinData.createTilesetForRealityDataId( - iTwinId, - pointcloudId, - ); + const pointcloud = await Cesium.ITwinData.createTilesetForRealityDataId({ + iTwinId: iTwinId, + realityDataId: "ebf2ee74-f0de-4cd6-a311-19a169c55fdc", + }); // increase the size of the pointcloud points and turn on attenuation to // make them more visible in the viewer pointcloud.maximumScreenSpaceError = 1; diff --git a/CHANGES.md b/CHANGES.md index 5746fc0283a8..00729b5c2542 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,10 @@ ### @cesium/engine +#### Breaking Changes :mega: + +- Updated all of the `ITwinData.*` functions to accept an `options` parameter instead of individual arguments to avoid confusion with multiple optional arguments. There is a fallback to the old signature that will be removed in 1.133 [#12778](https://github.com/CesiumGS/cesium/issues/12778) + #### Fixes :wrench: - Fixes incorrect polygon culling in 2D scene mode. [#1552](https://github.com/CesiumGS/cesium/issues/1552) @@ -20,6 +24,7 @@ #### Additions :tada: - Expand the CustomShader Sample to support real-time modification of CustomShader. [#12702](https://github.com/CesiumGS/cesium/pull/12702) +- Added the ability to load a specific changeset for iTwin Mesh Exports using `ITwinData.createTilesetFromIModelId` [#12778](https://github.com/CesiumGS/cesium/issues/12778) ## 1.131 - 2025-07-01 diff --git a/packages/engine/Source/Scene/ITwinData.js b/packages/engine/Source/Scene/ITwinData.js index 03c012a55af3..721c0c74d7b7 100644 --- a/packages/engine/Source/Scene/ITwinData.js +++ b/packages/engine/Source/Scene/ITwinData.js @@ -7,6 +7,7 @@ import Check from "../Core/Check.js"; import KmlDataSource from "../DataSources/KmlDataSource.js"; import GeoJsonDataSource from "../DataSources/GeoJsonDataSource.js"; import DeveloperError from "../Core/DeveloperError.js"; +import deprecationWarning from "../Core/deprecationWarning.js"; /** * Methods for loading iTwin platform data into CesiumJS @@ -26,26 +27,36 @@ const ITwinData = {}; * If all exports are Invalid this will throw an error. * * @example - * const tileset = await Cesium.ITwinData.createTilesetFromIModelId(iModelId); + * const tileset = await Cesium.ITwinData.createTilesetFromIModelId({ iModelId }); * if (Cesium.defined(tileset)) { * viewer.scene.primitives.add(tileset); * } * * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * - * @param {string} iModelId The id of the iModel to load - * @param {Cesium3DTileset.ConstructorOptions} [options] Object containing options to pass to the internally created {@link Cesium3DTileset}. - * @param {string} [changesetId] The id of the changeset to load, if not provided the latest changesets will be used + * @param {Object} options + * @param {string} options.iModelId The id of the iModel to load + * @param {Cesium3DTileset.ConstructorOptions} [options.tilesetOptions] Object containing options to pass to the internally created {@link Cesium3DTileset}. + * @param {string} [options.changesetId] The id of the changeset to load, if not provided the latest changesets will be used * @returns {Promise} A promise that will resolve to the created 3D tileset or undefined if there is no completed export for the given iModel id * * @throws {RuntimeError} If all exports for the given iModel are Invalid * @throws {RuntimeError} If the iTwin API request is not successful */ -ITwinData.createTilesetFromIModelId = async function ( - iModelId, - options, - changesetId, -) { +ITwinData.createTilesetFromIModelId = async function (options) { + let internalOptions = options; + + if (typeof options === "string") { + // the old signature was (iModelId: string, options?: Cesium3DTileset.ConstructorOptions) + // grab the later arguments directly instead of in the params only for this special case + internalOptions = { iModelId: options, tilesetOptions: arguments[1] }; + deprecationWarning( + "ITwinData.createTilesetFromIModelId", + "The arguments signature for ITwinData functions has changed in 1.132 in favor of a single options object. Please update your code. This fallback will be removed in 1.133", + ); + } + + const { iModelId, changesetId, tilesetOptions } = internalOptions; const { exports } = await ITwinPlatform.getExports(iModelId, changesetId); if ( @@ -77,7 +88,7 @@ ITwinData.createTilesetFromIModelId = async function ( url: tilesetUrl, }); - return Cesium3DTileset.fromUrl(resource, options); + return Cesium3DTileset.fromUrl(resource, tilesetOptions); }; /** @@ -89,20 +100,35 @@ ITwinData.createTilesetFromIModelId = async function ( * * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * - * @param {string} iTwinId The id of the iTwin to load data from - * @param {string} realityDataId The id of the reality data to load - * @param {ITwinPlatform.RealityDataType} [type] The type of this reality data - * @param {string} [rootDocument] The path of the root document for this reality data + * @param {Object} options + * @param {string} options.iTwinId The id of the iTwin to load data from + * @param {string} options.realityDataId The id of the reality data to load + * @param {ITwinPlatform.RealityDataType} [options.type] The type of this reality data + * @param {string} [options.rootDocument] The path of the root document for this reality data * @returns {Promise} * * @throws {RuntimeError} if the type of reality data is not supported by this function */ -ITwinData.createTilesetForRealityDataId = async function ( - iTwinId, - realityDataId, - type, - rootDocument, -) { +ITwinData.createTilesetForRealityDataId = async function (options) { + let internalOptions = options; + + if (typeof options === "string") { + // the old signature was (iTwinId: string, realityDataId: string, type: RealityDataType, rootDocument: string) + // grab the later arguments directly instead of in the params only for this special case + internalOptions = { + iTwinId: options, + realityDataId: arguments[1], + type: arguments[2], + rootDocument: arguments[3], + }; + deprecationWarning( + "ITwinData.createTilesetFromIModelId", + "The arguments signature for ITwinData functions has changed in 1.132 in favor of a single options object. Please update your code. This fallback will be removed in 1.133", + ); + } + const { iTwinId, realityDataId } = internalOptions; + let { type, rootDocument } = internalOptions; + //>>includeStart('debug', pragmas.debug); Check.typeOf.string("iTwinId", iTwinId); Check.typeOf.string("realityDataId", realityDataId); @@ -152,20 +178,35 @@ ITwinData.createTilesetForRealityDataId = async function ( * If the type or rootDocument are not provided this function * will first request the full metadata for the specified reality data to fill these values. * - * @param {string} iTwinId The id of the iTwin to load data from - * @param {string} realityDataId The id of the reality data to load - * @param {ITwinPlatform.RealityDataType} [type] The type of this reality data - * @param {string} [rootDocument] The path of the root document for this reality data + * @param {Object} options + * @param {string} options.iTwinId The id of the iTwin to load data from + * @param {string} options.realityDataId The id of the reality data to load + * @param {ITwinPlatform.RealityDataType} [options.type] The type of this reality data + * @param {string} [options.rootDocument] The path of the root document for this reality data * @returns {Promise} * * @throws {RuntimeError} if the type of reality data is not supported by this function */ -ITwinData.createDataSourceForRealityDataId = async function ( - iTwinId, - realityDataId, - type, - rootDocument, -) { +ITwinData.createDataSourceForRealityDataId = async function (options) { + let internalOptions = options; + + if (typeof options === "string") { + // the old signature was (iTwinId: string, realityDataId: string, type: RealityDataType, rootDocument: string) + // grab the later arguments directly instead of in the params only for this special case + internalOptions = { + iTwinId: options, + realityDataId: arguments[1], + type: arguments[2], + rootDocument: arguments[3], + }; + deprecationWarning( + "ITwinData.createTilesetFromIModelId", + "The arguments signature for ITwinData functions has changed in 1.132 in favor of a single options object. Please update your code. This fallback will be removed in 1.133", + ); + } + const { iTwinId, realityDataId } = internalOptions; + let { type, rootDocument } = internalOptions; + //>>includeStart('debug', pragmas.debug); Check.typeOf.string("iTwinId", iTwinId); Check.typeOf.string("realityDataId", realityDataId); @@ -214,16 +255,31 @@ ITwinData.createDataSourceForRealityDataId = async function ( /** * Load data from the Geospatial Features API as GeoJSON. * - * @param {string} iTwinId The id of the iTwin to load data from - * @param {string} collectionId The id of the data collection to load - * @param {number} [limit=10000] number of items per page, must be between 1 and 10,000 inclusive + * @param {Object} options + * @param {string} options.iTwinId The id of the iTwin to load data from + * @param {string} options.collectionId The id of the data collection to load + * @param {number} [options.limit=10000] number of items per page, must be between 1 and 10,000 inclusive * @returns {Promise} */ -ITwinData.loadGeospatialFeatures = async function ( - iTwinId, - collectionId, - limit, -) { +ITwinData.loadGeospatialFeatures = async function (options) { + let internalOptions = options; + + if (typeof options === "string") { + // the old signature was (iTwinId: string, collectionId: string, limit: number) + // grab the later arguments directly instead of in the params only for this special case + internalOptions = { + iTwinId: options, + collectionId: arguments[1], + limit: arguments[2], + }; + deprecationWarning( + "ITwinData.createTilesetFromIModelId", + "The arguments signature for ITwinData functions has changed in 1.132 in favor of a single options object. Please update your code. This fallback will be removed in 1.133", + ); + } + + const { iTwinId, collectionId, limit } = internalOptions; + //>>includeStart('debug', pragmas.debug); Check.typeOf.string("iTwinId", iTwinId); Check.typeOf.string("collectionId", collectionId); diff --git a/packages/engine/Specs/Scene/ITwinDataSpec.js b/packages/engine/Specs/Scene/ITwinDataSpec.js index 755d29fedf19..f7e0b0084b8e 100644 --- a/packages/engine/Specs/Scene/ITwinDataSpec.js +++ b/packages/engine/Specs/Scene/ITwinDataSpec.js @@ -55,7 +55,7 @@ describe("ITwinData", () => { ], }); await expectAsync( - ITwinData.createTilesetFromIModelId("imodel-id-1"), + ITwinData.createTilesetFromIModelId({ iModelId: "imodel-id-1" }), ).toBeRejectedWithError(RuntimeError, /All exports for this iModel/); }); @@ -63,7 +63,9 @@ describe("ITwinData", () => { spyOn(ITwinPlatform, "getExports").and.resolveTo({ exports: [], }); - const tileset = await ITwinData.createTilesetFromIModelId("imodel-id-1"); + const tileset = await ITwinData.createTilesetFromIModelId({ + iModelId: "imodel-id-1", + }); expect(tileset).toBeUndefined(); }); @@ -74,7 +76,9 @@ describe("ITwinData", () => { createMockExport(2, ITwinPlatform.ExportStatus.NotStarted), ], }); - const tileset = await ITwinData.createTilesetFromIModelId("imodel-id-1"); + const tileset = await ITwinData.createTilesetFromIModelId({ + iModelId: "imodel-id-1", + }); expect(tileset).toBeUndefined(); }); @@ -85,7 +89,9 @@ describe("ITwinData", () => { createMockExport(2, ITwinPlatform.ExportStatus.NotStarted), ], }); - const tileset = await ITwinData.createTilesetFromIModelId("imodel-id-1"); + const tileset = await ITwinData.createTilesetFromIModelId({ + iModelId: "imodel-id-1", + }); expect(tileset).toBeUndefined(); }); @@ -97,7 +103,7 @@ describe("ITwinData", () => { ], }); const tilesetSpy = spyOn(Cesium3DTileset, "fromUrl"); - await ITwinData.createTilesetFromIModelId("imodel-id-1"); + await ITwinData.createTilesetFromIModelId({ iModelId: "imodel-id-1" }); expect(tilesetSpy).toHaveBeenCalledTimes(1); // Check that the resource url created is for the second export because // the first is invalid @@ -108,6 +114,20 @@ describe("ITwinData", () => { }); it("passes tileset options through to the tileset constructor", async () => { + spyOn(ITwinPlatform, "getExports").and.resolveTo({ + exports: [createMockExport(1, ITwinPlatform.ExportStatus.Complete)], + }); + const tilesetSpy = spyOn(Cesium3DTileset, "fromUrl"); + const tilesetOptions = { show: false }; + await ITwinData.createTilesetFromIModelId({ + iModelId: "imodel-id-1", + tilesetOptions, + }); + expect(tilesetSpy).toHaveBeenCalledTimes(1); + expect(tilesetSpy.calls.mostRecent().args[1]).toEqual(tilesetOptions); + }); + + it("passes tileset options through to the tileset constructor DEPRECATED arguments", async () => { spyOn(ITwinPlatform, "getExports").and.resolveTo({ exports: [createMockExport(1, ITwinPlatform.ExportStatus.Complete)], }); @@ -131,22 +151,22 @@ describe("ITwinData", () => { it("rejects if the type is not supported", async () => { await expectAsync( - ITwinData.createTilesetForRealityDataId( - "imodel-id-1", - "reality-data-id-1", - "DGN", - "root/path.json", - ), + ITwinData.createTilesetForRealityDataId({ + iTwinId: "imodel-id-1", + realityDataId: "reality-data-id-1", + type: "DGN", + rootDocument: "root/path.json", + }), ).toBeRejectedWithError(RuntimeError, /type is not/); }); it("does not fetch metadata if type and rootDocument are defined", async () => { - await ITwinData.createTilesetForRealityDataId( - "itwin-id-1", - "reality-data-id-1", - ITwinPlatform.RealityDataType.Cesium3DTiles, - "root/document/path.json", - ); + await ITwinData.createTilesetForRealityDataId({ + iTwinId: "itwin-id-1", + realityDataId: "reality-data-id-1", + type: ITwinPlatform.RealityDataType.Cesium3DTiles, + rootDocument: "root/document/path.json", + }); expect(getMetadataSpy).not.toHaveBeenCalled(); expect(getUrlSpy).toHaveBeenCalledOnceWith( @@ -163,12 +183,12 @@ describe("ITwinData", () => { type: ITwinPlatform.RealityDataType.Cesium3DTiles, rootDocument: "root/document/path.json", }); - await ITwinData.createTilesetForRealityDataId( - "itwin-id-1", - "reality-data-id-1", - undefined, - "root/document/path.json", - ); + await ITwinData.createTilesetForRealityDataId({ + iTwinId: "itwin-id-1", + realityDataId: "reality-data-id-1", + type: undefined, + rootDocument: "root/document/path.json", + }); expect(getMetadataSpy).toHaveBeenCalledOnceWith( "itwin-id-1", @@ -188,12 +208,12 @@ describe("ITwinData", () => { type: ITwinPlatform.RealityDataType.Cesium3DTiles, rootDocument: "root/document/path.json", }); - await ITwinData.createTilesetForRealityDataId( - "itwin-id-1", - "reality-data-id-1", - ITwinPlatform.RealityDataType.Cesium3DTiles, - undefined, - ); + await ITwinData.createTilesetForRealityDataId({ + iTwinId: "itwin-id-1", + realityDataId: "reality-data-id-1", + type: ITwinPlatform.RealityDataType.Cesium3DTiles, + rootDocument: undefined, + }); expect(getMetadataSpy).toHaveBeenCalledOnceWith( "itwin-id-1", @@ -211,6 +231,23 @@ describe("ITwinData", () => { "https://example.com/root/document/path.json?auth=token"; getUrlSpy.and.resolveTo(tilesetUrl); + await ITwinData.createTilesetForRealityDataId({ + iTwinId: "itwin-id-1", + realityDataId: "reality-data-id-1", + type: ITwinPlatform.RealityDataType.Cesium3DTiles, + rootDocument: "root/document/path.json", + }); + + expect(tilesetSpy).toHaveBeenCalledOnceWith(tilesetUrl, { + maximumScreenSpaceError: 4, + }); + }); + + it("creates a tileset from the constructed blob url using DEPRECATED arguments", async () => { + const tilesetUrl = + "https://example.com/root/document/path.json?auth=token"; + getUrlSpy.and.resolveTo(tilesetUrl); + await ITwinData.createTilesetForRealityDataId( "itwin-id-1", "reality-data-id-1", @@ -238,22 +275,22 @@ describe("ITwinData", () => { it("rejects if the type is not supported", async () => { await expectAsync( - ITwinData.createDataSourceForRealityDataId( - "imodel-id-1", - "reality-data-id-1", - "DGN", - "root/path.json", - ), + ITwinData.createDataSourceForRealityDataId({ + iTwinId: "imodel-id-1", + realityDataId: "reality-data-id-1", + type: "DGN", + rootDocument: "root/path.json", + }), ).toBeRejectedWithError(RuntimeError, /type is not/); }); it("does not fetch metadata if type and rootDocument are defined", async () => { - await ITwinData.createDataSourceForRealityDataId( - "itwin-id-1", - "reality-data-id-1", - ITwinPlatform.RealityDataType.GeoJSON, - "root/document/path.json", - ); + await ITwinData.createDataSourceForRealityDataId({ + iTwinId: "itwin-id-1", + realityDataId: "reality-data-id-1", + type: ITwinPlatform.RealityDataType.GeoJSON, + rootDocument: "root/document/path.json", + }); expect(getMetadataSpy).not.toHaveBeenCalled(); expect(getUrlSpy).toHaveBeenCalledOnceWith( @@ -271,12 +308,12 @@ describe("ITwinData", () => { type: ITwinPlatform.RealityDataType.GeoJSON, rootDocument: "root/document/path.json", }); - await ITwinData.createDataSourceForRealityDataId( - "itwin-id-1", - "reality-data-id-1", - undefined, - "root/document/path.json", - ); + await ITwinData.createDataSourceForRealityDataId({ + iTwinId: "itwin-id-1", + realityDataId: "reality-data-id-1", + type: undefined, + rootDocument: "root/document/path.json", + }); expect(getMetadataSpy).toHaveBeenCalledOnceWith( "itwin-id-1", @@ -296,12 +333,12 @@ describe("ITwinData", () => { type: ITwinPlatform.RealityDataType.GeoJSON, rootDocument: "root/document/path.json", }); - await ITwinData.createDataSourceForRealityDataId( - "itwin-id-1", - "reality-data-id-1", - ITwinPlatform.RealityDataType.Cesium3DTiles, - undefined, - ); + await ITwinData.createDataSourceForRealityDataId({ + iTwinId: "itwin-id-1", + realityDataId: "reality-data-id-1", + type: ITwinPlatform.RealityDataType.Cesium3DTiles, + rootDocument: undefined, + }); expect(getMetadataSpy).toHaveBeenCalledOnceWith( "itwin-id-1", @@ -319,6 +356,22 @@ describe("ITwinData", () => { "https://example.com/root/document/path.json?auth=token"; getUrlSpy.and.resolveTo(tilesetUrl); + await ITwinData.createDataSourceForRealityDataId({ + iTwinId: "itwin-id-1", + realityDataId: "reality-data-id-1", + type: ITwinPlatform.RealityDataType.GeoJSON, + rootDocument: "root/document/path.json", + }); + + expect(geojsonSpy).toHaveBeenCalledOnceWith(tilesetUrl); + expect(kmlSpy).not.toHaveBeenCalled(); + }); + + it("creates a GeoJsonDataSource from the constructed blob url if the type is GeoJSON using DEPRECATED arguments", async () => { + const tilesetUrl = + "https://example.com/root/document/path.json?auth=token"; + getUrlSpy.and.resolveTo(tilesetUrl); + await ITwinData.createDataSourceForRealityDataId( "itwin-id-1", "reality-data-id-1", @@ -335,6 +388,22 @@ describe("ITwinData", () => { "https://example.com/root/document/path.json?auth=token"; getUrlSpy.and.resolveTo(tilesetUrl); + await ITwinData.createDataSourceForRealityDataId({ + iTwinId: "itwin-id-1", + realityDataId: "reality-data-id-1", + type: ITwinPlatform.RealityDataType.KML, + rootDocument: "root/document/path.json", + }); + + expect(kmlSpy).toHaveBeenCalledOnceWith(tilesetUrl); + expect(geojsonSpy).not.toHaveBeenCalled(); + }); + + it("creates a KmlDataSource from the constructed blob url if the type is KML using DEPRECATED arguments", async () => { + const tilesetUrl = + "https://example.com/root/document/path.json?auth=token"; + getUrlSpy.and.resolveTo(tilesetUrl); + await ITwinData.createDataSourceForRealityDataId( "itwin-id-1", "reality-data-id-1", @@ -355,8 +424,7 @@ describe("ITwinData", () => { it("rejects with no iTwinId", async () => { await expectAsync( - // @ts-expect-error - ITwinData.loadGeospatialFeatures(undefined), + ITwinData.loadGeospatialFeatures({ iTwinId: undefined }), ).toBeRejectedWithDeveloperError( "Expected iTwinId to be typeof string, actual typeof was undefined", ); @@ -364,8 +432,10 @@ describe("ITwinData", () => { it("rejects with no collectionId", async () => { await expectAsync( - // @ts-expect-error - ITwinData.loadGeospatialFeatures("itwin-id-1", undefined), + ITwinData.loadGeospatialFeatures({ + iTwinId: "itwin-id-1", + collectionId: undefined, + }), ).toBeRejectedWithDeveloperError( "Expected collectionId to be typeof string, actual typeof was undefined", ); @@ -373,7 +443,11 @@ describe("ITwinData", () => { it("rejects with limit < 1", async () => { await expectAsync( - ITwinData.loadGeospatialFeatures("itwin-id-1", "collection-id-1", 0), + ITwinData.loadGeospatialFeatures({ + iTwinId: "itwin-id-1", + collectionId: "collection-id-1", + limit: 0, + }), ).toBeRejectedWithDeveloperError( "Expected limit to be greater than or equal to 1, actual value was 0", ); @@ -381,11 +455,11 @@ describe("ITwinData", () => { it("rejects with limit > 10000", async () => { await expectAsync( - ITwinData.loadGeospatialFeatures( - "itwin-id-1", - "collection-id-1", - 20000, - ), + ITwinData.loadGeospatialFeatures({ + iTwinId: "itwin-id-1", + collectionId: "collection-id-1", + limit: 20000, + }), ).toBeRejectedWithDeveloperError( "Expected limit to be less than or equal to 10000, actual value was 20000", ); @@ -395,13 +469,28 @@ describe("ITwinData", () => { ITwinPlatform.defaultAccessToken = undefined; ITwinPlatform.defaultShareKey = undefined; await expectAsync( - ITwinData.loadGeospatialFeatures("itwin-id-1", "collection-id-1"), + ITwinData.loadGeospatialFeatures({ + iTwinId: "itwin-id-1", + collectionId: "collection-id-1", + }), ).toBeRejectedWithDeveloperError( /Must set ITwinPlatform.defaultAccessToken or ITwinPlatform.defaultShareKey/, ); }); it("creates a GeoJsonDataSource from the constructed blob url if the type is GeoJSON", async () => { + await ITwinData.loadGeospatialFeatures({ + iTwinId: "itwin-id-1", + collectionId: "collection-id-1", + }); + + expect(geojsonSpy).toHaveBeenCalledTimes(1); + expect(geojsonSpy.calls.mostRecent().args[0].url).toEqual( + "https://api.bentley.com/geospatial-features/itwins/itwin-id-1/ogc/collections/collection-id-1/items?limit=10000&client=CesiumJS", + ); + }); + + it("creates a GeoJsonDataSource from the constructed blob url if the type is GeoJSON using DEPRECATED arguments", async () => { await ITwinData.loadGeospatialFeatures("itwin-id-1", "collection-id-1"); expect(geojsonSpy).toHaveBeenCalledTimes(1); From d339a715b65ab01bc9cf6884d731bcd5eed93c25 Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:24:05 -0400 Subject: [PATCH 5/8] do not request empty changeset ids --- packages/engine/Source/Core/ITwinPlatform.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/engine/Source/Core/ITwinPlatform.js b/packages/engine/Source/Core/ITwinPlatform.js index 18f7aacf2bd3..2b258545d3ec 100644 --- a/packages/engine/Source/Core/ITwinPlatform.js +++ b/packages/engine/Source/Core/ITwinPlatform.js @@ -195,7 +195,7 @@ ITwinPlatform.getExports = async function (iModelId, changesetId) { if (typeof CESIUM_VERSION !== "undefined") { resource.appendQueryParameters({ clientVersion: CESIUM_VERSION }); } - if (defined(changesetId)) { + if (defined(changesetId) && changesetId !== "") { resource.appendQueryParameters({ changesetId: changesetId }); } From 3b25fd21da81b8658badbc72834a431db215ab2f Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Thu, 31 Jul 2025 15:08:15 -0400 Subject: [PATCH 6/8] update changes --- CHANGES.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1ce0ef46b8eb..c3a0a0024d6b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,10 +4,6 @@ ### @cesium/engine -#### Breaking Changes :mega: - -- Updated all of the `ITwinData.*` functions to accept an `options` parameter instead of individual arguments to avoid confusion with multiple optional arguments. There is a fallback to the old signature that will be removed in 1.133 [#12778](https://github.com/CesiumGS/cesium/issues/12778) - #### Fixes :wrench: - Fixes incorrect polygon culling in 2D scene mode. [#1552](https://github.com/CesiumGS/cesium/issues/1552) @@ -27,6 +23,10 @@ - Expand the CustomShader Sample to support real-time modification of CustomShader. [#12702](https://github.com/CesiumGS/cesium/pull/12702) - Added the ability to load a specific changeset for iTwin Mesh Exports using `ITwinData.createTilesetFromIModelId` [#12778](https://github.com/CesiumGS/cesium/issues/12778) +#### Deprecated :hourglass_flowing_sand: + +- Updated all of the `ITwinData.*` functions to accept an `options` parameter instead of individual arguments to avoid confusion with multiple optional arguments. There is a fallback to the old signature that will be removed in 1.133 [#12778](https://github.com/CesiumGS/cesium/issues/12778) + ## 1.131 - 2025-07-01 ### @cesium/engine @@ -2453,7 +2453,6 @@ _This is an npm-only release to fix a publishing issue_. tileset.boundingSphere.center, ); ``` - - This also fixes several issues with clipping planes not using the correct transform for tilesets with children. ### Additions :tada: @@ -4299,7 +4298,6 @@ _This is an npm-only release to fix a publishing issue_. isStopIncluded : true, data : data }); - - `TimeInterval.fromIso8601` now takes a single options parameter. Code that looked like: TimeInterval.fromIso8601(intervalString, true, true, data); @@ -4312,7 +4310,6 @@ _This is an npm-only release to fix a publishing issue_. isStopIncluded : true, data : data }); - - `interval.intersect(otherInterval)` -> `TimeInterval.intersect(interval, otherInterval)` - `interval.contains(date)` -> `TimeInterval.contains(interval, date)` From 9974eb474f709bde69b100b393850f07f9354613 Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Thu, 31 Jul 2025 15:47:56 -0400 Subject: [PATCH 7/8] fix linting --- CHANGES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 60d33ab47f54..9d8fe3a04529 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2454,6 +2454,7 @@ _This is an npm-only release to fix a publishing issue_. tileset.boundingSphere.center, ); ``` + - This also fixes several issues with clipping planes not using the correct transform for tilesets with children. ### Additions :tada: @@ -4299,6 +4300,7 @@ _This is an npm-only release to fix a publishing issue_. isStopIncluded : true, data : data }); + - `TimeInterval.fromIso8601` now takes a single options parameter. Code that looked like: TimeInterval.fromIso8601(intervalString, true, true, data); @@ -4311,6 +4313,7 @@ _This is an npm-only release to fix a publishing issue_. isStopIncluded : true, data : data }); + - `interval.intersect(otherInterval)` -> `TimeInterval.intersect(interval, otherInterval)` - `interval.contains(date)` -> `TimeInterval.contains(interval, date)` From ab4867f38d86883bcaad39871bf20a049866df1e Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:07:28 -0400 Subject: [PATCH 8/8] update docs --- packages/engine/Source/Scene/ITwinData.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/engine/Source/Scene/ITwinData.js b/packages/engine/Source/Scene/ITwinData.js index 721c0c74d7b7..9e023599a918 100644 --- a/packages/engine/Source/Scene/ITwinData.js +++ b/packages/engine/Source/Scene/ITwinData.js @@ -26,6 +26,8 @@ const ITwinData = {}; * We recommend waiting 10-20 seconds and trying to load the tileset again. * If all exports are Invalid this will throw an error. * + * See the {@link https://developer.bentley.com/apis/mesh-export/overview/|iTwin Platform Mesh Export API documentation} for more information on request parameters + * * @example * const tileset = await Cesium.ITwinData.createTilesetFromIModelId({ iModelId }); * if (Cesium.defined(tileset)) {