From 0f6db77484718c2c706d4922a3118b02b40505f3 Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:14:56 -0500 Subject: [PATCH 01/22] initial rough api --- Apps/Sandcastle/gallery/iTwin Demo.html | 295 ++++++++++++++ packages/engine/Source/Core/ITwin.js | 66 ++++ .../Source/Scene/createIModel3DTileset.js | 371 ++++++++++++++++++ 3 files changed, 732 insertions(+) create mode 100644 Apps/Sandcastle/gallery/iTwin Demo.html create mode 100644 packages/engine/Source/Core/ITwin.js create mode 100644 packages/engine/Source/Scene/createIModel3DTileset.js diff --git a/Apps/Sandcastle/gallery/iTwin Demo.html b/Apps/Sandcastle/gallery/iTwin Demo.html new file mode 100644 index 000000000000..e1674a6b9e74 --- /dev/null +++ b/Apps/Sandcastle/gallery/iTwin Demo.html @@ -0,0 +1,295 @@ + + + + + + + + + Cesium Demo + + + + + +
+

Loading...

+
+
+ Initializing +
+ + + diff --git a/packages/engine/Source/Core/ITwin.js b/packages/engine/Source/Core/ITwin.js new file mode 100644 index 000000000000..23826cfc5d4b --- /dev/null +++ b/packages/engine/Source/Core/ITwin.js @@ -0,0 +1,66 @@ +import Resource from "./Resource.js"; + +/** + * Default settings for accessing the iTwin platform. + * + * Keys can be created using the iModels share routes {@link https://developer.bentley.com/apis/imodels-v2/operations/create-imodel-share/} + * + * An ion access token is only required if you are using any ion related APIs. + * A default access token is provided for evaluation purposes only. + * Sign up for a free ion account and get your own access token at {@link https://cesium.com} + * + * @see IonResource + * @see IonImageryProvider + * @see IonGeocoderService + * @see createWorldImagery + * @see createWorldTerrain + * @namespace ITwin + */ +const ITwin = {}; + +/** + * Gets or sets the default iTwin access token. + * + * TODO: I'm not sure we can even do this kind of access token. Each route seems to need it's own scopes + * and we may not be able to guarantee this "top level token" has them all + * So far we use + * `mesh-export:read` for loading meshes GET /mesh-export(s) + * `mesh-export:modify` if we want to include a function to create an export + * `itwin-platform` if we want to use the iModel shares ourselves GET /imodels/{id}/shares + * + * + * @type {string|undefined} + */ +ITwin.defaultAccessToken = undefined; + +/** + * Gets or sets the default Google Map Tiles API endpoint. + * + * @type {string|Resource} + * @default https://api.bentley.com + */ +ITwin.apiEndpoint = new Resource({ + url: "https://api.bentley.com", +}); + +// TODO: this should only be needed if we have a way to generate really long term access tokens +// to sample data that is accessible to everyone +// ITwin.getDefaultTokenCredit = function (providedKey) { +// if (providedKey !== defaultAccessToken) { +// return undefined; +// } + +// if (!defined(defaultTokenCredit)) { +// const defaultTokenMessage = +// ' \ +// This application is using Cesium\'s default ion access token. Please assign Cesium.Ion.defaultAccessToken \ +// with an access token from your ion account before making any Cesium API calls. \ +// You can sign up for a free ion account at https://cesium.com.'; + +// defaultTokenCredit = new Credit(defaultTokenMessage, true); +// } + +// return defaultTokenCredit; +// }; + +export default ITwin; diff --git a/packages/engine/Source/Scene/createIModel3DTileset.js b/packages/engine/Source/Scene/createIModel3DTileset.js new file mode 100644 index 000000000000..1405aac4e5a2 --- /dev/null +++ b/packages/engine/Source/Scene/createIModel3DTileset.js @@ -0,0 +1,371 @@ +import Cesium3DTileset from "./Cesium3DTileset.js"; +import defined from "../Core/defined.js"; +import Resource from "../Core/Resource.js"; +import ITwin from "../Core/ITwin.js"; +import DeveloperError from "../Core/DeveloperError.js"; + +function delay(ms) { + return new Promise((res) => setTimeout(res, ms)); +} + +/** + * @enum {string} + */ +const ExportStatus = Object.freeze({ + NotStarted: "NotStarted", + InProgress: "InProgress", + Complete: "Complete", + Invalid: "Invalid", +}); + +/** + * Type of an export currently, only GLTF and 3DFT are documented + * The CESIUM option is what we were told to use with Sandcastle + * I've also seen the IMODEL one but don't know where it's from + * @enum {string} + */ +const ExportType = Object.freeze({ + "3DFT": "3DFT", + GLFT: "GLTF", + IMODEL: "IMODEL", + CESIUM: "CESIUM", +}); + +/** + * @typedef {Object} GeometryOptions + * @property {boolean} includeLines + * @property {number} chordTol + * @property {number} angleTol + * @property {number} decimationTol + * @property {number} maxEdgeLength + * @property {number} minBRepFeatureSize + * @property {number} minLineStyleComponentSize + */ + +/** + * @typedef {Object} ViewDefinitionFilter + * @property {string[]} models Array of included model IDs. + * @property {string[]} categories Array of included category IDs. + * @property {string[]} neverDrawn Array of element IDs to filter out. + */ + +/** + * @typedef {Object} StartExport + * @property {string} iModelId + * @property {string} changesetId + * @property {ExportType} exportType Type of mesh to create. Currently, only GLTF and 3DFT are supported and undocumented CESIUM option + * @property {GeometryOptions} geometryOptions + * @property {ViewDefinitionFilter} viewDefinitionFilter + */ + +/** + * @typedef {Object} Link + * @property {string} href + */ + +/** + * @typedef {Object} Export + * @property {string} id + * @property {string} displayName + * @property {ExportStatus} status + * @property {StartExport} request + * @property {{mesh: Link}} _links + */ + +/** + * @typedef {Object} ExportResponse + * @property {Export} export + */ + +/** + * Creates a {@link Cesium3DTileset} instance for the Google Photorealistic 3D Tiles tileset. + * + * @function + * + * @param {string} exportId + * @param {Cesium3DTileset.ConstructorOptions} [options] An object describing initialization options. + * @returns {Promise} + * + * @see ITwin + * + * @example + * // Use your own iTwin API key for mesh export + * Cesium.ITwin.defaultApiKey = "your-api-key"; + * + * const viewer = new Cesium.Viewer("cesiumContainer"); + * + * try { + * const tileset = await Cesium.createIModel3DTileset(); + * viewer.scene.primitives.add(tileset)); + * } catch (error) { + * console.log(`Error creating tileset: ${error}`); + * } + */ +async function createIModel3DTileset(exportId, options) { + if (!defined(ITwin.defaultAccessToken)) { + throw new DeveloperError("Must set ITwin.defaultAccessToken first"); + } + + options = options ?? {}; + + const timeoutAfter = 300000; + const start = Date.now(); + let result = await createIModel3DTileset.getExport(exportId); + let status = result.export.status; + + if (result.export.request.exportType !== ExportType.CESIUM) { + // This is an undocumented value but I think it's the only one we want to load + // TODO: should we even be checking this? + throw new Error(`Wrong export type ${result.export.request.exportType}`); + } + + // wait until the export is complete + while (status !== ExportStatus.Complete) { + await delay(5000); + result = await createIModel3DTileset.getExport(exportId); + status = result.export.status; + console.log(`Export is ${status}`); + + if (Date.now() - start > timeoutAfter) { + throw new Error("Export did not complete in time."); + } + } + + // This link is only valid 1 hour + let tilesetUrl = result.export._links.mesh.href; + const splitStr = tilesetUrl.split("?"); + // is there a cleaner way to do this? + tilesetUrl = `${splitStr[0]}/tileset.json?${splitStr[1]}`; + + const resource = new Resource({ + url: tilesetUrl, + }); + + return Cesium3DTileset.fromUrl(resource, options); +} + +/** + * @param {string} exportId + */ +createIModel3DTileset.getExport = async function (exportId) { + const headers = { + Authorization: ITwin.defaultAccessToken, + Accept: "application/vnd.bentley.itwin-platform.v1+json", + }; + + // obtain export for specified export id + const url = `${ITwin.apiEndpoint}mesh-export/${exportId}`; + + // TODO: this request is _really_ slow, like 7 whole second alone for me + // Arun said this was kinda normal but to keep track of the `x-correlation-id` of any that take EXTRA long + const response = await fetch(url, { headers }); + if (!response.ok) { + const result = await response.json(); + if (response.status === 401) { + throw new Error( + `Unauthorized, bad token, wrong scopes or headers bad. ${result.error.details[0].code}`, + ); + } else if (response.status === 404) { + throw new Error(`Requested export is not available ${exportId}`); + } else if (response.status === 429) { + throw new Error("Too many requests"); + } + throw new Error(`Unknown request failure ${response.status}`); + } + + /** @type {ExportResponse} */ + const result = await response.json(); + return result; +}; + +/** + * Get the list of exports for the given iModel + changeset + * + * @param {string} iModelId + * @param {string} changesetId + */ +createIModel3DTileset.getExports = async function (iModelId, changesetId) { + if (!defined(ITwin.defaultAccessToken)) { + throw new DeveloperError("Must set ITwin.defaultAccessToken first"); + } + const headers = { + Authorization: ITwin.defaultAccessToken, + Accept: "application/vnd.bentley.itwin-platform.v1+json", + Prefer: "return=representation", // or return=minimal (the default) + }; + + // obtain export for specified export id + let url = `${ITwin.apiEndpoint}mesh-export/?iModelId=${iModelId}`; + if (defined(changesetId) && changesetId !== "") { + url += `&changesetId=${changesetId}`; + } + + const response = await fetch(url, { headers }); + if (!response.ok) { + const result = await response.json(); + if (response.status === 401) { + throw new Error( + `Unauthorized, bad token, wrong scopes or headers bad. ${result.error.details[0].code}`, + ); + } else if (response.status === 422) { + throw new Error( + `Unprocessable Entity:${result.error.code} ${result.error.message}`, + ); + } else if (response.status === 429) { + throw new Error("Too many requests"); + } + throw new Error(`Unknown request failure ${response.status}`); + } + + /** @type {{exports: Export[]}} */ + const result = await response.json(); + return result; +}; + +/** + * Check the exports for the given iModel + changeset combination for any that + * have the desired CESIUM type and return that one + * + * @param {string} iModelId + * @param {string} changesetId + */ +createIModel3DTileset.checkForCesiumExport = async function ( + iModelId, + changesetId, +) { + const { exports } = await createIModel3DTileset.getExports( + iModelId, + changesetId, + ); + const cesiumExport = exports.find( + (e) => e.request.exportType === ExportType.CESIUM, + ); + return cesiumExport; +}; + +/** + * Start the export process for the given iModel + changeset. + * + * Pair this with the {@link checkForCesiumExport} function to avoid creating extra exports + * + * @example + * const cesiumExport = await Cesium.createIModel3DTileset.checkForCesiumExport(imodelId, changesetId); + * let exportId = cesiumExport?.id; + * if (!Cesium.defined(cesiumExport)) { + * exportId = await Cesium.createIModel3DTileset.createExportForModelId( + * imodelId, + * changesetId, + * accessToken, + * ); + * } + * + * @param {string} iModelId + * @param {string} changesetId + */ +createIModel3DTileset.createExportForModelId = async function ( + iModelId, + changesetId, +) { + if (!defined(ITwin.defaultAccessToken)) { + throw new DeveloperError("Must set ITwin.defaultAccessToken first"); + } + + console.log("Start Export"); + + changesetId = changesetId ?? ""; + + const requestOptions = { + method: "POST", + headers: { + Authorization: ITwin.defaultAccessToken, + Accept: "application/vnd.bentley.itwin-platform.v1+json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + iModelId, + changesetId, + exportType: "CESIUM", + }), + }; + + // initiate mesh export + const response = await fetch( + `https://api.bentley.com/mesh-export/`, + requestOptions, + ); + + if (!response.ok) { + const result = await response.json(); + if (response.status === 401) { + console.error("Unauthorized, bad token, wrong scopes or headers bad"); + console.error( + result.error.code, + result.error.message, + result.error.details, + ); + } else if (response.status === 403) { + console.error("Not allowed, forbidden"); + console.error(result.error.code, result.error.message); + } else if (response.status === 422) { + console.error("Unprocessable: Cannot create export job"); + console.error(result.error.code, result.error.message); + console.error(result.error.details); + } else if (response.status === 429) { + console.log( + "Too many requests, retry after:", + response.headers.get("retry-after"), + ); + console.error(result.error.code, result.error.message); + } else { + console.error("Bad request, unknown error", response); + } + return undefined; + } + + /** @type {ExportResponse} */ + const result = await response.json(); + return result.export.id; +}; + +/** + * Delete the specified export + * + * TODO: I'm not sure if we want this or not. Might belong better as an APP level function + * I just started creating helpers for all the routes under the `mesh-export` API + * for ease of access during testing + * + * @param {string} exportId + */ +createIModel3DTileset.deleteExport = async function (exportId) { + if (!defined(ITwin.defaultAccessToken)) { + throw new DeveloperError("Must set ITwin.defaultAccessToken first"); + } + const headers = { + Authorization: ITwin.defaultAccessToken, + Accept: "application/vnd.bentley.itwin-platform.v1+json", + }; + + // obtain export for specified export id + const url = `${ITwin.apiEndpoint}mesh-export/${exportId}`; + + const response = await fetch(url, { method: "DELETE", headers }); + if (!response.ok) { + const result = await response.json(); + if (response.status === 401) { + throw new Error( + `Unauthorized, bad token, wrong scopes or headers bad. ${result.error.details[0].code}`, + ); + } else if (response.status === 404) { + throw new Error("Export not found"); + } else if (response.status === 422) { + throw new Error( + `Unprocessable Entity:${result.error.code} ${result.error.message}`, + ); + } else if (response.status === 429) { + throw new Error("Too many requests"); + } + throw new Error(`Unknown request failure ${response.status}`); + } +}; + +export default createIModel3DTileset; From 421e5dda69820dd7e9a947f5644567325368d2d7 Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:33:11 -0500 Subject: [PATCH 02/22] restructure api, create tileset from model id --- Apps/Sandcastle/gallery/iTwin Demo.html | 158 ++------ packages/engine/Source/Core/ITwin.js | 274 +++++++++++++- .../Source/Scene/createIModel3DTileset.js | 358 +++--------------- 3 files changed, 351 insertions(+), 439 deletions(-) diff --git a/Apps/Sandcastle/gallery/iTwin Demo.html b/Apps/Sandcastle/gallery/iTwin Demo.html index e1674a6b9e74..d8d4865927e2 100644 --- a/Apps/Sandcastle/gallery/iTwin Demo.html +++ b/Apps/Sandcastle/gallery/iTwin Demo.html @@ -33,85 +33,18 @@ // must be created for the Start export route if you want to create new exports // Needs to have the mesh-export:modify scope not just mesh-export:read const accessToken = - "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkJlbnRsZXlJTVNfMjAyNCIsInBpLmF0bSI6ImE4bWUifQ.eyJzY29wZSI6WyJtZXNoLWV4cG9ydDpyZWFkIiwibWVzaC1leHBvcnQ6bW9kaWZ5Il0sImNsaWVudF9pZCI6Iml0d2luLWRldmVsb3Blci1jb25zb2xlIiwiYXVkIjpbImh0dHBzOi8vaW1zLmJlbnRsZXkuY29tL2FzL3Rva2VuLm9hdXRoMiIsImh0dHBzOi8vaW1zb2lkYy5iZW50bGV5LmNvbS9hcy90b2tlbi5vYXV0aDIiLCJodHRwczovL2ltc29pZGMuYmVudGxleS5jb20vcmVzb3VyY2VzIiwiYmVudGxleS1hcGktbWFuYWdlbWVudCJdLCJzdWIiOiJjMWM1MzRhNy0zZDk2LTQ2MzMtYjY5ZC1jMGEzNzE5OWQwZGUiLCJyb2xlIjoiQkVOVExFWV9FTVBMT1lFRSIsIm9yZyI6ImZhYjk3NzRiLWIzMzgtNGNjMi1hNmM5LTQ1OGJkZjdmOTY2YSIsInN1YmplY3QiOiJjMWM1MzRhNy0zZDk2LTQ2MzMtYjY5ZC1jMGEzNzE5OWQwZGUiLCJpc3MiOiJodHRwczovL2ltcy5iZW50bGV5LmNvbSIsImVudGl0bGVtZW50IjpbIkJFTlRMRVlfTEVBUk4iLCJJTlRFUk5BTCIsIlNFTEVDVF8yMDA2IiwiQkVOIiwiQkROIl0sInByZWZlcnJlZF91c2VybmFtZSI6Ikpvc2guUm91emVyQGJlbnRsZXkuY29tIiwiZ2l2ZW5fbmFtZSI6Ikpvc2giLCJzaWQiOiJHMGZTamlOXzBMRjE1Vy1IYnRTaWtoeDNuSXMuU1UxVExVSmxiblJzWlhrdFZWTS5LUDVRLjZCakNoYXptMldBMjBIdXVWMUxwNFB6RVAiLCJuYmYiOjE3MzA3NDc3NjUsInVsdGltYXRlX3NpdGUiOiIxMDAxMzg5MTE3IiwidXNhZ2VfY291bnRyeV9pc28iOiJVUyIsImF1dGhfdGltZSI6MTczMDc0ODA2NSwibmFtZSI6Ikpvc2guUm91emVyQGJlbnRsZXkuY29tIiwib3JnX25hbWUiOiJCZW50bGV5IFN5c3RlbXMgSW5jIiwiZmFtaWx5X25hbWUiOiJSb3V6ZXIiLCJlbWFpbCI6Ikpvc2guUm91emVyQGJlbnRsZXkuY29tIiwiZXhwIjoxNzMwNzUxNjY2fQ.0yEQZAyKKAdwNDiwHSD6f_Qzq0M8cbHJcMfT6JidBldw9qiyU4jx6ZdqILddrL-seWCkf9sRtWuoHm7Fw-j_wtaLASaOpHMMwC7IVdh25pbRB-D3mN8_rmQiDbXUadJ1MwH8-pNCubrER1lZLYEPrQ4zJcRtAblbJGNjFdoOi3FXB-y3JLleH4qYykLceDkbW3l2lZRfdIW2pytCIuZs7XZ9Hr6F_cLsYIrs9iRfBFVxxHxxfwgRLQoRuPmDuzKl9-ylLZUFN6CQZUfv7vzL9feoXJXGdZeGgqlyFuMOCiVbYU_elx7P7fFm8G13HvTti5hz98mV1r2bXYvfNpuklg"; + "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkJlbnRsZXlJTVNfMjAyNCIsInBpLmF0bSI6ImE4bWUifQ.eyJzY29wZSI6WyJtZXNoLWV4cG9ydDpyZWFkIiwibWVzaC1leHBvcnQ6bW9kaWZ5Il0sImNsaWVudF9pZCI6Iml0d2luLWRldmVsb3Blci1jb25zb2xlIiwiYXVkIjpbImh0dHBzOi8vaW1zLmJlbnRsZXkuY29tL2FzL3Rva2VuLm9hdXRoMiIsImh0dHBzOi8vaW1zb2lkYy5iZW50bGV5LmNvbS9hcy90b2tlbi5vYXV0aDIiLCJodHRwczovL2ltc29pZGMuYmVudGxleS5jb20vcmVzb3VyY2VzIiwiYmVudGxleS1hcGktbWFuYWdlbWVudCJdLCJzdWIiOiJjMWM1MzRhNy0zZDk2LTQ2MzMtYjY5ZC1jMGEzNzE5OWQwZGUiLCJyb2xlIjoiQkVOVExFWV9FTVBMT1lFRSIsIm9yZyI6ImZhYjk3NzRiLWIzMzgtNGNjMi1hNmM5LTQ1OGJkZjdmOTY2YSIsInN1YmplY3QiOiJjMWM1MzRhNy0zZDk2LTQ2MzMtYjY5ZC1jMGEzNzE5OWQwZGUiLCJpc3MiOiJodHRwczovL2ltcy5iZW50bGV5LmNvbSIsImVudGl0bGVtZW50IjpbIkJFTlRMRVlfTEVBUk4iLCJJTlRFUk5BTCIsIlNFTEVDVF8yMDA2IiwiQkVOIiwiQkROIl0sInByZWZlcnJlZF91c2VybmFtZSI6Ikpvc2guUm91emVyQGJlbnRsZXkuY29tIiwiZ2l2ZW5fbmFtZSI6Ikpvc2giLCJzaWQiOiJHMGZTamlOXzBMRjE1Vy1IYnRTaWtoeDNuSXMuU1UxVExVSmxiblJzWlhrdFZWTS5LbWlWLlF2UExVcGJyc2R3engwWGxPbkF3eTFTT0IiLCJuYmYiOjE3MzA4MzgyNDIsInVsdGltYXRlX3NpdGUiOiIxMDAxMzg5MTE3IiwidXNhZ2VfY291bnRyeV9pc28iOiJVUyIsImF1dGhfdGltZSI6MTczMDgzODU0MiwibmFtZSI6Ikpvc2guUm91emVyQGJlbnRsZXkuY29tIiwib3JnX25hbWUiOiJCZW50bGV5IFN5c3RlbXMgSW5jIiwiZmFtaWx5X25hbWUiOiJSb3V6ZXIiLCJlbWFpbCI6Ikpvc2guUm91emVyQGJlbnRsZXkuY29tIiwiZXhwIjoxNzMwODQyMTQyfQ.N_WrgjL2bqxdNLEM5nHh4Fg-FzeA-qxxpryaoaMKz8onnpgzmp-X8dyQ1TyMRqKQY99iLypE9bU45DwRJs7vBgNQ73d53SkC9TKGYn8AAOTJz8c_oEHgkoTEDaaLsMPCw88tqZ34pY0e0oIHIofVDTCvwlzwaJPADfkIxz-8GzhIt2WrXR7f6LWBFSlWNrtNhIr9Tz7UNaLOh97_3fS9KVU1I084CpBga9cj_mjGBeki7mIEQvpqMj8x2bJPae_c6WrPEjKLayrOzq4q0X0Kvle0ZFAm-Se9MFCusTFS51bYrI3IsjagGeIP2U06zzcMJ22NylomE60hz6GK_4uB3g"; Cesium.ITwin.defaultAccessToken = accessToken; // this is the iModel in the "Hello iTwinCesium" iTwin that we should all have access to // https://developer.bentley.com/my-itwins/b4a30036-0456-49ea-a439-3fcd9365e24e/home/ const imodelId = "2852c3d7-00c3-4b5d-a0ce-82bbde4f061e"; + // const imodelId = "88673c1d-12b8-48f1-8beb-5000d0edbd0b"; + + const changesetId = ""; // const knownExportId = undefined; // const knownExportId = "ab9953b2-bc8e-48ac-a5b0-5d43d68593e8"; - // async function startExport(iModelId, changesetId, accessToken) { - // console.log("Start Export"); - - // const requestOptions = { - // method: "POST", - // headers: { - // Authorization: accessToken, - // Accept: "application/vnd.bentley.itwin-platform.v1+json", - // "Content-Type": "application/json", - // }, - // body: JSON.stringify({ - // iModelId, - // changesetId, - // exportType: "CESIUM", - // }), - // }; - - // // initiate mesh export - // const response = await fetch( - // `https://api.bentley.com/mesh-export/`, - // requestOptions, - // ); - // if (!response.ok) { - // if (response.status === 401) { - // console.error("Unauthorized, bad token, wrong scopes or headers bad"); - // } else if (response.status === 403) { - // console.error("Not allowed, forbidden"); - // } else if (response.status === 422) { - // console.error("Unprocessable: Cannot create export job"); - // } else if (response.status === 429) { - // console.log("Too many requests"); - // } else { - // console.error("Bad request, unknown error", response); - // } - // return undefined; - // } - // const result = JSON.parse(JSON.stringify(await response.json())); - // return result?.export?.id; - // } - - // async function getExport(exportId, accessToken) { - // const headers = { - // Authorization: accessToken, - // Accept: "application/vnd.bentley.itwin-platform.v1+json", - // }; - - // // obtain export for specified export id - // const url = `https://api.bentley.com/mesh-export/${exportId}`; - // try { - // // TODO: this request is _really_ slow, like 7 whole second alone for me - // const response = await fetch(url, { headers }); - // if (!response.ok) { - // if (response.status === 401) { - // console.error("Unauthorized, bad token, wrong scopes or headers bad"); - // } else if (response.status === 404) { - // console.error("Requested export is not available", exportId); - // } else if (response.status === 429) { - // console.error("Too many requests"); - // } else { - // console.log("Unknown request failure", response); - // } - // return undefined; - // } - // const result = JSON.parse(JSON.stringify(await response.json())); - // return result; - // } catch (err) { - // return undefined; - // } - // } - const delay = (ms) => new Promise((res) => setTimeout(res, ms)); // Grabbed mapping from the iTwin Viewer @@ -192,69 +125,44 @@ const statusOutput = document.querySelector("#status"); async function init() { // TODO: just for testing - // await Cesium.createIModel3DTileset.deleteExport( - // "3a627319-cf6e-4535-9499-da5320a69791", - // ); - // console.log(await Cesium.createIModel3DTileset.checkForCesiumExport(imodelId)); - // console.log(await Cesium.createIModel3DTileset.getExports(imodelId)); - - const changesetId = ""; - const cesiumExport = await Cesium.createIModel3DTileset.checkForCesiumExport( - imodelId, - changesetId, - ); - let exportId = cesiumExport?.id; - if (!Cesium.defined(cesiumExport)) { - exportId = await Cesium.createIModel3DTileset.createExportForModelId( - imodelId, - changesetId, - accessToken, - ); - } + // await Cesium.ITwin.deleteExport("8bd9c52e-b379-4f7d-bf39-c45954030b26"); + // console.log(await Cesium.ITwin.getExports(imodelId)); statusOutput.innerText = "Starting export"; // const exportId = // knownExportId ?? - // (await Cesium.createIModel3DTileset.createExportForModelId( + // (await Cesium.ITwin.createExportForModelId( // imodelId, // "", // accessToken, // )); - if (!exportId) { - console.error("No export id returned"); - return; - } - console.log("Using export id", exportId); + // if (!exportId) { + // console.error("No export id returned"); + // return; + // } + // console.log("Using export id", exportId); const start = Date.now(); - // let result = await getExport(exportId, accessToken); - // let status = result.export.status; - // while (status !== "Complete") { - // await delay(5000); - // result = await getExport(exportId, accessToken); - // status = result.export.status; - // console.log(`Export is ${status}`); - - // if (Date.now() - start > 300000) { - // throw new Error("Export did not complete in time."); - // } - // } - // if (result.export.request.exportType !== "CESIUM") { - // console.error("Wrong export type", result.export.request.exportType); - // throw new Error(`Wrong export type ${result.export.request.exportType}`); - // } + statusOutput.innerText = "Creating Tileset"; - // // This link is only valid 1 hour - // let tilesetUrl = result.export._links.mesh.href; - // const splitStr = tilesetUrl.split("?"); - // // is there a cleaner way to do this? - // tilesetUrl = `${splitStr[0]}/tileset.json?${splitStr[1]}`; + let tileset = await Cesium.createIModel3DTileset.fromModelId( + imodelId, + changesetId, + ); + if (!Cesium.defined(tileset)) { + statusOutput.innerText = "Starting export"; + const exportId = await Cesium.ITwin.createExportForModelId( + imodelId, + changesetId, + accessToken, + ); + statusOutput.innerText = "Creating Tileset from export"; + tileset = await Cesium.createIModel3DTileset.fromExportId(exportId); + } - // const tileset = await Cesium.Cesium3DTileset.fromUrl(tilesetUrl); - statusOutput.innerText = "Creating Tileset"; - const tileset = await Cesium.createIModel3DTileset(exportId); + // const tileset = await Cesium.createIModel3DTileset(exportId); scene.primitives.add(tileset); tileset.colorBlendMode = Cesium.Cesium3DTileColorBlendMode.REPLACE; @@ -278,7 +186,15 @@ }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); } - init(); + init().catch((error) => { + statusOutput.style.color = "red"; + if (error.message.includes("Unauthorized")) { + statusOutput.innerText = "Error: Unauthorized"; + } else { + statusOutput.innerText = "Error"; + } + console.error(error); + }); //Sandcastle_End Sandcastle.finishedLoading(); diff --git a/packages/engine/Source/Core/ITwin.js b/packages/engine/Source/Core/ITwin.js index 23826cfc5d4b..ab74ad0c5536 100644 --- a/packages/engine/Source/Core/ITwin.js +++ b/packages/engine/Source/Core/ITwin.js @@ -1,4 +1,76 @@ +import defined from "./defined.js"; +import DeveloperError from "./DeveloperError.js"; import Resource from "./Resource.js"; +import RuntimeError from "./RuntimeError.js"; + +/** + * @enum {string} + */ +const ExportStatus = Object.freeze({ + NotStarted: "NotStarted", + InProgress: "InProgress", + Complete: "Complete", + Invalid: "Invalid", +}); + +/** + * Type of an export currently, only GLTF and 3DFT are documented + * The CESIUM option is what we were told to use with Sandcastle + * I've also seen the IMODEL one but don't know where it's from + * @enum {string} + */ +const ExportType = Object.freeze({ + "3DFT": "3DFT", + GLFT: "GLTF", + IMODEL: "IMODEL", + CESIUM: "CESIUM", +}); + +/** + * @typedef {Object} GeometryOptions + * @property {boolean} includeLines + * @property {number} chordTol + * @property {number} angleTol + * @property {number} decimationTol + * @property {number} maxEdgeLength + * @property {number} minBRepFeatureSize + * @property {number} minLineStyleComponentSize + */ + +/** + * @typedef {Object} ViewDefinitionFilter + * @property {string[]} models Array of included model IDs. + * @property {string[]} categories Array of included category IDs. + * @property {string[]} neverDrawn Array of element IDs to filter out. + */ + +/** + * @typedef {Object} StartExport + * @property {string} iModelId + * @property {string} changesetId + * @property {ExportType} exportType Type of mesh to create. Currently, only GLTF and 3DFT are supported and undocumented CESIUM option + * @property {GeometryOptions} geometryOptions + * @property {ViewDefinitionFilter} viewDefinitionFilter + */ + +/** + * @typedef {Object} Link + * @property {string} href + */ + +/** + * @typedef {Object} Export + * @property {string} id + * @property {string} displayName + * @property {ExportStatus} status + * @property {StartExport} request + * @property {{mesh: Link}} _links + */ + +/** + * @typedef {Object} ExportResponse + * @property {Export} export + */ /** * Default settings for accessing the iTwin platform. @@ -34,7 +106,7 @@ const ITwin = {}; ITwin.defaultAccessToken = undefined; /** - * Gets or sets the default Google Map Tiles API endpoint. + * Gets or sets the default iTwin API endpoint. * * @type {string|Resource} * @default https://api.bentley.com @@ -43,24 +115,192 @@ ITwin.apiEndpoint = new Resource({ url: "https://api.bentley.com", }); -// TODO: this should only be needed if we have a way to generate really long term access tokens -// to sample data that is accessible to everyone -// ITwin.getDefaultTokenCredit = function (providedKey) { -// if (providedKey !== defaultAccessToken) { -// return undefined; -// } +/** + * @param {string} exportId + */ +ITwin.getExport = async function (exportId) { + if (!defined(ITwin.defaultAccessToken)) { + throw new DeveloperError("Must set ITwin.defaultAccessToken first"); + } + + const headers = { + Authorization: ITwin.defaultAccessToken, + Accept: "application/vnd.bentley.itwin-platform.v1+json", + }; + + // obtain export for specified export id + const url = `${ITwin.apiEndpoint}mesh-export/${exportId}`; + + // TODO: this request is _really_ slow, like 7 whole second alone for me + // Arun said this was kinda normal but to keep track of the `x-correlation-id` of any that take EXTRA long + const response = await fetch(url, { headers }); + if (!response.ok) { + const result = await response.json(); + if (response.status === 401) { + throw new RuntimeError( + `Unauthorized, bad token, wrong scopes or headers bad. ${result.error.details[0].code}`, + ); + } else if (response.status === 404) { + throw new RuntimeError(`Requested export is not available ${exportId}`); + } else if (response.status === 429) { + throw new RuntimeError("Too many requests"); + } + throw new RuntimeError(`Unknown request failure ${response.status}`); + } + + /** @type {ExportResponse} */ + const result = await response.json(); + return result; +}; + +/** + * Get the list of exports for the given iModel + changeset + * + * @param {string} iModelId + * @param {string} changesetId + */ +ITwin.getExports = async function (iModelId, changesetId) { + if (!defined(ITwin.defaultAccessToken)) { + throw new DeveloperError("Must set ITwin.defaultAccessToken first"); + } + + const headers = { + Authorization: ITwin.defaultAccessToken, + Accept: "application/vnd.bentley.itwin-platform.v1+json", + Prefer: "return=representation", // or return=minimal (the default) + }; + + // obtain export for specified export id + let url = `${ITwin.apiEndpoint}mesh-export/?iModelId=${iModelId}`; + if (defined(changesetId) && changesetId !== "") { + url += `&changesetId=${changesetId}`; + } -// if (!defined(defaultTokenCredit)) { -// const defaultTokenMessage = -// ' \ -// This application is using Cesium\'s default ion access token. Please assign Cesium.Ion.defaultAccessToken \ -// with an access token from your ion account before making any Cesium API calls. \ -// You can sign up for a free ion account at https://cesium.com.'; + const response = await fetch(url, { headers }); + if (!response.ok) { + const result = await response.json(); + if (response.status === 401) { + throw new RuntimeError( + `Unauthorized, bad token, wrong scopes or headers bad. ${result.error.details[0].code}`, + ); + } else if (response.status === 422) { + throw new RuntimeError( + `Unprocessable Entity:${result.error.code} ${result.error.message}`, + ); + } else if (response.status === 429) { + throw new RuntimeError("Too many requests"); + } + throw new RuntimeError(`Unknown request failure ${response.status}`); + } + + /** @type {{exports: Export[]}} */ + const result = await response.json(); + return result; +}; + +/** + * Start the export process for the given iModel + changeset. + * + * @param {string} iModelId + * @param {string} changesetId + */ +ITwin.createExportForModelId = async function (iModelId, changesetId) { + if (!defined(ITwin.defaultAccessToken)) { + throw new DeveloperError("Must set ITwin.defaultAccessToken first"); + } + + changesetId = changesetId ?? ""; + + const requestOptions = { + method: "POST", + headers: { + Authorization: ITwin.defaultAccessToken, + Accept: "application/vnd.bentley.itwin-platform.v1+json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + iModelId, + changesetId, + exportType: "CESIUM", + }), + }; + + // initiate mesh export + const response = await fetch( + `https://api.bentley.com/mesh-export/`, + requestOptions, + ); + + if (!response.ok) { + const result = await response.json(); + if (response.status === 401) { + console.error( + result.error.code, + result.error.message, + result.error.details, + ); + throw new RuntimeError( + "Unauthorized, bad token, wrong scopes or headers bad", + ); + } else if (response.status === 403) { + console.error(result.error.code, result.error.message); + throw new RuntimeError("Not allowed, forbidden"); + } else if (response.status === 422) { + console.error(result.error.code, result.error.message); + console.error(result.error.details); + throw new RuntimeError("Unprocessable: Cannot create export job"); + } else if (response.status === 429) { + throw new RuntimeError("Too many requests"); + } + + throw new RuntimeError(`Unknown request failure ${response.status}`); + } + + /** @type {ExportResponse} */ + const result = await response.json(); + return result.export.id; +}; + +/** + * Delete the specified export + * + * TODO: I'm not sure if we want this or not. Might belong better as an APP level function + * I just started creating helpers for all the routes under the `mesh-export` API + * for ease of access during testing + * + * @param {string} exportId + */ +ITwin.deleteExport = async function (exportId) { + if (!defined(ITwin.defaultAccessToken)) { + throw new DeveloperError("Must set ITwin.defaultAccessToken first"); + } + const headers = { + Authorization: ITwin.defaultAccessToken, + Accept: "application/vnd.bentley.itwin-platform.v1+json", + }; -// defaultTokenCredit = new Credit(defaultTokenMessage, true); -// } + // obtain export for specified export id + const url = `${ITwin.apiEndpoint}mesh-export/${exportId}`; -// return defaultTokenCredit; -// }; + const response = await fetch(url, { method: "DELETE", headers }); + if (!response.ok) { + const result = await response.json(); + if (response.status === 401) { + throw new RuntimeError( + `Unauthorized, bad token, wrong scopes or headers bad. ${result.error.details[0].code}`, + ); + } else if (response.status === 404) { + throw new RuntimeError("Export not found"); + } else if (response.status === 422) { + throw new RuntimeError( + `Unprocessable Entity: ${result.error.code} ${result.error.message}`, + ); + } else if (response.status === 429) { + throw new RuntimeError("Too many requests"); + } + throw new RuntimeError(`Unknown request failure ${response.status}`); + } +}; export default ITwin; +export { ExportStatus, ExportType }; diff --git a/packages/engine/Source/Scene/createIModel3DTileset.js b/packages/engine/Source/Scene/createIModel3DTileset.js index 1405aac4e5a2..7bbefa6e0e0f 100644 --- a/packages/engine/Source/Scene/createIModel3DTileset.js +++ b/packages/engine/Source/Scene/createIModel3DTileset.js @@ -1,81 +1,55 @@ import Cesium3DTileset from "./Cesium3DTileset.js"; import defined from "../Core/defined.js"; import Resource from "../Core/Resource.js"; -import ITwin from "../Core/ITwin.js"; +import ITwin, { ExportStatus, ExportType } from "../Core/ITwin.js"; import DeveloperError from "../Core/DeveloperError.js"; +import RuntimeError from "../Core/RuntimeError.js"; function delay(ms) { return new Promise((res) => setTimeout(res, ms)); } /** - * @enum {string} + * @param {Export} exportObj + * @param {Cesium3DTileset.ConstructorOptions} [options] */ -const ExportStatus = Object.freeze({ - NotStarted: "NotStarted", - InProgress: "InProgress", - Complete: "Complete", - Invalid: "Invalid", -}); +async function loadExport(exportObj, options) { + let status = exportObj.status; -/** - * Type of an export currently, only GLTF and 3DFT are documented - * The CESIUM option is what we were told to use with Sandcastle - * I've also seen the IMODEL one but don't know where it's from - * @enum {string} - */ -const ExportType = Object.freeze({ - "3DFT": "3DFT", - GLFT: "GLTF", - IMODEL: "IMODEL", - CESIUM: "CESIUM", -}); + if (exportObj.request.exportType !== ExportType.CESIUM) { + // This is an undocumented value but I think it's the only one we want to load + // TODO: should we even be checking this? + throw new Error(`Wrong export type ${exportObj.request.exportType}`); + } -/** - * @typedef {Object} GeometryOptions - * @property {boolean} includeLines - * @property {number} chordTol - * @property {number} angleTol - * @property {number} decimationTol - * @property {number} maxEdgeLength - * @property {number} minBRepFeatureSize - * @property {number} minLineStyleComponentSize - */ + const timeoutAfter = 300000; + const start = Date.now(); + // wait until the export is complete + while (status !== ExportStatus.Complete) { + await delay(5000); + exportObj = (await ITwin.getExport(exportObj.id)).export; + status = exportObj.status; + console.log(`Export is ${status}`); -/** - * @typedef {Object} ViewDefinitionFilter - * @property {string[]} models Array of included model IDs. - * @property {string[]} categories Array of included category IDs. - * @property {string[]} neverDrawn Array of element IDs to filter out. - */ + if (Date.now() - start > timeoutAfter) { + throw new RuntimeError("Export did not complete in time."); + } + } -/** - * @typedef {Object} StartExport - * @property {string} iModelId - * @property {string} changesetId - * @property {ExportType} exportType Type of mesh to create. Currently, only GLTF and 3DFT are supported and undocumented CESIUM option - * @property {GeometryOptions} geometryOptions - * @property {ViewDefinitionFilter} viewDefinitionFilter - */ + // This link is only valid 1 hour + let tilesetUrl = exportObj._links.mesh.href; + const splitStr = tilesetUrl.split("?"); + // is there a cleaner way to do this? + tilesetUrl = `${splitStr[0]}/tileset.json?${splitStr[1]}`; -/** - * @typedef {Object} Link - * @property {string} href - */ + const resource = new Resource({ + url: tilesetUrl, + }); -/** - * @typedef {Object} Export - * @property {string} id - * @property {string} displayName - * @property {ExportStatus} status - * @property {StartExport} request - * @property {{mesh: Link}} _links - */ + return Cesium3DTileset.fromUrl(resource, options); +} -/** - * @typedef {Object} ExportResponse - * @property {Export} export - */ +const createIModel3DTileset = {}; /** * Creates a {@link Cesium3DTileset} instance for the Google Photorealistic 3D Tiles tileset. @@ -101,271 +75,53 @@ const ExportType = Object.freeze({ * console.log(`Error creating tileset: ${error}`); * } */ -async function createIModel3DTileset(exportId, options) { +createIModel3DTileset.fromExportId = async function (exportId, options) { if (!defined(ITwin.defaultAccessToken)) { throw new DeveloperError("Must set ITwin.defaultAccessToken first"); } options = options ?? {}; - const timeoutAfter = 300000; - const start = Date.now(); - let result = await createIModel3DTileset.getExport(exportId); - let status = result.export.status; - - if (result.export.request.exportType !== ExportType.CESIUM) { - // This is an undocumented value but I think it's the only one we want to load - // TODO: should we even be checking this? - throw new Error(`Wrong export type ${result.export.request.exportType}`); - } - - // wait until the export is complete - while (status !== ExportStatus.Complete) { - await delay(5000); - result = await createIModel3DTileset.getExport(exportId); - status = result.export.status; - console.log(`Export is ${status}`); - - if (Date.now() - start > timeoutAfter) { - throw new Error("Export did not complete in time."); - } - } - - // This link is only valid 1 hour - let tilesetUrl = result.export._links.mesh.href; - const splitStr = tilesetUrl.split("?"); - // is there a cleaner way to do this? - tilesetUrl = `${splitStr[0]}/tileset.json?${splitStr[1]}`; - - const resource = new Resource({ - url: tilesetUrl, - }); - - return Cesium3DTileset.fromUrl(resource, options); -} - -/** - * @param {string} exportId - */ -createIModel3DTileset.getExport = async function (exportId) { - const headers = { - Authorization: ITwin.defaultAccessToken, - Accept: "application/vnd.bentley.itwin-platform.v1+json", - }; - - // obtain export for specified export id - const url = `${ITwin.apiEndpoint}mesh-export/${exportId}`; - - // TODO: this request is _really_ slow, like 7 whole second alone for me - // Arun said this was kinda normal but to keep track of the `x-correlation-id` of any that take EXTRA long - const response = await fetch(url, { headers }); - if (!response.ok) { - const result = await response.json(); - if (response.status === 401) { - throw new Error( - `Unauthorized, bad token, wrong scopes or headers bad. ${result.error.details[0].code}`, - ); - } else if (response.status === 404) { - throw new Error(`Requested export is not available ${exportId}`); - } else if (response.status === 429) { - throw new Error("Too many requests"); - } - throw new Error(`Unknown request failure ${response.status}`); - } - - /** @type {ExportResponse} */ - const result = await response.json(); - return result; -}; - -/** - * Get the list of exports for the given iModel + changeset - * - * @param {string} iModelId - * @param {string} changesetId - */ -createIModel3DTileset.getExports = async function (iModelId, changesetId) { - if (!defined(ITwin.defaultAccessToken)) { - throw new DeveloperError("Must set ITwin.defaultAccessToken first"); - } - const headers = { - Authorization: ITwin.defaultAccessToken, - Accept: "application/vnd.bentley.itwin-platform.v1+json", - Prefer: "return=representation", // or return=minimal (the default) - }; - - // obtain export for specified export id - let url = `${ITwin.apiEndpoint}mesh-export/?iModelId=${iModelId}`; - if (defined(changesetId) && changesetId !== "") { - url += `&changesetId=${changesetId}`; - } - - const response = await fetch(url, { headers }); - if (!response.ok) { - const result = await response.json(); - if (response.status === 401) { - throw new Error( - `Unauthorized, bad token, wrong scopes or headers bad. ${result.error.details[0].code}`, - ); - } else if (response.status === 422) { - throw new Error( - `Unprocessable Entity:${result.error.code} ${result.error.message}`, - ); - } else if (response.status === 429) { - throw new Error("Too many requests"); - } - throw new Error(`Unknown request failure ${response.status}`); - } - - /** @type {{exports: Export[]}} */ - const result = await response.json(); - return result; + const result = await ITwin.getExport(exportId); + const tileset = await loadExport(result.export, options); + return tileset; }; /** * Check the exports for the given iModel + changeset combination for any that - * have the desired CESIUM type and return that one + * have the desired CESIUM type and returns the first one that matches as a new tileset. * - * @param {string} iModelId - * @param {string} changesetId - */ -createIModel3DTileset.checkForCesiumExport = async function ( - iModelId, - changesetId, -) { - const { exports } = await createIModel3DTileset.getExports( - iModelId, - changesetId, - ); - const cesiumExport = exports.find( - (e) => e.request.exportType === ExportType.CESIUM, - ); - return cesiumExport; -}; - -/** - * Start the export process for the given iModel + changeset. + * If there is not a CESIUM export you can create it using {@link ITwin.createExportForModelId} * - * Pair this with the {@link checkForCesiumExport} function to avoid creating extra exports + * This function assumes one export per type per "iModel id + changeset id". If you need to create + * multiple exports per "iModel id + changeset id" you should switch to using {@link createIModel3DTileset} + * with the export id directly * * @example - * const cesiumExport = await Cesium.createIModel3DTileset.checkForCesiumExport(imodelId, changesetId); - * let exportId = cesiumExport?.id; - * if (!Cesium.defined(cesiumExport)) { - * exportId = await Cesium.createIModel3DTileset.createExportForModelId( - * imodelId, - * changesetId, - * accessToken, - * ); + * // Try to load the corresponding tileset export or create it if it doesn't exist + * let tileset = await Cesium.createIModel3DTileset.fromModelIdimodelId, changesetId); + * if (!Cesium.defined(tileset)) { + * const exportId = await Cesium.ITwin.createExportForModelId(imodelId, changesetId, accessToken); + * tileset = await Cesium.createIModel3DTileset(exportId); * } * * @param {string} iModelId * @param {string} changesetId + * @param {Cesium3DTileset.ConstructorOptions} options */ -createIModel3DTileset.createExportForModelId = async function ( +createIModel3DTileset.fromModelId = async function ( iModelId, changesetId, + options, ) { - if (!defined(ITwin.defaultAccessToken)) { - throw new DeveloperError("Must set ITwin.defaultAccessToken first"); - } - - console.log("Start Export"); - - changesetId = changesetId ?? ""; - - const requestOptions = { - method: "POST", - headers: { - Authorization: ITwin.defaultAccessToken, - Accept: "application/vnd.bentley.itwin-platform.v1+json", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - iModelId, - changesetId, - exportType: "CESIUM", - }), - }; - - // initiate mesh export - const response = await fetch( - `https://api.bentley.com/mesh-export/`, - requestOptions, + const { exports } = await ITwin.getExports(iModelId, changesetId); + const cesiumExport = exports.find( + (exportObj) => exportObj.request?.exportType === ExportType.CESIUM, ); - - if (!response.ok) { - const result = await response.json(); - if (response.status === 401) { - console.error("Unauthorized, bad token, wrong scopes or headers bad"); - console.error( - result.error.code, - result.error.message, - result.error.details, - ); - } else if (response.status === 403) { - console.error("Not allowed, forbidden"); - console.error(result.error.code, result.error.message); - } else if (response.status === 422) { - console.error("Unprocessable: Cannot create export job"); - console.error(result.error.code, result.error.message); - console.error(result.error.details); - } else if (response.status === 429) { - console.log( - "Too many requests, retry after:", - response.headers.get("retry-after"), - ); - console.error(result.error.code, result.error.message); - } else { - console.error("Bad request, unknown error", response); - } - return undefined; - } - - /** @type {ExportResponse} */ - const result = await response.json(); - return result.export.id; -}; - -/** - * Delete the specified export - * - * TODO: I'm not sure if we want this or not. Might belong better as an APP level function - * I just started creating helpers for all the routes under the `mesh-export` API - * for ease of access during testing - * - * @param {string} exportId - */ -createIModel3DTileset.deleteExport = async function (exportId) { - if (!defined(ITwin.defaultAccessToken)) { - throw new DeveloperError("Must set ITwin.defaultAccessToken first"); - } - const headers = { - Authorization: ITwin.defaultAccessToken, - Accept: "application/vnd.bentley.itwin-platform.v1+json", - }; - - // obtain export for specified export id - const url = `${ITwin.apiEndpoint}mesh-export/${exportId}`; - - const response = await fetch(url, { method: "DELETE", headers }); - if (!response.ok) { - const result = await response.json(); - if (response.status === 401) { - throw new Error( - `Unauthorized, bad token, wrong scopes or headers bad. ${result.error.details[0].code}`, - ); - } else if (response.status === 404) { - throw new Error("Export not found"); - } else if (response.status === 422) { - throw new Error( - `Unprocessable Entity:${result.error.code} ${result.error.message}`, - ); - } else if (response.status === 429) { - throw new Error("Too many requests"); - } - throw new Error(`Unknown request failure ${response.status}`); + if (!defined(cesiumExport)) { + return; } + return loadExport(cesiumExport, options); }; export default createIModel3DTileset; From 4ea390891d356d89f47639bebd07e65dff34644a Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:09:02 -0500 Subject: [PATCH 03/22] small adjustments --- packages/engine/Source/Core/ITwin.js | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/packages/engine/Source/Core/ITwin.js b/packages/engine/Source/Core/ITwin.js index ab74ad0c5536..e7d49d3c59c1 100644 --- a/packages/engine/Source/Core/ITwin.js +++ b/packages/engine/Source/Core/ITwin.js @@ -21,7 +21,7 @@ const ExportStatus = Object.freeze({ */ const ExportType = Object.freeze({ "3DFT": "3DFT", - GLFT: "GLTF", + GLTF: "GLTF", IMODEL: "IMODEL", CESIUM: "CESIUM", }); @@ -75,17 +75,7 @@ const ExportType = Object.freeze({ /** * Default settings for accessing the iTwin platform. * - * Keys can be created using the iModels share routes {@link https://developer.bentley.com/apis/imodels-v2/operations/create-imodel-share/} - * - * An ion access token is only required if you are using any ion related APIs. - * A default access token is provided for evaluation purposes only. - * Sign up for a free ion account and get your own access token at {@link https://cesium.com} - * - * @see IonResource - * @see IonImageryProvider - * @see IonGeocoderService - * @see createWorldImagery - * @see createWorldTerrain + * @see createIModel3DTileset * @namespace ITwin */ const ITwin = {}; @@ -99,7 +89,7 @@ const ITwin = {}; * `mesh-export:read` for loading meshes GET /mesh-export(s) * `mesh-export:modify` if we want to include a function to create an export * `itwin-platform` if we want to use the iModel shares ourselves GET /imodels/{id}/shares - * + * Seems the `itwin-platform` scope should apply to everything but the docs are a little unclear * * @type {string|undefined} */ @@ -157,7 +147,7 @@ ITwin.getExport = async function (exportId) { * Get the list of exports for the given iModel + changeset * * @param {string} iModelId - * @param {string} changesetId + * @param {string} [changesetId] */ ITwin.getExports = async function (iModelId, changesetId) { if (!defined(ITwin.defaultAccessToken)) { @@ -202,7 +192,7 @@ ITwin.getExports = async function (iModelId, changesetId) { * Start the export process for the given iModel + changeset. * * @param {string} iModelId - * @param {string} changesetId + * @param {string} [changesetId] */ ITwin.createExportForModelId = async function (iModelId, changesetId) { if (!defined(ITwin.defaultAccessToken)) { @@ -227,7 +217,7 @@ ITwin.createExportForModelId = async function (iModelId, changesetId) { // initiate mesh export const response = await fetch( - `https://api.bentley.com/mesh-export/`, + `${ITwin.apiEndpoint}mesh-export/`, requestOptions, ); From d779bc186441d9661a1e64fa4e08c81bd7abab45 Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Mon, 11 Nov 2024 14:25:55 -0500 Subject: [PATCH 04/22] oauth testing web app and service app --- .prettierignore | 1 + Apps/Sandcastle/gallery/iTwin Demo.html | 80 ++++++++++++++++- eslint.config.js | 8 ++ itwin-oauth-demo/.gitignore | 1 + itwin-oauth-demo/index.html | 34 +++++++ itwin-oauth-demo/server.js | 114 ++++++++++++++++++++++++ packages/engine/Source/Core/ITwin.js | 5 +- 7 files changed, 238 insertions(+), 5 deletions(-) create mode 100644 itwin-oauth-demo/.gitignore create mode 100644 itwin-oauth-demo/index.html create mode 100644 itwin-oauth-demo/server.js diff --git a/.prettierignore b/.prettierignore index c46eee96cb50..e0d51a3709d0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,6 +11,7 @@ !packages/**/ !Specs/**/ !Tools/**/ +!itwin-oauth-demo/**/ !**/*.js !**/*.cjs diff --git a/Apps/Sandcastle/gallery/iTwin Demo.html b/Apps/Sandcastle/gallery/iTwin Demo.html index d8d4865927e2..ef4991471367 100644 --- a/Apps/Sandcastle/gallery/iTwin Demo.html +++ b/Apps/Sandcastle/gallery/iTwin Demo.html @@ -30,15 +30,87 @@ window.startup = async function (Cesium) { "use strict"; //Sandcastle_Begin + const popup = (url) => { + // based on https://gist.github.com/gauravtiwari/2ae9f44aee281c759fe5a66d5c2721a2 + const windowArea = { + width: 500, + height: 600, + }; + + windowArea.left = Math.floor( + window.screenX + (window.outerWidth - windowArea.width) / 2, + ); + windowArea.top = Math.floor( + window.screenY + (window.outerHeight - windowArea.height) / 8, + ); + + const sep = url.indexOf("?") !== -1 ? "&" : "?"; + const fullUrl = `${url}${sep}`; + const windowOpts = `toolbar=0,scrollbars=1,status=1,resizable=1,location=1,menuBar=0, + width=${windowArea.width},height=${windowArea.height}, + left=${windowArea.left},top=${windowArea.top}`; + + const authWindow = window.open(fullUrl, "popup", windowOpts); + + // Listen to message from child window + const authPromise = new Promise((resolve, reject) => { + window.addEventListener( + "message", + (e) => { + console.log("message", e); + // TODO: if we go this route we may want this back to make sure + // it's coming from wherever we host this + // if (e.origin !== window.SITE_DOMAIN) { + // authWindow.close(); + // reject("Not allowed"); + // } + + if (e.data.auth) { + resolve(e.data.auth); + authWindow.close(); + } else { + authWindow.close(); + reject("Unauthorised"); + } + }, + false, + ); + }); + return authPromise; + }; + + // ===== Web app oauth flow ===== + + // const clientId = "webapp-0MsbOTn56gKXMoI1WsJ0SoVEn"; + // const redirectUri = "http://localhost:3000"; + + // const result = await popup( + // `https://ims.bentley.com/connect/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&scope=itwin-platform`, + // ).catch((error) => { + // console.error(error); + // throw new Error("OAuth failed"); + // }); + // const accessCode = result.code; + // console.log("popup returned", result); + + // ===== Service app request ===== + + const serviceResponse = await fetch("http://localhost:3000/service"); + const serviceResult = await serviceResponse.json(); + const { token: accessCode } = serviceResult; + // must be created for the Start export route if you want to create new exports // Needs to have the mesh-export:modify scope not just mesh-export:read - const accessToken = + let accessToken = "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkJlbnRsZXlJTVNfMjAyNCIsInBpLmF0bSI6ImE4bWUifQ.eyJzY29wZSI6WyJtZXNoLWV4cG9ydDpyZWFkIiwibWVzaC1leHBvcnQ6bW9kaWZ5Il0sImNsaWVudF9pZCI6Iml0d2luLWRldmVsb3Blci1jb25zb2xlIiwiYXVkIjpbImh0dHBzOi8vaW1zLmJlbnRsZXkuY29tL2FzL3Rva2VuLm9hdXRoMiIsImh0dHBzOi8vaW1zb2lkYy5iZW50bGV5LmNvbS9hcy90b2tlbi5vYXV0aDIiLCJodHRwczovL2ltc29pZGMuYmVudGxleS5jb20vcmVzb3VyY2VzIiwiYmVudGxleS1hcGktbWFuYWdlbWVudCJdLCJzdWIiOiJjMWM1MzRhNy0zZDk2LTQ2MzMtYjY5ZC1jMGEzNzE5OWQwZGUiLCJyb2xlIjoiQkVOVExFWV9FTVBMT1lFRSIsIm9yZyI6ImZhYjk3NzRiLWIzMzgtNGNjMi1hNmM5LTQ1OGJkZjdmOTY2YSIsInN1YmplY3QiOiJjMWM1MzRhNy0zZDk2LTQ2MzMtYjY5ZC1jMGEzNzE5OWQwZGUiLCJpc3MiOiJodHRwczovL2ltcy5iZW50bGV5LmNvbSIsImVudGl0bGVtZW50IjpbIkJFTlRMRVlfTEVBUk4iLCJJTlRFUk5BTCIsIlNFTEVDVF8yMDA2IiwiQkVOIiwiQkROIl0sInByZWZlcnJlZF91c2VybmFtZSI6Ikpvc2guUm91emVyQGJlbnRsZXkuY29tIiwiZ2l2ZW5fbmFtZSI6Ikpvc2giLCJzaWQiOiJHMGZTamlOXzBMRjE1Vy1IYnRTaWtoeDNuSXMuU1UxVExVSmxiblJzWlhrdFZWTS5LbWlWLlF2UExVcGJyc2R3engwWGxPbkF3eTFTT0IiLCJuYmYiOjE3MzA4MzgyNDIsInVsdGltYXRlX3NpdGUiOiIxMDAxMzg5MTE3IiwidXNhZ2VfY291bnRyeV9pc28iOiJVUyIsImF1dGhfdGltZSI6MTczMDgzODU0MiwibmFtZSI6Ikpvc2guUm91emVyQGJlbnRsZXkuY29tIiwib3JnX25hbWUiOiJCZW50bGV5IFN5c3RlbXMgSW5jIiwiZmFtaWx5X25hbWUiOiJSb3V6ZXIiLCJlbWFpbCI6Ikpvc2guUm91emVyQGJlbnRsZXkuY29tIiwiZXhwIjoxNzMwODQyMTQyfQ.N_WrgjL2bqxdNLEM5nHh4Fg-FzeA-qxxpryaoaMKz8onnpgzmp-X8dyQ1TyMRqKQY99iLypE9bU45DwRJs7vBgNQ73d53SkC9TKGYn8AAOTJz8c_oEHgkoTEDaaLsMPCw88tqZ34pY0e0oIHIofVDTCvwlzwaJPADfkIxz-8GzhIt2WrXR7f6LWBFSlWNrtNhIr9Tz7UNaLOh97_3fS9KVU1I084CpBga9cj_mjGBeki7mIEQvpqMj8x2bJPae_c6WrPEjKLayrOzq4q0X0Kvle0ZFAm-Se9MFCusTFS51bYrI3IsjagGeIP2U06zzcMJ22NylomE60hz6GK_4uB3g"; + + accessToken = `Bearer ${accessCode}`; + Cesium.ITwin.defaultAccessToken = accessToken; // this is the iModel in the "Hello iTwinCesium" iTwin that we should all have access to // https://developer.bentley.com/my-itwins/b4a30036-0456-49ea-a439-3fcd9365e24e/home/ - const imodelId = "2852c3d7-00c3-4b5d-a0ce-82bbde4f061e"; - // const imodelId = "88673c1d-12b8-48f1-8beb-5000d0edbd0b"; + // const imodelId = "2852c3d7-00c3-4b5d-a0ce-82bbde4f061e"; + const imodelId = "88673c1d-12b8-48f1-8beb-5000d0edbd0b"; const changesetId = ""; @@ -107,7 +179,7 @@ const subcategory = feature.getProperty("subcategory"); const message = ` Element ID: ${element} - Subcategory: ${classes[subcategory] ?? subcategory} + Subcategory: ${classes[subcategory] || subcategory} Feature ID: ${feature.featureId}`; nameOverlay.textContent = message; } diff --git a/eslint.config.js b/eslint.config.js index c8ec3d9bd70c..84dd74b032c4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -103,4 +103,12 @@ export default [ "n/no-missing-import": "off", }, }, + { + files: ["itwin-oauth-demo/*"], + languageOptions: { + ...configCesium.configs.node.languageOptions, + sourceType: "module", + ecmaVersion: 2022, + }, + }, ]; diff --git a/itwin-oauth-demo/.gitignore b/itwin-oauth-demo/.gitignore new file mode 100644 index 000000000000..d344ba6b06cb --- /dev/null +++ b/itwin-oauth-demo/.gitignore @@ -0,0 +1 @@ +config.json diff --git a/itwin-oauth-demo/index.html b/itwin-oauth-demo/index.html new file mode 100644 index 000000000000..af6bc98d6499 --- /dev/null +++ b/itwin-oauth-demo/index.html @@ -0,0 +1,34 @@ + + + + + + Auth! + + + + + diff --git a/itwin-oauth-demo/server.js b/itwin-oauth-demo/server.js new file mode 100644 index 000000000000..790b96c9958a --- /dev/null +++ b/itwin-oauth-demo/server.js @@ -0,0 +1,114 @@ +import express from "express"; +import { readFileSync, writeFileSync } from "fs"; +import { dirname, join } from "path"; +import { exit } from "process"; +import { fileURLToPath } from "url"; + +let config = { + webapp: { + clientId: "", + clientSecret: "", + }, + serviceapp: { + clientId: "", + clientSecret: "", + }, + port: 3000, + redirectUri: "http://localhost:3000", +}; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const configPath = join(__dirname, "./config.json"); +try { + const configFile = readFileSync(configPath, { encoding: "utf-8" }); + config = JSON.parse(configFile); +} catch { + console.log("config file missing, default written to", configPath); + console.log("Please update the config with the desired values"); + writeFileSync(configPath, JSON.stringify(config, undefined, 2)); + exit(1); +} + +const app = express(); +const port = config.port ?? 3000; +const redirectUri = config.redirectUri ?? "http://localhost:3000"; + +// eslint-disable-next-line no-unused-vars +app.get("/", async (req, res) => { + res.sendFile(join(__dirname, "./index.html")); +}); + +app.get("/token", async (req, res) => { + console.log("/token request received"); + const { code } = req.query; + + if (!code) { + res.status(404).send("Code missing"); + } + + const body = new URLSearchParams(); + body.set("grant_type", "authorization_code"); + body.set("client_id", config.webapp.clientId); + body.set("client_secret", config.webapp.clientSecret); + body.set("code", code); + body.set("redirect_uri", redirectUri); + + const response = await fetch("https://ims.bentley.com/connect/token", { + method: "POST", + body, + }); + + const result = await response.json(); + + if (!response.ok || !result) { + console.log(" bad response/no result"); + res.status(response.status).send(); + return; + } + const { access_token } = result; + if (access_token) { + console.log(" token acquired, returned"); + res.status(200).send({ token: access_token }); + return; + } + console.log(" token not found"); + res.status(404).send("token not found"); +}); + +// eslint-disable-next-line no-unused-vars +app.get("/service", async (req, res) => { + console.log("/service request received"); + + const body = new URLSearchParams(); + body.set("grant_type", "client_credentials"); + body.set("client_id", config.serviceapp.clientId); + body.set("client_secret", config.serviceapp.clientSecret); + body.set("scope", "itwin-platform"); + + const response = await fetch("https://ims.bentley.com/connect/token", { + method: "POST", + body, + }); + + const result = await response.json(); + + res.setHeader("Access-Control-Allow-Origin", "*"); + + if (!response.ok || !result) { + console.log(" bad response/no result"); + res.status(response.status).send(); + return; + } + const { access_token } = result; + if (access_token) { + console.log(" token acquired, returned"); + res.status(200).send({ token: access_token }); + return; + } + console.log(" token not found"); + res.status(404).send("token not found"); +}); + +app.listen(port, () => { + console.log(`Server listening on port ${port}`); +}); diff --git a/packages/engine/Source/Core/ITwin.js b/packages/engine/Source/Core/ITwin.js index e7d49d3c59c1..e3ed43b1ee68 100644 --- a/packages/engine/Source/Core/ITwin.js +++ b/packages/engine/Source/Core/ITwin.js @@ -161,7 +161,7 @@ ITwin.getExports = async function (iModelId, changesetId) { }; // obtain export for specified export id - let url = `${ITwin.apiEndpoint}mesh-export/?iModelId=${iModelId}`; + let url = `${ITwin.apiEndpoint}mesh-export/?iModelId=${iModelId}&exportType=CESIUM&$top=1`; if (defined(changesetId) && changesetId !== "") { url += `&changesetId=${changesetId}`; } @@ -173,6 +173,9 @@ ITwin.getExports = async function (iModelId, changesetId) { throw new RuntimeError( `Unauthorized, bad token, wrong scopes or headers bad. ${result.error.details[0].code}`, ); + } else if (response.status === 403) { + console.error(result.error.code, result.error.message); + throw new RuntimeError("Not allowed, forbidden"); } else if (response.status === 422) { throw new RuntimeError( `Unprocessable Entity:${result.error.code} ${result.error.message}`, From 50dfe46a90342611a1fc5c5d52ceb50fcfd54496 Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:18:25 -0500 Subject: [PATCH 05/22] small cleanup and removing code we won't use --- Apps/Sandcastle/gallery/iTwin Demo.html | 103 +----------------- itwin-oauth-demo/server.js | 48 -------- packages/engine/Source/Core/ITwin.js | 67 ++++-------- .../Source/Scene/createIModel3DTileset.js | 26 ++--- 4 files changed, 33 insertions(+), 211 deletions(-) diff --git a/Apps/Sandcastle/gallery/iTwin Demo.html b/Apps/Sandcastle/gallery/iTwin Demo.html index ef4991471367..eeea23b17f51 100644 --- a/Apps/Sandcastle/gallery/iTwin Demo.html +++ b/Apps/Sandcastle/gallery/iTwin Demo.html @@ -30,95 +30,17 @@ window.startup = async function (Cesium) { "use strict"; //Sandcastle_Begin - const popup = (url) => { - // based on https://gist.github.com/gauravtiwari/2ae9f44aee281c759fe5a66d5c2721a2 - const windowArea = { - width: 500, - height: 600, - }; - - windowArea.left = Math.floor( - window.screenX + (window.outerWidth - windowArea.width) / 2, - ); - windowArea.top = Math.floor( - window.screenY + (window.outerHeight - windowArea.height) / 8, - ); - - const sep = url.indexOf("?") !== -1 ? "&" : "?"; - const fullUrl = `${url}${sep}`; - const windowOpts = `toolbar=0,scrollbars=1,status=1,resizable=1,location=1,menuBar=0, - width=${windowArea.width},height=${windowArea.height}, - left=${windowArea.left},top=${windowArea.top}`; - - const authWindow = window.open(fullUrl, "popup", windowOpts); - - // Listen to message from child window - const authPromise = new Promise((resolve, reject) => { - window.addEventListener( - "message", - (e) => { - console.log("message", e); - // TODO: if we go this route we may want this back to make sure - // it's coming from wherever we host this - // if (e.origin !== window.SITE_DOMAIN) { - // authWindow.close(); - // reject("Not allowed"); - // } - - if (e.data.auth) { - resolve(e.data.auth); - authWindow.close(); - } else { - authWindow.close(); - reject("Unauthorised"); - } - }, - false, - ); - }); - return authPromise; - }; - - // ===== Web app oauth flow ===== - - // const clientId = "webapp-0MsbOTn56gKXMoI1WsJ0SoVEn"; - // const redirectUri = "http://localhost:3000"; - - // const result = await popup( - // `https://ims.bentley.com/connect/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&scope=itwin-platform`, - // ).catch((error) => { - // console.error(error); - // throw new Error("OAuth failed"); - // }); - // const accessCode = result.code; - // console.log("popup returned", result); - - // ===== Service app request ===== - const serviceResponse = await fetch("http://localhost:3000/service"); - const serviceResult = await serviceResponse.json(); - const { token: accessCode } = serviceResult; - - // must be created for the Start export route if you want to create new exports - // Needs to have the mesh-export:modify scope not just mesh-export:read - let accessToken = - "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkJlbnRsZXlJTVNfMjAyNCIsInBpLmF0bSI6ImE4bWUifQ.eyJzY29wZSI6WyJtZXNoLWV4cG9ydDpyZWFkIiwibWVzaC1leHBvcnQ6bW9kaWZ5Il0sImNsaWVudF9pZCI6Iml0d2luLWRldmVsb3Blci1jb25zb2xlIiwiYXVkIjpbImh0dHBzOi8vaW1zLmJlbnRsZXkuY29tL2FzL3Rva2VuLm9hdXRoMiIsImh0dHBzOi8vaW1zb2lkYy5iZW50bGV5LmNvbS9hcy90b2tlbi5vYXV0aDIiLCJodHRwczovL2ltc29pZGMuYmVudGxleS5jb20vcmVzb3VyY2VzIiwiYmVudGxleS1hcGktbWFuYWdlbWVudCJdLCJzdWIiOiJjMWM1MzRhNy0zZDk2LTQ2MzMtYjY5ZC1jMGEzNzE5OWQwZGUiLCJyb2xlIjoiQkVOVExFWV9FTVBMT1lFRSIsIm9yZyI6ImZhYjk3NzRiLWIzMzgtNGNjMi1hNmM5LTQ1OGJkZjdmOTY2YSIsInN1YmplY3QiOiJjMWM1MzRhNy0zZDk2LTQ2MzMtYjY5ZC1jMGEzNzE5OWQwZGUiLCJpc3MiOiJodHRwczovL2ltcy5iZW50bGV5LmNvbSIsImVudGl0bGVtZW50IjpbIkJFTlRMRVlfTEVBUk4iLCJJTlRFUk5BTCIsIlNFTEVDVF8yMDA2IiwiQkVOIiwiQkROIl0sInByZWZlcnJlZF91c2VybmFtZSI6Ikpvc2guUm91emVyQGJlbnRsZXkuY29tIiwiZ2l2ZW5fbmFtZSI6Ikpvc2giLCJzaWQiOiJHMGZTamlOXzBMRjE1Vy1IYnRTaWtoeDNuSXMuU1UxVExVSmxiblJzWlhrdFZWTS5LbWlWLlF2UExVcGJyc2R3engwWGxPbkF3eTFTT0IiLCJuYmYiOjE3MzA4MzgyNDIsInVsdGltYXRlX3NpdGUiOiIxMDAxMzg5MTE3IiwidXNhZ2VfY291bnRyeV9pc28iOiJVUyIsImF1dGhfdGltZSI6MTczMDgzODU0MiwibmFtZSI6Ikpvc2guUm91emVyQGJlbnRsZXkuY29tIiwib3JnX25hbWUiOiJCZW50bGV5IFN5c3RlbXMgSW5jIiwiZmFtaWx5X25hbWUiOiJSb3V6ZXIiLCJlbWFpbCI6Ikpvc2guUm91emVyQGJlbnRsZXkuY29tIiwiZXhwIjoxNzMwODQyMTQyfQ.N_WrgjL2bqxdNLEM5nHh4Fg-FzeA-qxxpryaoaMKz8onnpgzmp-X8dyQ1TyMRqKQY99iLypE9bU45DwRJs7vBgNQ73d53SkC9TKGYn8AAOTJz8c_oEHgkoTEDaaLsMPCw88tqZ34pY0e0oIHIofVDTCvwlzwaJPADfkIxz-8GzhIt2WrXR7f6LWBFSlWNrtNhIr9Tz7UNaLOh97_3fS9KVU1I084CpBga9cj_mjGBeki7mIEQvpqMj8x2bJPae_c6WrPEjKLayrOzq4q0X0Kvle0ZFAm-Se9MFCusTFS51bYrI3IsjagGeIP2U06zzcMJ22NylomE60hz6GK_4uB3g"; - - accessToken = `Bearer ${accessCode}`; + const { token } = await serviceResponse.json(); + const accessToken = `Bearer ${token}`; Cesium.ITwin.defaultAccessToken = accessToken; // this is the iModel in the "Hello iTwinCesium" iTwin that we should all have access to // https://developer.bentley.com/my-itwins/b4a30036-0456-49ea-a439-3fcd9365e24e/home/ // const imodelId = "2852c3d7-00c3-4b5d-a0ce-82bbde4f061e"; const imodelId = "88673c1d-12b8-48f1-8beb-5000d0edbd0b"; - const changesetId = ""; - // const knownExportId = undefined; - // const knownExportId = "ab9953b2-bc8e-48ac-a5b0-5d43d68593e8"; - - const delay = (ms) => new Promise((res) => setTimeout(res, ms)); - // Grabbed mapping from the iTwin Viewer const classes = { 2199023255632: "Building Roof", @@ -196,24 +118,7 @@ const statusOutput = document.querySelector("#status"); async function init() { - // TODO: just for testing - // await Cesium.ITwin.deleteExport("8bd9c52e-b379-4f7d-bf39-c45954030b26"); - // console.log(await Cesium.ITwin.getExports(imodelId)); - statusOutput.innerText = "Starting export"; - // const exportId = - // knownExportId ?? - // (await Cesium.ITwin.createExportForModelId( - // imodelId, - // "", - // accessToken, - // )); - - // if (!exportId) { - // console.error("No export id returned"); - // return; - // } - // console.log("Using export id", exportId); const start = Date.now(); @@ -224,6 +129,8 @@ changesetId, ); if (!Cesium.defined(tileset)) { + // TODO: this is temporary, we should not have to call the Start Export route ever after + // auto generation is set up statusOutput.innerText = "Starting export"; const exportId = await Cesium.ITwin.createExportForModelId( imodelId, @@ -234,7 +141,6 @@ tileset = await Cesium.createIModel3DTileset.fromExportId(exportId); } - // const tileset = await Cesium.createIModel3DTileset(exportId); scene.primitives.add(tileset); tileset.colorBlendMode = Cesium.Cesium3DTileColorBlendMode.REPLACE; @@ -267,7 +173,6 @@ } console.error(error); }); - //Sandcastle_End Sandcastle.finishedLoading(); }; diff --git a/itwin-oauth-demo/server.js b/itwin-oauth-demo/server.js index 790b96c9958a..8a935343622e 100644 --- a/itwin-oauth-demo/server.js +++ b/itwin-oauth-demo/server.js @@ -5,16 +5,11 @@ import { exit } from "process"; import { fileURLToPath } from "url"; let config = { - webapp: { - clientId: "", - clientSecret: "", - }, serviceapp: { clientId: "", clientSecret: "", }, port: 3000, - redirectUri: "http://localhost:3000", }; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -31,49 +26,6 @@ try { const app = express(); const port = config.port ?? 3000; -const redirectUri = config.redirectUri ?? "http://localhost:3000"; - -// eslint-disable-next-line no-unused-vars -app.get("/", async (req, res) => { - res.sendFile(join(__dirname, "./index.html")); -}); - -app.get("/token", async (req, res) => { - console.log("/token request received"); - const { code } = req.query; - - if (!code) { - res.status(404).send("Code missing"); - } - - const body = new URLSearchParams(); - body.set("grant_type", "authorization_code"); - body.set("client_id", config.webapp.clientId); - body.set("client_secret", config.webapp.clientSecret); - body.set("code", code); - body.set("redirect_uri", redirectUri); - - const response = await fetch("https://ims.bentley.com/connect/token", { - method: "POST", - body, - }); - - const result = await response.json(); - - if (!response.ok || !result) { - console.log(" bad response/no result"); - res.status(response.status).send(); - return; - } - const { access_token } = result; - if (access_token) { - console.log(" token acquired, returned"); - res.status(200).send({ token: access_token }); - return; - } - console.log(" token not found"); - res.status(404).send("token not found"); -}); // eslint-disable-next-line no-unused-vars app.get("/service", async (req, res) => { diff --git a/packages/engine/Source/Core/ITwin.js b/packages/engine/Source/Core/ITwin.js index e3ed43b1ee68..51a89e15668d 100644 --- a/packages/engine/Source/Core/ITwin.js +++ b/packages/engine/Source/Core/ITwin.js @@ -20,10 +20,9 @@ const ExportStatus = Object.freeze({ * @enum {string} */ const ExportType = Object.freeze({ - "3DFT": "3DFT", - GLTF: "GLTF", IMODEL: "IMODEL", CESIUM: "CESIUM", + "3DTILES": "3DTILES", }); /** @@ -75,6 +74,8 @@ const ExportType = Object.freeze({ /** * Default settings for accessing the iTwin platform. * + * @experimental + * * @see createIModel3DTileset * @namespace ITwin */ @@ -91,6 +92,8 @@ const ITwin = {}; * `itwin-platform` if we want to use the iModel shares ourselves GET /imodels/{id}/shares * Seems the `itwin-platform` scope should apply to everything but the docs are a little unclear * + * @experimental + * * @type {string|undefined} */ ITwin.defaultAccessToken = undefined; @@ -98,6 +101,8 @@ ITwin.defaultAccessToken = undefined; /** * Gets or sets the default iTwin API endpoint. * + * @experimental + * * @type {string|Resource} * @default https://api.bentley.com */ @@ -106,6 +111,10 @@ ITwin.apiEndpoint = new Resource({ }); /** + * Get the export object for the specified export id + * + * @experimental + * * @param {string} exportId */ ITwin.getExport = async function (exportId) { @@ -146,6 +155,8 @@ ITwin.getExport = async function (exportId) { /** * Get the list of exports for the given iModel + changeset * + * @experimental + * * @param {string} iModelId * @param {string} [changesetId] */ @@ -161,7 +172,8 @@ ITwin.getExports = async function (iModelId, changesetId) { }; // obtain export for specified export id - let url = `${ITwin.apiEndpoint}mesh-export/?iModelId=${iModelId}&exportType=CESIUM&$top=1`; + // TODO: if we do include the clientVersion what should it be set to? can we sync it with the package.json? + let url = `${ITwin.apiEndpoint}mesh-export/?iModelId=${iModelId}&exportType=${ExportType["3DTILES"]}&$top=1&client=CesiumJS&clientVersion=1.123`; if (defined(changesetId) && changesetId !== "") { url += `&changesetId=${changesetId}`; } @@ -194,6 +206,12 @@ ITwin.getExports = async function (iModelId, changesetId) { /** * Start the export process for the given iModel + changeset. * + * TODO: REMOVE THIS FUNCTION! Auto generation of exports for the 3DTILES type is planned very soon + * and will be the desired way of interacting with iModels through exports. This function is here + * just while we continue testing during the PR process. + * + * @experimental + * * @param {string} iModelId * @param {string} [changesetId] */ @@ -214,7 +232,7 @@ ITwin.createExportForModelId = async function (iModelId, changesetId) { body: JSON.stringify({ iModelId, changesetId, - exportType: "CESIUM", + exportType: ExportType["3DTILES"], }), }; @@ -254,46 +272,5 @@ ITwin.createExportForModelId = async function (iModelId, changesetId) { return result.export.id; }; -/** - * Delete the specified export - * - * TODO: I'm not sure if we want this or not. Might belong better as an APP level function - * I just started creating helpers for all the routes under the `mesh-export` API - * for ease of access during testing - * - * @param {string} exportId - */ -ITwin.deleteExport = async function (exportId) { - if (!defined(ITwin.defaultAccessToken)) { - throw new DeveloperError("Must set ITwin.defaultAccessToken first"); - } - const headers = { - Authorization: ITwin.defaultAccessToken, - Accept: "application/vnd.bentley.itwin-platform.v1+json", - }; - - // obtain export for specified export id - const url = `${ITwin.apiEndpoint}mesh-export/${exportId}`; - - const response = await fetch(url, { method: "DELETE", headers }); - if (!response.ok) { - const result = await response.json(); - if (response.status === 401) { - throw new RuntimeError( - `Unauthorized, bad token, wrong scopes or headers bad. ${result.error.details[0].code}`, - ); - } else if (response.status === 404) { - throw new RuntimeError("Export not found"); - } else if (response.status === 422) { - throw new RuntimeError( - `Unprocessable Entity: ${result.error.code} ${result.error.message}`, - ); - } else if (response.status === 429) { - throw new RuntimeError("Too many requests"); - } - throw new RuntimeError(`Unknown request failure ${response.status}`); - } -}; - export default ITwin; export { ExportStatus, ExportType }; diff --git a/packages/engine/Source/Scene/createIModel3DTileset.js b/packages/engine/Source/Scene/createIModel3DTileset.js index 7bbefa6e0e0f..9e5908bee402 100644 --- a/packages/engine/Source/Scene/createIModel3DTileset.js +++ b/packages/engine/Source/Scene/createIModel3DTileset.js @@ -16,7 +16,7 @@ function delay(ms) { async function loadExport(exportObj, options) { let status = exportObj.status; - if (exportObj.request.exportType !== ExportType.CESIUM) { + if (exportObj.request.exportType !== ExportType["3DTILES"]) { // This is an undocumented value but I think it's the only one we want to load // TODO: should we even be checking this? throw new Error(`Wrong export type ${exportObj.request.exportType}`); @@ -55,6 +55,7 @@ const createIModel3DTileset = {}; * Creates a {@link Cesium3DTileset} instance for the Google Photorealistic 3D Tiles tileset. * * @function + * @experimental * * @param {string} exportId * @param {Cesium3DTileset.ConstructorOptions} [options] An object describing initialization options. @@ -63,17 +64,7 @@ const createIModel3DTileset = {}; * @see ITwin * * @example - * // Use your own iTwin API key for mesh export - * Cesium.ITwin.defaultApiKey = "your-api-key"; - * - * const viewer = new Cesium.Viewer("cesiumContainer"); - * - * try { - * const tileset = await Cesium.createIModel3DTileset(); - * viewer.scene.primitives.add(tileset)); - * } catch (error) { - * console.log(`Error creating tileset: ${error}`); - * } + * TODO: example after API finalized */ createIModel3DTileset.fromExportId = async function (exportId, options) { if (!defined(ITwin.defaultAccessToken)) { @@ -98,12 +89,9 @@ createIModel3DTileset.fromExportId = async function (exportId, options) { * with the export id directly * * @example - * // Try to load the corresponding tileset export or create it if it doesn't exist - * let tileset = await Cesium.createIModel3DTileset.fromModelIdimodelId, changesetId); - * if (!Cesium.defined(tileset)) { - * const exportId = await Cesium.ITwin.createExportForModelId(imodelId, changesetId, accessToken); - * tileset = await Cesium.createIModel3DTileset(exportId); - * } + * TODO: example after API finalized + * + * @experimental * * @param {string} iModelId * @param {string} changesetId @@ -116,7 +104,7 @@ createIModel3DTileset.fromModelId = async function ( ) { const { exports } = await ITwin.getExports(iModelId, changesetId); const cesiumExport = exports.find( - (exportObj) => exportObj.request?.exportType === ExportType.CESIUM, + (exportObj) => exportObj.request?.exportType === ExportType["3DTILES"], ); if (!defined(cesiumExport)) { return; From c953b6f91ba08999cbef0a979df82d031955d4fe Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:58:10 -0500 Subject: [PATCH 06/22] cleanup and adjustments from pr comments --- Apps/Sandcastle/gallery/iTwin Demo.html | 4 +- packages/engine/Source/Core/ITwin.js | 111 ++++++++++++------ .../Source/Scene/createIModel3DTileset.js | 41 ++++--- 3 files changed, 97 insertions(+), 59 deletions(-) diff --git a/Apps/Sandcastle/gallery/iTwin Demo.html b/Apps/Sandcastle/gallery/iTwin Demo.html index eeea23b17f51..a26bc50f5dbb 100644 --- a/Apps/Sandcastle/gallery/iTwin Demo.html +++ b/Apps/Sandcastle/gallery/iTwin Demo.html @@ -32,9 +32,8 @@ //Sandcastle_Begin const serviceResponse = await fetch("http://localhost:3000/service"); const { token } = await serviceResponse.json(); - const accessToken = `Bearer ${token}`; - Cesium.ITwin.defaultAccessToken = accessToken; + Cesium.ITwin.defaultAccessToken = token; // this is the iModel in the "Hello iTwinCesium" iTwin that we should all have access to // https://developer.bentley.com/my-itwins/b4a30036-0456-49ea-a439-3fcd9365e24e/home/ // const imodelId = "2852c3d7-00c3-4b5d-a0ce-82bbde4f061e"; @@ -135,7 +134,6 @@ const exportId = await Cesium.ITwin.createExportForModelId( imodelId, changesetId, - accessToken, ); statusOutput.innerText = "Creating Tileset from export"; tileset = await Cesium.createIModel3DTileset.fromExportId(exportId); diff --git a/packages/engine/Source/Core/ITwin.js b/packages/engine/Source/Core/ITwin.js index 51a89e15668d..249acd76ef88 100644 --- a/packages/engine/Source/Core/ITwin.js +++ b/packages/engine/Source/Core/ITwin.js @@ -1,30 +1,9 @@ +import Check from "./Check.js"; import defined from "./defined.js"; import DeveloperError from "./DeveloperError.js"; import Resource from "./Resource.js"; import RuntimeError from "./RuntimeError.js"; -/** - * @enum {string} - */ -const ExportStatus = Object.freeze({ - NotStarted: "NotStarted", - InProgress: "InProgress", - Complete: "Complete", - Invalid: "Invalid", -}); - -/** - * Type of an export currently, only GLTF and 3DFT are documented - * The CESIUM option is what we were told to use with Sandcastle - * I've also seen the IMODEL one but don't know where it's from - * @enum {string} - */ -const ExportType = Object.freeze({ - IMODEL: "IMODEL", - CESIUM: "CESIUM", - "3DTILES": "3DTILES", -}); - /** * @typedef {Object} GeometryOptions * @property {boolean} includeLines @@ -47,7 +26,7 @@ const ExportType = Object.freeze({ * @typedef {Object} StartExport * @property {string} iModelId * @property {string} changesetId - * @property {ExportType} exportType Type of mesh to create. Currently, only GLTF and 3DFT are supported and undocumented CESIUM option + * @property {ITwin.ExportType} exportType Type of mesh to create. Currently, only GLTF and 3DFT are supported and undocumented CESIUM option * @property {GeometryOptions} geometryOptions * @property {ViewDefinitionFilter} viewDefinitionFilter */ @@ -61,7 +40,7 @@ const ExportType = Object.freeze({ * @typedef {Object} Export * @property {string} id * @property {string} displayName - * @property {ExportStatus} status + * @property {ITwin.ExportStatus} status * @property {StartExport} request * @property {{mesh: Link}} _links */ @@ -74,13 +53,32 @@ const ExportType = Object.freeze({ /** * Default settings for accessing the iTwin platform. * - * @experimental + * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * * @see createIModel3DTileset * @namespace ITwin */ const ITwin = {}; +/** + * @enum {string} + */ +ITwin.ExportStatus = Object.freeze({ + NotStarted: "NotStarted", + InProgress: "InProgress", + Complete: "Complete", + Invalid: "Invalid", +}); + +/** + * @enum {string} + */ +ITwin.ExportType = Object.freeze({ + IMODEL: "IMODEL", + CESIUM: "CESIUM", + "3DTILES": "3DTILES", +}); + /** * Gets or sets the default iTwin access token. * @@ -92,7 +90,7 @@ const ITwin = {}; * `itwin-platform` if we want to use the iModel shares ourselves GET /imodels/{id}/shares * Seems the `itwin-platform` scope should apply to everything but the docs are a little unclear * - * @experimental + * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * * @type {string|undefined} */ @@ -101,7 +99,7 @@ ITwin.defaultAccessToken = undefined; /** * Gets or sets the default iTwin API endpoint. * - * @experimental + * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * * @type {string|Resource} * @default https://api.bentley.com @@ -113,17 +111,25 @@ ITwin.apiEndpoint = new Resource({ /** * Get the export object for the specified export id * - * @experimental + * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * * @param {string} exportId + * + * @throws {RuntimeError} Unauthorized, bad token, wrong scopes or headers bad. + * @throws {RuntimeError} Requested export is not available + * @throws {RuntimeError} Too many requests + * @throws {RuntimeError} Unknown request failure */ ITwin.getExport = async function (exportId) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.string("exportId", exportId); if (!defined(ITwin.defaultAccessToken)) { throw new DeveloperError("Must set ITwin.defaultAccessToken first"); } + //>>includeEnd('debug') const headers = { - Authorization: ITwin.defaultAccessToken, + Authorization: `Bearer ${ITwin.defaultAccessToken}`, Accept: "application/vnd.bentley.itwin-platform.v1+json", }; @@ -155,27 +161,47 @@ ITwin.getExport = async function (exportId) { /** * Get the list of exports for the given iModel + changeset * - * @experimental + * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * * @param {string} iModelId * @param {string} [changesetId] + * + * @throws {RuntimeError} Unauthorized, bad token, wrong scopes or headers bad. + * @throws {RuntimeError} Not allowed, forbidden + * @throws {RuntimeError} Unprocessable Entity + * @throws {RuntimeError} Too many requests + * @throws {RuntimeError} Unknown request failure */ ITwin.getExports = async function (iModelId, changesetId) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.string("iModelId", iModelId); + if (defined(changesetId)) { + Check.typeOf.string("changesetId", changesetId); + } if (!defined(ITwin.defaultAccessToken)) { throw new DeveloperError("Must set ITwin.defaultAccessToken first"); } + //>>includeEnd('debug') const headers = { - Authorization: ITwin.defaultAccessToken, + Authorization: `Bearer ${ITwin.defaultAccessToken}`, Accept: "application/vnd.bentley.itwin-platform.v1+json", Prefer: "return=representation", // or return=minimal (the default) }; // obtain export for specified export id // TODO: if we do include the clientVersion what should it be set to? can we sync it with the package.json? - let url = `${ITwin.apiEndpoint}mesh-export/?iModelId=${iModelId}&exportType=${ExportType["3DTILES"]}&$top=1&client=CesiumJS&clientVersion=1.123`; + const url = new URL(`${ITwin.apiEndpoint}mesh-export`); + url.searchParams.set("iModelId", iModelId); if (defined(changesetId) && changesetId !== "") { - url += `&changesetId=${changesetId}`; + url.searchParams.set("changesetId", changesetId); + } + url.searchParams.set("exportType", ITwin.ExportType["3DTILES"]); + url.searchParams.set("$top", "1"); + url.searchParams.set("client", "CesiumJS"); + /* global CESIUM_VERSION */ + if (typeof CESIUM_VERSION !== "undefined") { + url.searchParams.set("clientVersion", CESIUM_VERSION); } const response = await fetch(url, { headers }); @@ -210,29 +236,41 @@ ITwin.getExports = async function (iModelId, changesetId) { * and will be the desired way of interacting with iModels through exports. This function is here * just while we continue testing during the PR process. * - * @experimental + * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * * @param {string} iModelId * @param {string} [changesetId] + * + * @throws {RuntimeError} Unauthorized, bad token, wrong scopes or headers bad. + * @throws {RuntimeError} Not allowed, forbidden + * @throws {RuntimeError} Unprocessable: Cannot create export job + * @throws {RuntimeError} Too many requests + * @throws {RuntimeError} Unknown request failure */ ITwin.createExportForModelId = async function (iModelId, changesetId) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.string("iModelId", iModelId); + if (defined(changesetId)) { + Check.typeOf.string("changesetId", changesetId); + } if (!defined(ITwin.defaultAccessToken)) { throw new DeveloperError("Must set ITwin.defaultAccessToken first"); } + //>>includeEnd('debug') changesetId = changesetId ?? ""; const requestOptions = { method: "POST", headers: { - Authorization: ITwin.defaultAccessToken, + Authorization: `Bearer ${ITwin.defaultAccessToken}`, Accept: "application/vnd.bentley.itwin-platform.v1+json", "Content-Type": "application/json", }, body: JSON.stringify({ iModelId, changesetId, - exportType: ExportType["3DTILES"], + exportType: ITwin.ExportType["3DTILES"], }), }; @@ -273,4 +311,3 @@ ITwin.createExportForModelId = async function (iModelId, changesetId) { }; export default ITwin; -export { ExportStatus, ExportType }; diff --git a/packages/engine/Source/Scene/createIModel3DTileset.js b/packages/engine/Source/Scene/createIModel3DTileset.js index 9e5908bee402..f4ebb904318b 100644 --- a/packages/engine/Source/Scene/createIModel3DTileset.js +++ b/packages/engine/Source/Scene/createIModel3DTileset.js @@ -1,9 +1,9 @@ import Cesium3DTileset from "./Cesium3DTileset.js"; import defined from "../Core/defined.js"; import Resource from "../Core/Resource.js"; -import ITwin, { ExportStatus, ExportType } from "../Core/ITwin.js"; -import DeveloperError from "../Core/DeveloperError.js"; +import ITwin from "../Core/ITwin.js"; import RuntimeError from "../Core/RuntimeError.js"; +import Check from "../Core/Check.js"; function delay(ms) { return new Promise((res) => setTimeout(res, ms)); @@ -14,18 +14,20 @@ function delay(ms) { * @param {Cesium3DTileset.ConstructorOptions} [options] */ async function loadExport(exportObj, options) { + //>>includeStart('debug', pragmas.debug); + Check.defined("exportObj", exportObj); + //>>includeEnd('debug') + let status = exportObj.status; - if (exportObj.request.exportType !== ExportType["3DTILES"]) { - // This is an undocumented value but I think it's the only one we want to load - // TODO: should we even be checking this? - throw new Error(`Wrong export type ${exportObj.request.exportType}`); + if (exportObj.request.exportType !== ITwin.ExportType["3DTILES"]) { + throw new RuntimeError(`Wrong export type ${exportObj.request.exportType}`); } const timeoutAfter = 300000; const start = Date.now(); // wait until the export is complete - while (status !== ExportStatus.Complete) { + while (status !== ITwin.ExportStatus.Complete) { await delay(5000); exportObj = (await ITwin.getExport(exportObj.id)).export; status = exportObj.status; @@ -36,11 +38,11 @@ async function loadExport(exportObj, options) { } } + // Convert the link to the tileset url while preserving the search paramaters // This link is only valid 1 hour - let tilesetUrl = exportObj._links.mesh.href; - const splitStr = tilesetUrl.split("?"); - // is there a cleaner way to do this? - tilesetUrl = `${splitStr[0]}/tileset.json?${splitStr[1]}`; + const baseUrl = new URL(exportObj._links.mesh.href); + baseUrl.pathname = `${baseUrl.pathname}/tileset.json`; + const tilesetUrl = baseUrl.toString(); const resource = new Resource({ url: tilesetUrl, @@ -55,22 +57,19 @@ const createIModel3DTileset = {}; * Creates a {@link Cesium3DTileset} instance for the Google Photorealistic 3D Tiles tileset. * * @function - * @experimental + * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * * @param {string} exportId * @param {Cesium3DTileset.ConstructorOptions} [options] An object describing initialization options. * @returns {Promise} * - * @see ITwin + * @throws {RuntimeError} Wrong export type + * @throws {RuntimeError} Export did not complete in time. * * @example * TODO: example after API finalized */ createIModel3DTileset.fromExportId = async function (exportId, options) { - if (!defined(ITwin.defaultAccessToken)) { - throw new DeveloperError("Must set ITwin.defaultAccessToken first"); - } - options = options ?? {}; const result = await ITwin.getExport(exportId); @@ -91,11 +90,14 @@ createIModel3DTileset.fromExportId = async function (exportId, options) { * @example * TODO: example after API finalized * - * @experimental + * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * * @param {string} iModelId * @param {string} changesetId * @param {Cesium3DTileset.ConstructorOptions} options + * + * @throws {RuntimeError} Wrong export type + * @throws {RuntimeError} Export did not complete in time. */ createIModel3DTileset.fromModelId = async function ( iModelId, @@ -104,7 +106,8 @@ createIModel3DTileset.fromModelId = async function ( ) { const { exports } = await ITwin.getExports(iModelId, changesetId); const cesiumExport = exports.find( - (exportObj) => exportObj.request?.exportType === ExportType["3DTILES"], + (exportObj) => + exportObj.request?.exportType === ITwin.ExportType["3DTILES"], ); if (!defined(cesiumExport)) { return; From 46bd2f004681ff700989c309477e8dfff4852d19 Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:03:00 -0500 Subject: [PATCH 07/22] rename namespaces --- Apps/Sandcastle/gallery/iTwin Demo.html | 8 ++-- .../Core/{ITwin.js => ITwinPlatform.js} | 48 +++++++++---------- ...{createIModel3DTileset.js => ITwinData.js} | 26 +++++----- 3 files changed, 41 insertions(+), 41 deletions(-) rename packages/engine/Source/Core/{ITwin.js => ITwinPlatform.js} (86%) rename packages/engine/Source/Scene/{createIModel3DTileset.js => ITwinData.js} (81%) diff --git a/Apps/Sandcastle/gallery/iTwin Demo.html b/Apps/Sandcastle/gallery/iTwin Demo.html index a26bc50f5dbb..dc87286bcd24 100644 --- a/Apps/Sandcastle/gallery/iTwin Demo.html +++ b/Apps/Sandcastle/gallery/iTwin Demo.html @@ -33,7 +33,7 @@ const serviceResponse = await fetch("http://localhost:3000/service"); const { token } = await serviceResponse.json(); - Cesium.ITwin.defaultAccessToken = token; + Cesium.ITwinPlatform.defaultAccessToken = token; // this is the iModel in the "Hello iTwinCesium" iTwin that we should all have access to // https://developer.bentley.com/my-itwins/b4a30036-0456-49ea-a439-3fcd9365e24e/home/ // const imodelId = "2852c3d7-00c3-4b5d-a0ce-82bbde4f061e"; @@ -123,7 +123,7 @@ statusOutput.innerText = "Creating Tileset"; - let tileset = await Cesium.createIModel3DTileset.fromModelId( + let tileset = await Cesium.ITwinData.createTilesetFromModelId( imodelId, changesetId, ); @@ -131,12 +131,12 @@ // TODO: this is temporary, we should not have to call the Start Export route ever after // auto generation is set up statusOutput.innerText = "Starting export"; - const exportId = await Cesium.ITwin.createExportForModelId( + const exportId = await Cesium.ITwinPlatform.createExportForModelId( imodelId, changesetId, ); statusOutput.innerText = "Creating Tileset from export"; - tileset = await Cesium.createIModel3DTileset.fromExportId(exportId); + tileset = await Cesium.ITwinData.createTilesetFromExportId(exportId); } scene.primitives.add(tileset); diff --git a/packages/engine/Source/Core/ITwin.js b/packages/engine/Source/Core/ITwinPlatform.js similarity index 86% rename from packages/engine/Source/Core/ITwin.js rename to packages/engine/Source/Core/ITwinPlatform.js index 249acd76ef88..0131e8efd5ee 100644 --- a/packages/engine/Source/Core/ITwin.js +++ b/packages/engine/Source/Core/ITwinPlatform.js @@ -26,7 +26,7 @@ import RuntimeError from "./RuntimeError.js"; * @typedef {Object} StartExport * @property {string} iModelId * @property {string} changesetId - * @property {ITwin.ExportType} exportType Type of mesh to create. Currently, only GLTF and 3DFT are supported and undocumented CESIUM option + * @property {ITwinPlatform.ExportType} exportType Type of mesh to create. Currently, only GLTF and 3DFT are supported and undocumented CESIUM option * @property {GeometryOptions} geometryOptions * @property {ViewDefinitionFilter} viewDefinitionFilter */ @@ -40,7 +40,7 @@ import RuntimeError from "./RuntimeError.js"; * @typedef {Object} Export * @property {string} id * @property {string} displayName - * @property {ITwin.ExportStatus} status + * @property {ITwinPlatform.ExportStatus} status * @property {StartExport} request * @property {{mesh: Link}} _links */ @@ -55,15 +55,15 @@ import RuntimeError from "./RuntimeError.js"; * * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * - * @see createIModel3DTileset - * @namespace ITwin + * @see ITwinData for ways to import data + * @namespace ITwinPlatform */ -const ITwin = {}; +const ITwinPlatform = {}; /** * @enum {string} */ -ITwin.ExportStatus = Object.freeze({ +ITwinPlatform.ExportStatus = Object.freeze({ NotStarted: "NotStarted", InProgress: "InProgress", Complete: "Complete", @@ -73,7 +73,7 @@ ITwin.ExportStatus = Object.freeze({ /** * @enum {string} */ -ITwin.ExportType = Object.freeze({ +ITwinPlatform.ExportType = Object.freeze({ IMODEL: "IMODEL", CESIUM: "CESIUM", "3DTILES": "3DTILES", @@ -94,7 +94,7 @@ ITwin.ExportType = Object.freeze({ * * @type {string|undefined} */ -ITwin.defaultAccessToken = undefined; +ITwinPlatform.defaultAccessToken = undefined; /** * Gets or sets the default iTwin API endpoint. @@ -104,7 +104,7 @@ ITwin.defaultAccessToken = undefined; * @type {string|Resource} * @default https://api.bentley.com */ -ITwin.apiEndpoint = new Resource({ +ITwinPlatform.apiEndpoint = new Resource({ url: "https://api.bentley.com", }); @@ -120,21 +120,21 @@ ITwin.apiEndpoint = new Resource({ * @throws {RuntimeError} Too many requests * @throws {RuntimeError} Unknown request failure */ -ITwin.getExport = async function (exportId) { +ITwinPlatform.getExport = async function (exportId) { //>>includeStart('debug', pragmas.debug); Check.typeOf.string("exportId", exportId); - if (!defined(ITwin.defaultAccessToken)) { + if (!defined(ITwinPlatform.defaultAccessToken)) { throw new DeveloperError("Must set ITwin.defaultAccessToken first"); } //>>includeEnd('debug') const headers = { - Authorization: `Bearer ${ITwin.defaultAccessToken}`, + Authorization: `Bearer ${ITwinPlatform.defaultAccessToken}`, Accept: "application/vnd.bentley.itwin-platform.v1+json", }; // obtain export for specified export id - const url = `${ITwin.apiEndpoint}mesh-export/${exportId}`; + const url = `${ITwinPlatform.apiEndpoint}mesh-export/${exportId}`; // TODO: this request is _really_ slow, like 7 whole second alone for me // Arun said this was kinda normal but to keep track of the `x-correlation-id` of any that take EXTRA long @@ -172,31 +172,31 @@ ITwin.getExport = async function (exportId) { * @throws {RuntimeError} Too many requests * @throws {RuntimeError} Unknown request failure */ -ITwin.getExports = async function (iModelId, changesetId) { +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(ITwin.defaultAccessToken)) { + if (!defined(ITwinPlatform.defaultAccessToken)) { throw new DeveloperError("Must set ITwin.defaultAccessToken first"); } //>>includeEnd('debug') const headers = { - Authorization: `Bearer ${ITwin.defaultAccessToken}`, + Authorization: `Bearer ${ITwinPlatform.defaultAccessToken}`, Accept: "application/vnd.bentley.itwin-platform.v1+json", Prefer: "return=representation", // or return=minimal (the default) }; // obtain export for specified export id // TODO: if we do include the clientVersion what should it be set to? can we sync it with the package.json? - const url = new URL(`${ITwin.apiEndpoint}mesh-export`); + const url = new URL(`${ITwinPlatform.apiEndpoint}mesh-export`); url.searchParams.set("iModelId", iModelId); if (defined(changesetId) && changesetId !== "") { url.searchParams.set("changesetId", changesetId); } - url.searchParams.set("exportType", ITwin.ExportType["3DTILES"]); + url.searchParams.set("exportType", ITwinPlatform.ExportType["3DTILES"]); url.searchParams.set("$top", "1"); url.searchParams.set("client", "CesiumJS"); /* global CESIUM_VERSION */ @@ -247,13 +247,13 @@ ITwin.getExports = async function (iModelId, changesetId) { * @throws {RuntimeError} Too many requests * @throws {RuntimeError} Unknown request failure */ -ITwin.createExportForModelId = async function (iModelId, changesetId) { +ITwinPlatform.createExportForModelId = async function (iModelId, changesetId) { //>>includeStart('debug', pragmas.debug); Check.typeOf.string("iModelId", iModelId); if (defined(changesetId)) { Check.typeOf.string("changesetId", changesetId); } - if (!defined(ITwin.defaultAccessToken)) { + if (!defined(ITwinPlatform.defaultAccessToken)) { throw new DeveloperError("Must set ITwin.defaultAccessToken first"); } //>>includeEnd('debug') @@ -263,20 +263,20 @@ ITwin.createExportForModelId = async function (iModelId, changesetId) { const requestOptions = { method: "POST", headers: { - Authorization: `Bearer ${ITwin.defaultAccessToken}`, + Authorization: `Bearer ${ITwinPlatform.defaultAccessToken}`, Accept: "application/vnd.bentley.itwin-platform.v1+json", "Content-Type": "application/json", }, body: JSON.stringify({ iModelId, changesetId, - exportType: ITwin.ExportType["3DTILES"], + exportType: ITwinPlatform.ExportType["3DTILES"], }), }; // initiate mesh export const response = await fetch( - `${ITwin.apiEndpoint}mesh-export/`, + `${ITwinPlatform.apiEndpoint}mesh-export/`, requestOptions, ); @@ -310,4 +310,4 @@ ITwin.createExportForModelId = async function (iModelId, changesetId) { return result.export.id; }; -export default ITwin; +export default ITwinPlatform; diff --git a/packages/engine/Source/Scene/createIModel3DTileset.js b/packages/engine/Source/Scene/ITwinData.js similarity index 81% rename from packages/engine/Source/Scene/createIModel3DTileset.js rename to packages/engine/Source/Scene/ITwinData.js index f4ebb904318b..e64307798e99 100644 --- a/packages/engine/Source/Scene/createIModel3DTileset.js +++ b/packages/engine/Source/Scene/ITwinData.js @@ -1,7 +1,7 @@ import Cesium3DTileset from "./Cesium3DTileset.js"; import defined from "../Core/defined.js"; import Resource from "../Core/Resource.js"; -import ITwin from "../Core/ITwin.js"; +import ITwinPlatform from "../Core/ITwinPlatform.js"; import RuntimeError from "../Core/RuntimeError.js"; import Check from "../Core/Check.js"; @@ -20,16 +20,16 @@ async function loadExport(exportObj, options) { let status = exportObj.status; - if (exportObj.request.exportType !== ITwin.ExportType["3DTILES"]) { + if (exportObj.request.exportType !== ITwinPlatform.ExportType["3DTILES"]) { throw new RuntimeError(`Wrong export type ${exportObj.request.exportType}`); } const timeoutAfter = 300000; const start = Date.now(); // wait until the export is complete - while (status !== ITwin.ExportStatus.Complete) { + while (status !== ITwinPlatform.ExportStatus.Complete) { await delay(5000); - exportObj = (await ITwin.getExport(exportObj.id)).export; + exportObj = (await ITwinPlatform.getExport(exportObj.id)).export; status = exportObj.status; console.log(`Export is ${status}`); @@ -51,7 +51,7 @@ async function loadExport(exportObj, options) { return Cesium3DTileset.fromUrl(resource, options); } -const createIModel3DTileset = {}; +const ITwinData = {}; /** * Creates a {@link Cesium3DTileset} instance for the Google Photorealistic 3D Tiles tileset. @@ -69,10 +69,10 @@ const createIModel3DTileset = {}; * @example * TODO: example after API finalized */ -createIModel3DTileset.fromExportId = async function (exportId, options) { +ITwinData.createTilesetFromExportId = async function (exportId, options) { options = options ?? {}; - const result = await ITwin.getExport(exportId); + const result = await ITwinPlatform.getExport(exportId); const tileset = await loadExport(result.export, options); return tileset; }; @@ -81,10 +81,10 @@ createIModel3DTileset.fromExportId = async function (exportId, options) { * Check the exports for the given iModel + changeset combination for any that * have the desired CESIUM type and returns the first one that matches as a new tileset. * - * If there is not a CESIUM export you can create it using {@link ITwin.createExportForModelId} + * If there is not a CESIUM export you can create it using {@link ITwinPlatform.createExportForModelId} * * This function assumes one export per type per "iModel id + changeset id". If you need to create - * multiple exports per "iModel id + changeset id" you should switch to using {@link createIModel3DTileset} + * multiple exports per "iModel id + changeset id" you should switch to using {@link ITwinData} * with the export id directly * * @example @@ -99,15 +99,15 @@ createIModel3DTileset.fromExportId = async function (exportId, options) { * @throws {RuntimeError} Wrong export type * @throws {RuntimeError} Export did not complete in time. */ -createIModel3DTileset.fromModelId = async function ( +ITwinData.createTilesetFromModelId = async function ( iModelId, changesetId, options, ) { - const { exports } = await ITwin.getExports(iModelId, changesetId); + const { exports } = await ITwinPlatform.getExports(iModelId, changesetId); const cesiumExport = exports.find( (exportObj) => - exportObj.request?.exportType === ITwin.ExportType["3DTILES"], + exportObj.request?.exportType === ITwinPlatform.ExportType["3DTILES"], ); if (!defined(cesiumExport)) { return; @@ -115,4 +115,4 @@ createIModel3DTileset.fromModelId = async function ( return loadExport(cesiumExport, options); }; -export default createIModel3DTileset; +export default ITwinData; From 1757f77a0878d695d80f4f4c2d24172f994adf55 Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:32:03 -0500 Subject: [PATCH 08/22] partial cleanup, remove changeset ids --- Apps/Sandcastle/gallery/iTwin Demo.html | 11 +---- packages/engine/Source/Core/ITwinPlatform.js | 50 ++++---------------- packages/engine/Source/Scene/ITwinData.js | 47 ++++++++---------- 3 files changed, 33 insertions(+), 75 deletions(-) diff --git a/Apps/Sandcastle/gallery/iTwin Demo.html b/Apps/Sandcastle/gallery/iTwin Demo.html index dc87286bcd24..27047ba7d14d 100644 --- a/Apps/Sandcastle/gallery/iTwin Demo.html +++ b/Apps/Sandcastle/gallery/iTwin Demo.html @@ -38,7 +38,6 @@ // https://developer.bentley.com/my-itwins/b4a30036-0456-49ea-a439-3fcd9365e24e/home/ // const imodelId = "2852c3d7-00c3-4b5d-a0ce-82bbde4f061e"; const imodelId = "88673c1d-12b8-48f1-8beb-5000d0edbd0b"; - const changesetId = ""; // Grabbed mapping from the iTwin Viewer const classes = { @@ -123,18 +122,12 @@ statusOutput.innerText = "Creating Tileset"; - let tileset = await Cesium.ITwinData.createTilesetFromModelId( - imodelId, - changesetId, - ); + let tileset = await Cesium.ITwinData.createTilesetFromModelId(imodelId); if (!Cesium.defined(tileset)) { // TODO: this is temporary, we should not have to call the Start Export route ever after // auto generation is set up statusOutput.innerText = "Starting export"; - const exportId = await Cesium.ITwinPlatform.createExportForModelId( - imodelId, - changesetId, - ); + const exportId = await Cesium.ITwinPlatform.createExportForModelId(imodelId); statusOutput.innerText = "Creating Tileset from export"; tileset = await Cesium.ITwinData.createTilesetFromExportId(exportId); } diff --git a/packages/engine/Source/Core/ITwinPlatform.js b/packages/engine/Source/Core/ITwinPlatform.js index 0131e8efd5ee..c968ed48dd29 100644 --- a/packages/engine/Source/Core/ITwinPlatform.js +++ b/packages/engine/Source/Core/ITwinPlatform.js @@ -61,6 +61,7 @@ import RuntimeError from "./RuntimeError.js"; const ITwinPlatform = {}; /** + * Status states for a mesh-export export * @enum {string} */ ITwinPlatform.ExportStatus = Object.freeze({ @@ -71,6 +72,7 @@ ITwinPlatform.ExportStatus = Object.freeze({ }); /** + * Types of mesh-export exports. CesiumJS only supports loading 3DTILES type exports * @enum {string} */ ITwinPlatform.ExportType = Object.freeze({ @@ -80,15 +82,7 @@ ITwinPlatform.ExportType = Object.freeze({ }); /** - * Gets or sets the default iTwin access token. - * - * TODO: I'm not sure we can even do this kind of access token. Each route seems to need it's own scopes - * and we may not be able to guarantee this "top level token" has them all - * So far we use - * `mesh-export:read` for loading meshes GET /mesh-export(s) - * `mesh-export:modify` if we want to include a function to create an export - * `itwin-platform` if we want to use the iModel shares ourselves GET /imodels/{id}/shares - * Seems the `itwin-platform` scope should apply to everything but the docs are a little unclear + * Gets or sets the default iTwin access token. This token should have the itwin-platform scope. * * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * @@ -119,6 +113,7 @@ ITwinPlatform.apiEndpoint = new Resource({ * @throws {RuntimeError} Requested export is not available * @throws {RuntimeError} Too many requests * @throws {RuntimeError} Unknown request failure + * TODO: remove? this is used when we're looping to wait for jobs to finish */ ITwinPlatform.getExport = async function (exportId) { //>>includeStart('debug', pragmas.debug); @@ -159,25 +154,22 @@ ITwinPlatform.getExport = async function (exportId) { }; /** - * Get the list of exports for the given iModel + changeset + * Get the list of exports for the specified iModel at it's most current version. This will only return exports with {@link ITwinPlatform.ExportType} of 3DTILES. * * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * - * @param {string} iModelId - * @param {string} [changesetId] + * @param {string} iModelId iModel id * * @throws {RuntimeError} Unauthorized, bad token, wrong scopes or headers bad. * @throws {RuntimeError} Not allowed, forbidden * @throws {RuntimeError} Unprocessable Entity * @throws {RuntimeError} Too many requests * @throws {RuntimeError} Unknown request failure + * @returns {Promise<{exports: Export[]}>} */ -ITwinPlatform.getExports = async function (iModelId, changesetId) { +ITwinPlatform.getExports = async function (iModelId) { //>>includeStart('debug', pragmas.debug); Check.typeOf.string("iModelId", iModelId); - if (defined(changesetId)) { - Check.typeOf.string("changesetId", changesetId); - } if (!defined(ITwinPlatform.defaultAccessToken)) { throw new DeveloperError("Must set ITwin.defaultAccessToken first"); } @@ -190,12 +182,8 @@ ITwinPlatform.getExports = async function (iModelId, changesetId) { }; // obtain export for specified export id - // TODO: if we do include the clientVersion what should it be set to? can we sync it with the package.json? const url = new URL(`${ITwinPlatform.apiEndpoint}mesh-export`); url.searchParams.set("iModelId", iModelId); - if (defined(changesetId) && changesetId !== "") { - url.searchParams.set("changesetId", changesetId); - } url.searchParams.set("exportType", ITwinPlatform.ExportType["3DTILES"]); url.searchParams.set("$top", "1"); url.searchParams.set("client", "CesiumJS"); @@ -230,36 +218,19 @@ ITwinPlatform.getExports = async function (iModelId, changesetId) { }; /** - * Start the export process for the given iModel + changeset. - * * TODO: REMOVE THIS FUNCTION! Auto generation of exports for the 3DTILES type is planned very soon * and will be the desired way of interacting with iModels through exports. This function is here * just while we continue testing during the PR process. - * - * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. - * - * @param {string} iModelId - * @param {string} [changesetId] - * - * @throws {RuntimeError} Unauthorized, bad token, wrong scopes or headers bad. - * @throws {RuntimeError} Not allowed, forbidden - * @throws {RuntimeError} Unprocessable: Cannot create export job - * @throws {RuntimeError} Too many requests - * @throws {RuntimeError} Unknown request failure + * @deprecated */ -ITwinPlatform.createExportForModelId = async function (iModelId, changesetId) { +ITwinPlatform.createExportForModelId = async function (iModelId) { //>>includeStart('debug', pragmas.debug); Check.typeOf.string("iModelId", iModelId); - if (defined(changesetId)) { - Check.typeOf.string("changesetId", changesetId); - } if (!defined(ITwinPlatform.defaultAccessToken)) { throw new DeveloperError("Must set ITwin.defaultAccessToken first"); } //>>includeEnd('debug') - changesetId = changesetId ?? ""; - const requestOptions = { method: "POST", headers: { @@ -269,7 +240,6 @@ ITwinPlatform.createExportForModelId = async function (iModelId, changesetId) { }, body: JSON.stringify({ iModelId, - changesetId, exportType: ITwinPlatform.ExportType["3DTILES"], }), }; diff --git a/packages/engine/Source/Scene/ITwinData.js b/packages/engine/Source/Scene/ITwinData.js index e64307798e99..f822b4d9bc97 100644 --- a/packages/engine/Source/Scene/ITwinData.js +++ b/packages/engine/Source/Scene/ITwinData.js @@ -11,7 +11,8 @@ function delay(ms) { /** * @param {Export} exportObj - * @param {Cesium3DTileset.ConstructorOptions} [options] + * @param {Cesium3DTileset.ConstructorOptions} [options] Object containing options to pass to an internally created {@link Cesium3DTileset}. + * @returns {Promise} */ async function loadExport(exportObj, options) { //>>includeStart('debug', pragmas.debug); @@ -51,16 +52,24 @@ async function loadExport(exportObj, options) { return Cesium3DTileset.fromUrl(resource, options); } +/** + * Methods for loading iTwin platform data into CesiumJS + * + * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. + * + * @see ITwinPlatform to set the API token and access api functions + * @namespace ITwinData + */ const ITwinData = {}; /** - * Creates a {@link Cesium3DTileset} instance for the Google Photorealistic 3D Tiles tileset. + * Creates a {@link Cesium3DTileset} instance for the given export id. * * @function * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * * @param {string} exportId - * @param {Cesium3DTileset.ConstructorOptions} [options] An object describing initialization options. + * @param {Cesium3DTileset.ConstructorOptions} [options] Object containing options to pass to an internally created {@link Cesium3DTileset}. * @returns {Promise} * * @throws {RuntimeError} Wrong export type @@ -68,47 +77,33 @@ const ITwinData = {}; * * @example * TODO: example after API finalized + * @deprecated */ ITwinData.createTilesetFromExportId = async function (exportId, options) { - options = options ?? {}; - const result = await ITwinPlatform.getExport(exportId); const tileset = await loadExport(result.export, options); return tileset; }; /** - * Check the exports for the given iModel + changeset combination for any that - * have the desired CESIUM type and returns the first one that matches as a new tileset. - * - * If there is not a CESIUM export you can create it using {@link ITwinPlatform.createExportForModelId} - * - * This function assumes one export per type per "iModel id + changeset id". If you need to create - * multiple exports per "iModel id + changeset id" you should switch to using {@link ITwinData} - * with the export id directly + * Loads the export for the specified iModel with the export type that CesiumJS can load and returns + * a tileset created from that export. * * @example * TODO: example after API finalized * * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * - * @param {string} iModelId - * @param {string} changesetId - * @param {Cesium3DTileset.ConstructorOptions} options + * @param {string} iModelId The id of the iModel to load + * @param {Cesium3DTileset.ConstructorOptions} [options] Object containing options to pass to an internally created {@link Cesium3DTileset}. + * @returns {Promise} Will return undefined if there is no export for the given iModel id * * @throws {RuntimeError} Wrong export type * @throws {RuntimeError} Export did not complete in time. */ -ITwinData.createTilesetFromModelId = async function ( - iModelId, - changesetId, - options, -) { - const { exports } = await ITwinPlatform.getExports(iModelId, changesetId); - const cesiumExport = exports.find( - (exportObj) => - exportObj.request?.exportType === ITwinPlatform.ExportType["3DTILES"], - ); +ITwinData.createTilesetFromModelId = async function (iModelId, options) { + const { exports } = await ITwinPlatform.getExports(iModelId); + const cesiumExport = exports[0]; if (!defined(cesiumExport)) { return; } From b8c1ac54bb7914451ae43806bc544114ef190197 Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:40:03 -0500 Subject: [PATCH 09/22] remove get export and creation routes --- Apps/Sandcastle/gallery/iTwin Demo.html | 10 +- packages/engine/Source/Core/ITwinPlatform.js | 115 +------------------ packages/engine/Source/Scene/ITwinData.js | 53 ++------- 3 files changed, 11 insertions(+), 167 deletions(-) diff --git a/Apps/Sandcastle/gallery/iTwin Demo.html b/Apps/Sandcastle/gallery/iTwin Demo.html index 27047ba7d14d..de045f350659 100644 --- a/Apps/Sandcastle/gallery/iTwin Demo.html +++ b/Apps/Sandcastle/gallery/iTwin Demo.html @@ -122,15 +122,7 @@ statusOutput.innerText = "Creating Tileset"; - let tileset = await Cesium.ITwinData.createTilesetFromModelId(imodelId); - if (!Cesium.defined(tileset)) { - // TODO: this is temporary, we should not have to call the Start Export route ever after - // auto generation is set up - statusOutput.innerText = "Starting export"; - const exportId = await Cesium.ITwinPlatform.createExportForModelId(imodelId); - statusOutput.innerText = "Creating Tileset from export"; - tileset = await Cesium.ITwinData.createTilesetFromExportId(exportId); - } + const tileset = await Cesium.ITwinData.createTilesetFromModelId(imodelId); scene.primitives.add(tileset); tileset.colorBlendMode = Cesium.Cesium3DTileColorBlendMode.REPLACE; diff --git a/packages/engine/Source/Core/ITwinPlatform.js b/packages/engine/Source/Core/ITwinPlatform.js index c968ed48dd29..134c5a553f83 100644 --- a/packages/engine/Source/Core/ITwinPlatform.js +++ b/packages/engine/Source/Core/ITwinPlatform.js @@ -102,61 +102,11 @@ ITwinPlatform.apiEndpoint = new Resource({ url: "https://api.bentley.com", }); -/** - * Get the export object for the specified export id - * - * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. - * - * @param {string} exportId - * - * @throws {RuntimeError} Unauthorized, bad token, wrong scopes or headers bad. - * @throws {RuntimeError} Requested export is not available - * @throws {RuntimeError} Too many requests - * @throws {RuntimeError} Unknown request failure - * TODO: remove? this is used when we're looping to wait for jobs to finish - */ -ITwinPlatform.getExport = async function (exportId) { - //>>includeStart('debug', pragmas.debug); - Check.typeOf.string("exportId", exportId); - if (!defined(ITwinPlatform.defaultAccessToken)) { - throw new DeveloperError("Must set ITwin.defaultAccessToken first"); - } - //>>includeEnd('debug') - - const headers = { - Authorization: `Bearer ${ITwinPlatform.defaultAccessToken}`, - Accept: "application/vnd.bentley.itwin-platform.v1+json", - }; - - // obtain export for specified export id - const url = `${ITwinPlatform.apiEndpoint}mesh-export/${exportId}`; - - // TODO: this request is _really_ slow, like 7 whole second alone for me - // Arun said this was kinda normal but to keep track of the `x-correlation-id` of any that take EXTRA long - const response = await fetch(url, { headers }); - if (!response.ok) { - const result = await response.json(); - if (response.status === 401) { - throw new RuntimeError( - `Unauthorized, bad token, wrong scopes or headers bad. ${result.error.details[0].code}`, - ); - } else if (response.status === 404) { - throw new RuntimeError(`Requested export is not available ${exportId}`); - } else if (response.status === 429) { - throw new RuntimeError("Too many requests"); - } - throw new RuntimeError(`Unknown request failure ${response.status}`); - } - - /** @type {ExportResponse} */ - const result = await response.json(); - return result; -}; - /** * Get the list of exports for the specified iModel at it's most current version. This will only return exports with {@link ITwinPlatform.ExportType} of 3DTILES. * * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. + * @private * * @param {string} iModelId iModel id * @@ -217,67 +167,4 @@ ITwinPlatform.getExports = async function (iModelId) { return result; }; -/** - * TODO: REMOVE THIS FUNCTION! Auto generation of exports for the 3DTILES type is planned very soon - * and will be the desired way of interacting with iModels through exports. This function is here - * just while we continue testing during the PR process. - * @deprecated - */ -ITwinPlatform.createExportForModelId = async function (iModelId) { - //>>includeStart('debug', pragmas.debug); - Check.typeOf.string("iModelId", iModelId); - if (!defined(ITwinPlatform.defaultAccessToken)) { - throw new DeveloperError("Must set ITwin.defaultAccessToken first"); - } - //>>includeEnd('debug') - - const requestOptions = { - method: "POST", - headers: { - Authorization: `Bearer ${ITwinPlatform.defaultAccessToken}`, - Accept: "application/vnd.bentley.itwin-platform.v1+json", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - iModelId, - exportType: ITwinPlatform.ExportType["3DTILES"], - }), - }; - - // initiate mesh export - const response = await fetch( - `${ITwinPlatform.apiEndpoint}mesh-export/`, - requestOptions, - ); - - if (!response.ok) { - const result = await response.json(); - if (response.status === 401) { - console.error( - result.error.code, - result.error.message, - result.error.details, - ); - throw new RuntimeError( - "Unauthorized, bad token, wrong scopes or headers bad", - ); - } else if (response.status === 403) { - console.error(result.error.code, result.error.message); - throw new RuntimeError("Not allowed, forbidden"); - } else if (response.status === 422) { - console.error(result.error.code, result.error.message); - console.error(result.error.details); - throw new RuntimeError("Unprocessable: Cannot create export job"); - } else if (response.status === 429) { - throw new RuntimeError("Too many requests"); - } - - throw new RuntimeError(`Unknown request failure ${response.status}`); - } - - /** @type {ExportResponse} */ - const result = await response.json(); - return result.export.id; -}; - export default ITwinPlatform; diff --git a/packages/engine/Source/Scene/ITwinData.js b/packages/engine/Source/Scene/ITwinData.js index f822b4d9bc97..ef5769431748 100644 --- a/packages/engine/Source/Scene/ITwinData.js +++ b/packages/engine/Source/Scene/ITwinData.js @@ -5,10 +5,6 @@ import ITwinPlatform from "../Core/ITwinPlatform.js"; import RuntimeError from "../Core/RuntimeError.js"; import Check from "../Core/Check.js"; -function delay(ms) { - return new Promise((res) => setTimeout(res, ms)); -} - /** * @param {Export} exportObj * @param {Cesium3DTileset.ConstructorOptions} [options] Object containing options to pass to an internally created {@link Cesium3DTileset}. @@ -19,24 +15,13 @@ async function loadExport(exportObj, options) { Check.defined("exportObj", exportObj); //>>includeEnd('debug') - let status = exportObj.status; - if (exportObj.request.exportType !== ITwinPlatform.ExportType["3DTILES"]) { throw new RuntimeError(`Wrong export type ${exportObj.request.exportType}`); } - - const timeoutAfter = 300000; - const start = Date.now(); - // wait until the export is complete - while (status !== ITwinPlatform.ExportStatus.Complete) { - await delay(5000); - exportObj = (await ITwinPlatform.getExport(exportObj.id)).export; - status = exportObj.status; - console.log(`Export is ${status}`); - - if (Date.now() - start > timeoutAfter) { - throw new RuntimeError("Export did not complete in time."); - } + if (exportObj.status !== ITwinPlatform.ExportStatus.Complete) { + throw new RuntimeError( + `Export is not completed. ${exportObj.id} - ${exportObj.status}`, + ); } // Convert the link to the tileset url while preserving the search paramaters @@ -62,33 +47,13 @@ async function loadExport(exportObj, options) { */ const ITwinData = {}; -/** - * Creates a {@link Cesium3DTileset} instance for the given export id. - * - * @function - * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. - * - * @param {string} exportId - * @param {Cesium3DTileset.ConstructorOptions} [options] Object containing options to pass to an internally created {@link Cesium3DTileset}. - * @returns {Promise} - * - * @throws {RuntimeError} Wrong export type - * @throws {RuntimeError} Export did not complete in time. - * - * @example - * TODO: example after API finalized - * @deprecated - */ -ITwinData.createTilesetFromExportId = async function (exportId, options) { - const result = await ITwinPlatform.getExport(exportId); - const tileset = await loadExport(result.export, options); - return tileset; -}; - /** * Loads the export for the specified iModel with the export type that CesiumJS can load and returns * a tileset created from that export. * + * If the export is not finished processing this will throw an error. It is up to the caller + * to re-attempt loading at a later time + * * @example * TODO: example after API finalized * @@ -98,8 +63,8 @@ ITwinData.createTilesetFromExportId = async function (exportId, options) { * @param {Cesium3DTileset.ConstructorOptions} [options] Object containing options to pass to an internally created {@link Cesium3DTileset}. * @returns {Promise} Will return undefined if there is no export for the given iModel id * - * @throws {RuntimeError} Wrong export type - * @throws {RuntimeError} Export did not complete in time. + * @throws {RuntimeError} Wrong export type [type] + * @throws {RuntimeError} Export is not completed. [id] - [status] */ ITwinData.createTilesetFromModelId = async function (iModelId, options) { const { exports } = await ITwinPlatform.getExports(iModelId); From 958756eea714c26998405fb6956532e2b4a86170 Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:53:29 -0500 Subject: [PATCH 10/22] update sandcastle for exports still processing --- Apps/Sandcastle/gallery/iTwin Demo.html | 27 ++++++++++++++++++-- packages/engine/Source/Core/ITwinPlatform.js | 2 ++ packages/engine/Source/Scene/ITwinData.js | 3 ++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Apps/Sandcastle/gallery/iTwin Demo.html b/Apps/Sandcastle/gallery/iTwin Demo.html index de045f350659..7bee11033cad 100644 --- a/Apps/Sandcastle/gallery/iTwin Demo.html +++ b/Apps/Sandcastle/gallery/iTwin Demo.html @@ -119,10 +119,33 @@ statusOutput.innerText = "Starting export"; const start = Date.now(); + const delay = (ms) => new Promise((res) => setTimeout(res, ms)); statusOutput.innerText = "Creating Tileset"; - - const tileset = await Cesium.ITwinData.createTilesetFromModelId(imodelId); + let tileset; + try { + tileset = await Cesium.ITwinData.createTilesetFromModelId(imodelId); + } catch (error) { + if (!error.message.includes("not completed")) { + throw error; + } + statusOutput.innerText = "Tileset not completed, retrying..."; + + const timeoutAfter = 300000; + // wait until the export is complete + while (Date.now() - start > timeoutAfter) { + await delay(5000); + try { + tileset = await Cesium.ITwinData.createTilesetFromModelId(imodelId); + break; + } catch (error) { + if (!error.message.includes("not completed")) { + throw error; + } + } + } + throw new Cesium.RuntimeError("Export did not complete in time."); + } scene.primitives.add(tileset); tileset.colorBlendMode = Cesium.Cesium3DTileColorBlendMode.REPLACE; diff --git a/packages/engine/Source/Core/ITwinPlatform.js b/packages/engine/Source/Core/ITwinPlatform.js index 134c5a553f83..defe07107bbb 100644 --- a/packages/engine/Source/Core/ITwinPlatform.js +++ b/packages/engine/Source/Core/ITwinPlatform.js @@ -135,6 +135,8 @@ ITwinPlatform.getExports = async function (iModelId) { const url = new URL(`${ITwinPlatform.apiEndpoint}mesh-export`); url.searchParams.set("iModelId", iModelId); url.searchParams.set("exportType", ITwinPlatform.ExportType["3DTILES"]); + // TODO: If we're only requesting the top 1 is there a chance it's `Invalid` instead of `Complete` + // and never possible to load it? url.searchParams.set("$top", "1"); url.searchParams.set("client", "CesiumJS"); /* global CESIUM_VERSION */ diff --git a/packages/engine/Source/Scene/ITwinData.js b/packages/engine/Source/Scene/ITwinData.js index ef5769431748..bfe560ec8440 100644 --- a/packages/engine/Source/Scene/ITwinData.js +++ b/packages/engine/Source/Scene/ITwinData.js @@ -55,7 +55,8 @@ const ITwinData = {}; * to re-attempt loading at a later time * * @example - * TODO: example after API finalized + * const tileset = await Cesium.ITwinData.createTilesetFromModelId(imodelId); + * viewer.scene.primitives.add(tileset); * * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * From 7049dbefa28b93f1898192d29eaefc9c0806ee84 Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:28:31 -0500 Subject: [PATCH 11/22] re-organize, resource instead of fetch, clean up types --- Apps/Sandcastle/gallery/iTwin Demo.html | 2 +- packages/engine/Source/Core/ITwinPlatform.js | 133 ++++++++----------- packages/engine/Source/Scene/ITwinData.js | 22 +-- 3 files changed, 71 insertions(+), 86 deletions(-) diff --git a/Apps/Sandcastle/gallery/iTwin Demo.html b/Apps/Sandcastle/gallery/iTwin Demo.html index 7bee11033cad..17e54c4f2294 100644 --- a/Apps/Sandcastle/gallery/iTwin Demo.html +++ b/Apps/Sandcastle/gallery/iTwin Demo.html @@ -134,7 +134,7 @@ const timeoutAfter = 300000; // wait until the export is complete while (Date.now() - start > timeoutAfter) { - await delay(5000); + await delay(15000); try { tileset = await Cesium.ITwinData.createTilesetFromModelId(imodelId); break; diff --git a/packages/engine/Source/Core/ITwinPlatform.js b/packages/engine/Source/Core/ITwinPlatform.js index defe07107bbb..734d2b150749 100644 --- a/packages/engine/Source/Core/ITwinPlatform.js +++ b/packages/engine/Source/Core/ITwinPlatform.js @@ -4,52 +4,6 @@ import DeveloperError from "./DeveloperError.js"; import Resource from "./Resource.js"; import RuntimeError from "./RuntimeError.js"; -/** - * @typedef {Object} GeometryOptions - * @property {boolean} includeLines - * @property {number} chordTol - * @property {number} angleTol - * @property {number} decimationTol - * @property {number} maxEdgeLength - * @property {number} minBRepFeatureSize - * @property {number} minLineStyleComponentSize - */ - -/** - * @typedef {Object} ViewDefinitionFilter - * @property {string[]} models Array of included model IDs. - * @property {string[]} categories Array of included category IDs. - * @property {string[]} neverDrawn Array of element IDs to filter out. - */ - -/** - * @typedef {Object} StartExport - * @property {string} iModelId - * @property {string} changesetId - * @property {ITwinPlatform.ExportType} exportType Type of mesh to create. Currently, only GLTF and 3DFT are supported and undocumented CESIUM option - * @property {GeometryOptions} geometryOptions - * @property {ViewDefinitionFilter} viewDefinitionFilter - */ - -/** - * @typedef {Object} Link - * @property {string} href - */ - -/** - * @typedef {Object} Export - * @property {string} id - * @property {string} displayName - * @property {ITwinPlatform.ExportStatus} status - * @property {StartExport} request - * @property {{mesh: Link}} _links - */ - -/** - * @typedef {Object} ExportResponse - * @property {Export} export - */ - /** * Default settings for accessing the iTwin platform. * @@ -63,6 +17,7 @@ const ITwinPlatform = {}; /** * Status states for a mesh-export export * @enum {string} + * @private */ ITwinPlatform.ExportStatus = Object.freeze({ NotStarted: "NotStarted", @@ -74,6 +29,7 @@ ITwinPlatform.ExportStatus = Object.freeze({ /** * Types of mesh-export exports. CesiumJS only supports loading 3DTILES type exports * @enum {string} + * @private */ ITwinPlatform.ExportType = Object.freeze({ IMODEL: "IMODEL", @@ -102,6 +58,35 @@ ITwinPlatform.apiEndpoint = new Resource({ url: "https://api.bentley.com", }); +/** + * @typedef {Object} ExportRequest + * @property {string} iModelId + * @property {string} changesetId + * @property {ITwinPlatform.ExportType} exportType Type of the export. CesiumJS only supports the 3DTILES type + */ + +/** + * @typedef {Object} Link + * @property {string} href + */ + +/** + * @typedef {Object} ExportRepresentation + * The export objects from get-exports when using return=representation + * @property {string} id Export id + * @property {string} displayName Name of the iModel + * @property {ITwinPlatform.ExportStatus} status Status of this export + * @property {string} lastModified + * @property {ExportRequest} request Object containing info about the export itself + * @property {{mesh: Link}} _links Object containing relevant links. For Exports this includes the access url for the mesh itself + */ + +/** + * @typedef {Object} GetExportsResponse + * @property {ExportRepresentation[]} exports The list of exports for the current page + * @property {{self: Link, next: Link | undefined, prev: Link | undefined}} _links Pagination links + */ + /** * Get the list of exports for the specified iModel at it's most current version. This will only return exports with {@link ITwinPlatform.ExportType} of 3DTILES. * @@ -115,7 +100,7 @@ ITwinPlatform.apiEndpoint = new Resource({ * @throws {RuntimeError} Unprocessable Entity * @throws {RuntimeError} Too many requests * @throws {RuntimeError} Unknown request failure - * @returns {Promise<{exports: Export[]}>} + * @returns {Promise} */ ITwinPlatform.getExports = async function (iModelId) { //>>includeStart('debug', pragmas.debug); @@ -125,48 +110,48 @@ ITwinPlatform.getExports = async function (iModelId) { } //>>includeEnd('debug') - const headers = { - Authorization: `Bearer ${ITwinPlatform.defaultAccessToken}`, - Accept: "application/vnd.bentley.itwin-platform.v1+json", - Prefer: "return=representation", // or return=minimal (the default) - }; - - // obtain export for specified export id - const url = new URL(`${ITwinPlatform.apiEndpoint}mesh-export`); - url.searchParams.set("iModelId", iModelId); - url.searchParams.set("exportType", ITwinPlatform.ExportType["3DTILES"]); - // TODO: If we're only requesting the top 1 is there a chance it's `Invalid` instead of `Complete` - // and never possible to load it? - url.searchParams.set("$top", "1"); - url.searchParams.set("client", "CesiumJS"); + const resource = new Resource({ + url: `${ITwinPlatform.apiEndpoint}mesh-export`, + headers: { + Authorization: `Bearer ${ITwinPlatform.defaultAccessToken}`, + Accept: "application/vnd.bentley.itwin-platform.v1+json", + Prefer: "return=representation", + }, + queryParameters: { + iModelId: iModelId, + exportType: ITwinPlatform.ExportType["3DTILES"], + // TODO: If we're only requesting the top 1 is there a chance it's `Invalid` instead of `Complete` + // and never possible to load it? + $top: "1", + client: "CesiumJS", + }, + }); /* global CESIUM_VERSION */ if (typeof CESIUM_VERSION !== "undefined") { - url.searchParams.set("clientVersion", CESIUM_VERSION); + resource.appendQueryParameters({ clientVersion: CESIUM_VERSION }); } - const response = await fetch(url, { headers }); - if (!response.ok) { - const result = await response.json(); - if (response.status === 401) { + try { + const response = await resource.fetchJson(); + return response; + } catch (error) { + const result = JSON.parse(error.response); + if (error.statusCode === 401) { throw new RuntimeError( `Unauthorized, bad token, wrong scopes or headers bad. ${result.error.details[0].code}`, ); - } else if (response.status === 403) { + } else if (error.statusCode === 403) { console.error(result.error.code, result.error.message); throw new RuntimeError("Not allowed, forbidden"); - } else if (response.status === 422) { + } else if (error.statusCode === 422) { throw new RuntimeError( `Unprocessable Entity:${result.error.code} ${result.error.message}`, ); - } else if (response.status === 429) { + } else if (error.statusCode === 429) { throw new RuntimeError("Too many requests"); } - throw new RuntimeError(`Unknown request failure ${response.status}`); + throw new RuntimeError(`Unknown request failure ${error.statusCode}`); } - - /** @type {{exports: Export[]}} */ - const result = await response.json(); - return result; }; export default ITwinPlatform; diff --git a/packages/engine/Source/Scene/ITwinData.js b/packages/engine/Source/Scene/ITwinData.js index bfe560ec8440..0659620bdb13 100644 --- a/packages/engine/Source/Scene/ITwinData.js +++ b/packages/engine/Source/Scene/ITwinData.js @@ -6,7 +6,17 @@ import RuntimeError from "../Core/RuntimeError.js"; import Check from "../Core/Check.js"; /** - * @param {Export} exportObj + * Methods for loading iTwin platform data into CesiumJS + * + * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. + * + * @see ITwinPlatform to set the API token and base api url + * @namespace ITwinData + */ +const ITwinData = {}; + +/** + * @param {ExportRepresentation} exportObj * @param {Cesium3DTileset.ConstructorOptions} [options] Object containing options to pass to an internally created {@link Cesium3DTileset}. * @returns {Promise} */ @@ -37,16 +47,6 @@ async function loadExport(exportObj, options) { return Cesium3DTileset.fromUrl(resource, options); } -/** - * Methods for loading iTwin platform data into CesiumJS - * - * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. - * - * @see ITwinPlatform to set the API token and access api functions - * @namespace ITwinData - */ -const ITwinData = {}; - /** * Loads the export for the specified iModel with the export type that CesiumJS can load and returns * a tileset created from that export. From 188e2bf3948fe95479f64fa6b5c6d287f01a4f18 Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:40:33 -0500 Subject: [PATCH 12/22] fix type generation --- packages/engine/Source/Core/ITwinPlatform.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/engine/Source/Core/ITwinPlatform.js b/packages/engine/Source/Core/ITwinPlatform.js index 734d2b150749..cbb8499aac0a 100644 --- a/packages/engine/Source/Core/ITwinPlatform.js +++ b/packages/engine/Source/Core/ITwinPlatform.js @@ -17,7 +17,6 @@ const ITwinPlatform = {}; /** * Status states for a mesh-export export * @enum {string} - * @private */ ITwinPlatform.ExportStatus = Object.freeze({ NotStarted: "NotStarted", @@ -29,7 +28,6 @@ ITwinPlatform.ExportStatus = Object.freeze({ /** * Types of mesh-export exports. CesiumJS only supports loading 3DTILES type exports * @enum {string} - * @private */ ITwinPlatform.ExportType = Object.freeze({ IMODEL: "IMODEL", From 3726464a6294ae1ddb2dcd486670f8b3aa10fbd0 Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:46:51 -0500 Subject: [PATCH 13/22] minor renaming and descriptions --- Apps/Sandcastle/gallery/iTwin Demo.html | 38 +++++------------------ packages/engine/Source/Scene/ITwinData.js | 6 ++-- 2 files changed, 10 insertions(+), 34 deletions(-) diff --git a/Apps/Sandcastle/gallery/iTwin Demo.html b/Apps/Sandcastle/gallery/iTwin Demo.html index 17e54c4f2294..29a95fe0747a 100644 --- a/Apps/Sandcastle/gallery/iTwin Demo.html +++ b/Apps/Sandcastle/gallery/iTwin Demo.html @@ -9,10 +9,10 @@ /> - - Cesium Demo + + iTwin iModel demo @@ -34,10 +34,11 @@ const { token } = await serviceResponse.json(); Cesium.ITwinPlatform.defaultAccessToken = token; + // TODO: remove/clean up when we pick the final sample iTwin we want to use // this is the iModel in the "Hello iTwinCesium" iTwin that we should all have access to // https://developer.bentley.com/my-itwins/b4a30036-0456-49ea-a439-3fcd9365e24e/home/ - // const imodelId = "2852c3d7-00c3-4b5d-a0ce-82bbde4f061e"; - const imodelId = "88673c1d-12b8-48f1-8beb-5000d0edbd0b"; + // const iModelId = "2852c3d7-00c3-4b5d-a0ce-82bbde4f061e"; + const iModelId = "88673c1d-12b8-48f1-8beb-5000d0edbd0b"; // Grabbed mapping from the iTwin Viewer const classes = { @@ -117,36 +118,11 @@ const statusOutput = document.querySelector("#status"); async function init() { statusOutput.innerText = "Starting export"; - const start = Date.now(); - const delay = (ms) => new Promise((res) => setTimeout(res, ms)); statusOutput.innerText = "Creating Tileset"; - let tileset; - try { - tileset = await Cesium.ITwinData.createTilesetFromModelId(imodelId); - } catch (error) { - if (!error.message.includes("not completed")) { - throw error; - } - statusOutput.innerText = "Tileset not completed, retrying..."; - - const timeoutAfter = 300000; - // wait until the export is complete - while (Date.now() - start > timeoutAfter) { - await delay(15000); - try { - tileset = await Cesium.ITwinData.createTilesetFromModelId(imodelId); - break; - } catch (error) { - if (!error.message.includes("not completed")) { - throw error; - } - } - } - throw new Cesium.RuntimeError("Export did not complete in time."); - } + const tileset = await Cesium.ITwinData.createTilesetFromIModelId(iModelId); scene.primitives.add(tileset); tileset.colorBlendMode = Cesium.Cesium3DTileColorBlendMode.REPLACE; diff --git a/packages/engine/Source/Scene/ITwinData.js b/packages/engine/Source/Scene/ITwinData.js index 0659620bdb13..70de32fe5e9f 100644 --- a/packages/engine/Source/Scene/ITwinData.js +++ b/packages/engine/Source/Scene/ITwinData.js @@ -10,7 +10,7 @@ import Check from "../Core/Check.js"; * * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * - * @see ITwinPlatform to set the API token and base api url + * @see ITwinPlatform to set the API token and base API URL * @namespace ITwinData */ const ITwinData = {}; @@ -55,7 +55,7 @@ async function loadExport(exportObj, options) { * to re-attempt loading at a later time * * @example - * const tileset = await Cesium.ITwinData.createTilesetFromModelId(imodelId); + * const tileset = await Cesium.ITwinData.createTilesetFromIModelId(iModelId); * viewer.scene.primitives.add(tileset); * * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. @@ -67,7 +67,7 @@ async function loadExport(exportObj, options) { * @throws {RuntimeError} Wrong export type [type] * @throws {RuntimeError} Export is not completed. [id] - [status] */ -ITwinData.createTilesetFromModelId = async function (iModelId, options) { +ITwinData.createTilesetFromIModelId = async function (iModelId, options) { const { exports } = await ITwinPlatform.getExports(iModelId); const cesiumExport = exports[0]; if (!defined(cesiumExport)) { From d2055b277c72665b001fd08003d03913d0bd5b75 Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:53:23 -0500 Subject: [PATCH 14/22] pull out dev auth server --- .prettierignore | 1 - itwin-oauth-demo/.gitignore | 1 - itwin-oauth-demo/index.html | 34 ------------------- itwin-oauth-demo/server.js | 66 ------------------------------------- 4 files changed, 102 deletions(-) delete mode 100644 itwin-oauth-demo/.gitignore delete mode 100644 itwin-oauth-demo/index.html delete mode 100644 itwin-oauth-demo/server.js diff --git a/.prettierignore b/.prettierignore index e0d51a3709d0..c46eee96cb50 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,7 +11,6 @@ !packages/**/ !Specs/**/ !Tools/**/ -!itwin-oauth-demo/**/ !**/*.js !**/*.cjs diff --git a/itwin-oauth-demo/.gitignore b/itwin-oauth-demo/.gitignore deleted file mode 100644 index d344ba6b06cb..000000000000 --- a/itwin-oauth-demo/.gitignore +++ /dev/null @@ -1 +0,0 @@ -config.json diff --git a/itwin-oauth-demo/index.html b/itwin-oauth-demo/index.html deleted file mode 100644 index af6bc98d6499..000000000000 --- a/itwin-oauth-demo/index.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - Auth! - - - - - diff --git a/itwin-oauth-demo/server.js b/itwin-oauth-demo/server.js deleted file mode 100644 index 8a935343622e..000000000000 --- a/itwin-oauth-demo/server.js +++ /dev/null @@ -1,66 +0,0 @@ -import express from "express"; -import { readFileSync, writeFileSync } from "fs"; -import { dirname, join } from "path"; -import { exit } from "process"; -import { fileURLToPath } from "url"; - -let config = { - serviceapp: { - clientId: "", - clientSecret: "", - }, - port: 3000, -}; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const configPath = join(__dirname, "./config.json"); -try { - const configFile = readFileSync(configPath, { encoding: "utf-8" }); - config = JSON.parse(configFile); -} catch { - console.log("config file missing, default written to", configPath); - console.log("Please update the config with the desired values"); - writeFileSync(configPath, JSON.stringify(config, undefined, 2)); - exit(1); -} - -const app = express(); -const port = config.port ?? 3000; - -// eslint-disable-next-line no-unused-vars -app.get("/service", async (req, res) => { - console.log("/service request received"); - - const body = new URLSearchParams(); - body.set("grant_type", "client_credentials"); - body.set("client_id", config.serviceapp.clientId); - body.set("client_secret", config.serviceapp.clientSecret); - body.set("scope", "itwin-platform"); - - const response = await fetch("https://ims.bentley.com/connect/token", { - method: "POST", - body, - }); - - const result = await response.json(); - - res.setHeader("Access-Control-Allow-Origin", "*"); - - if (!response.ok || !result) { - console.log(" bad response/no result"); - res.status(response.status).send(); - return; - } - const { access_token } = result; - if (access_token) { - console.log(" token acquired, returned"); - res.status(200).send({ token: access_token }); - return; - } - console.log(" token not found"); - res.status(404).send("token not found"); -}); - -app.listen(port, () => { - console.log(`Server listening on port ${port}`); -}); From 47e1642eabffb26f301661f1ea1815a4f90bb38e Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:17:31 -0500 Subject: [PATCH 15/22] condense code, update docs --- CHANGES.md | 1 + packages/engine/Source/Core/ITwinPlatform.js | 23 +++--- packages/engine/Source/Scene/ITwinData.js | 80 +++++++++----------- 3 files changed, 50 insertions(+), 54 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 49e2c0056ac6..f6b0782a351a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ ##### Additions :tada: - Added `Entity.trackingReferenceFrame` property to allow tracking entities in their own inertial reference frame. [#12194](https://github.com/CesiumGS/cesium/pull/12194) +- Added a new integration with the iTwin Platform to easily load iModels directly in the viewer. Use `ITwinPlatform.defaultAccessToken` to set the access token then use `ITwinData.createTilesetFromIModelId(iModelId)` to load the iModel as a `Cesium3DTileset`. [#12289](https://github.com/CesiumGS/cesium/pull/12289) ##### Fixes :wrench: diff --git a/packages/engine/Source/Core/ITwinPlatform.js b/packages/engine/Source/Core/ITwinPlatform.js index cbb8499aac0a..a21a2f6e0276 100644 --- a/packages/engine/Source/Core/ITwinPlatform.js +++ b/packages/engine/Source/Core/ITwinPlatform.js @@ -58,6 +58,7 @@ ITwinPlatform.apiEndpoint = new Resource({ /** * @typedef {Object} ExportRequest + * @private * @property {string} iModelId * @property {string} changesetId * @property {ITwinPlatform.ExportType} exportType Type of the export. CesiumJS only supports the 3DTILES type @@ -65,12 +66,14 @@ ITwinPlatform.apiEndpoint = new Resource({ /** * @typedef {Object} Link + * @private * @property {string} href */ /** * @typedef {Object} ExportRepresentation * The export objects from get-exports when using return=representation + * @private * @property {string} id Export id * @property {string} displayName Name of the iModel * @property {ITwinPlatform.ExportStatus} status Status of this export @@ -81,24 +84,21 @@ ITwinPlatform.apiEndpoint = new Resource({ /** * @typedef {Object} GetExportsResponse + * @private * @property {ExportRepresentation[]} exports The list of exports for the current page * @property {{self: Link, next: Link | undefined, prev: Link | undefined}} _links Pagination links */ /** - * Get the list of exports for the specified iModel at it's most current version. This will only return exports with {@link ITwinPlatform.ExportType} of 3DTILES. + * Get the list of exports for the specified iModel at it's most current version. + * This will only return the top 5 exports with {@link ITwinPlatform.ExportType} of 3DTILES. * - * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * @private * * @param {string} iModelId iModel id - * - * @throws {RuntimeError} Unauthorized, bad token, wrong scopes or headers bad. - * @throws {RuntimeError} Not allowed, forbidden - * @throws {RuntimeError} Unprocessable Entity - * @throws {RuntimeError} Too many requests - * @throws {RuntimeError} Unknown request failure * @returns {Promise} + * + * @throws {RuntimeError} If the iTwin API request is not successful */ ITwinPlatform.getExports = async function (iModelId) { //>>includeStart('debug', pragmas.debug); @@ -118,9 +118,10 @@ ITwinPlatform.getExports = async function (iModelId) { queryParameters: { iModelId: iModelId, exportType: ITwinPlatform.ExportType["3DTILES"], - // TODO: If we're only requesting the top 1 is there a chance it's `Invalid` instead of `Complete` - // and never possible to load it? - $top: "1", + // With the export auto-generation it will auto-delete the 6th export so + // there should never be more than 5 results. Just request them all and parse + // for ones that are COMPLETE + $top: "5", client: "CesiumJS", }, }); diff --git a/packages/engine/Source/Scene/ITwinData.js b/packages/engine/Source/Scene/ITwinData.js index 70de32fe5e9f..219480c52ba2 100644 --- a/packages/engine/Source/Scene/ITwinData.js +++ b/packages/engine/Source/Scene/ITwinData.js @@ -3,7 +3,6 @@ import defined from "../Core/defined.js"; import Resource from "../Core/Resource.js"; import ITwinPlatform from "../Core/ITwinPlatform.js"; import RuntimeError from "../Core/RuntimeError.js"; -import Check from "../Core/Check.js"; /** * Methods for loading iTwin platform data into CesiumJS @@ -16,27 +15,51 @@ import Check from "../Core/Check.js"; const ITwinData = {}; /** - * @param {ExportRepresentation} exportObj - * @param {Cesium3DTileset.ConstructorOptions} [options] Object containing options to pass to an internally created {@link Cesium3DTileset}. - * @returns {Promise} + * Create a {@link Cesium3DTileset} for the given iModel id using the mesh export service. + * + * If there is not a completed export available for the given iModel id this function will return undefined. + * We recommend waiting 10-20 seconds and trying to load the tileset again. + * If all exports are Invalid this will throw an error. In that case there's likely something wrong with the iModel itself + * + * @example + * 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}. + * @returns {Promise} Will return 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 */ -async function loadExport(exportObj, options) { - //>>includeStart('debug', pragmas.debug); - Check.defined("exportObj", exportObj); - //>>includeEnd('debug') +ITwinData.createTilesetFromIModelId = async function (iModelId, options) { + const { exports } = await ITwinPlatform.getExports(iModelId); - if (exportObj.request.exportType !== ITwinPlatform.ExportType["3DTILES"]) { - throw new RuntimeError(`Wrong export type ${exportObj.request.exportType}`); - } - if (exportObj.status !== ITwinPlatform.ExportStatus.Complete) { + if ( + exports.every((exportObj) => { + return exportObj.status === ITwinPlatform.ExportStatus.Invalid; + }) + ) { throw new RuntimeError( - `Export is not completed. ${exportObj.id} - ${exportObj.status}`, + `All exports for this iModel are Invalid: ${iModelId}`, ); } + const completeExport = exports.find((exportObj) => { + return exportObj.status === ITwinPlatform.ExportStatus.Complete; + }); + + if (!defined(completeExport)) { + return; + } + // Convert the link to the tileset url while preserving the search paramaters // This link is only valid 1 hour - const baseUrl = new URL(exportObj._links.mesh.href); + const baseUrl = new URL(completeExport._links.mesh.href); baseUrl.pathname = `${baseUrl.pathname}/tileset.json`; const tilesetUrl = baseUrl.toString(); @@ -45,35 +68,6 @@ async function loadExport(exportObj, options) { }); return Cesium3DTileset.fromUrl(resource, options); -} - -/** - * Loads the export for the specified iModel with the export type that CesiumJS can load and returns - * a tileset created from that export. - * - * If the export is not finished processing this will throw an error. It is up to the caller - * to re-attempt loading at a later time - * - * @example - * const tileset = await Cesium.ITwinData.createTilesetFromIModelId(iModelId); - * 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 an internally created {@link Cesium3DTileset}. - * @returns {Promise} Will return undefined if there is no export for the given iModel id - * - * @throws {RuntimeError} Wrong export type [type] - * @throws {RuntimeError} Export is not completed. [id] - [status] - */ -ITwinData.createTilesetFromIModelId = async function (iModelId, options) { - const { exports } = await ITwinPlatform.getExports(iModelId); - const cesiumExport = exports[0]; - if (!defined(cesiumExport)) { - return; - } - return loadExport(cesiumExport, options); }; export default ITwinData; From 990004b98526c55eacddf3ceb639680fb824ec70 Mon Sep 17 00:00:00 2001 From: jjspace <8007967+jjspace@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:52:25 -0500 Subject: [PATCH 16/22] streamline sandcastle with new imodels --- Apps/Sandcastle/gallery/iTwin Demo.html | 138 ++++++++++++++-------- CHANGES.md | 2 +- packages/engine/Source/Scene/ITwinData.js | 1 + 3 files changed, 94 insertions(+), 47 deletions(-) diff --git a/Apps/Sandcastle/gallery/iTwin Demo.html b/Apps/Sandcastle/gallery/iTwin Demo.html index 29a95fe0747a..cab4f922ad4f 100644 --- a/Apps/Sandcastle/gallery/iTwin Demo.html +++ b/Apps/Sandcastle/gallery/iTwin Demo.html @@ -24,24 +24,22 @@

Loading...

- Initializing +
+