Skip to content
Merged
3 changes: 3 additions & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* [auth](auth/overview.md)
* [login](auth/login.md)
* [logout](auth/logout.md)
* [api](api.md)
* [itwin](itwin/overview.md)
* [create](itwin/create.md)
* [update](itwin/update.md)
Expand All @@ -20,6 +21,8 @@
* [delete](imodel/delete.md)
* [info](imodel/info.md)
* [list](imodel/list.md)
* [view](imodel/view/overview.md)
* [cesium-sandcastle](imodel/view/cesium-sandcastle.md)
* [changeset](imodel/changeset/overview.md)
* [named-version](imodel/named-version/overview.md)
* [connection](imodel/connection/overview.md)
Expand Down
1 change: 1 addition & 0 deletions docs/imodel/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Work with iModels of an iTwin.
- [itp imodel delete](imodel/delete.md)
- [itp imodel info](imodel/info.md)
- [itp imodel list](imodel/list.md)
- [itp imodel view](imodel/view/overview.md)
- [itp imodel changeset](imodel/changeset/overview.md)
- [itp imodel namedversion](imodel/named-version/overview.md)
- [itp imodel connection](imodel/connection/overview.md)
31 changes: 31 additions & 0 deletions docs/imodel/view/cesium-sandcastle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# itp imodel view cesium-sandcastle

Setup iModel and get URL to view it in Cesium Sandcastle.

## Options

- **`--changeset-id`**
Changeset id to be viewed in Cesium Sandcastle.
**Type:** `string` **Required:** Yes

- **`-m, --imodel-id`**
iModel id to be viewed in Cesium Sandcastle.
**Type:** `string` **Required:** Yes

- **`--open`**
Open the URL in the browser.
**Type:** `boolean` **Required:** No

## Examples

```bash
# Example 1: Get a link to a specific changeset of an iModel in Cesium Sandcastle
itp imodel view cesium-sandcastle --imodel-id "5e19bee0-3aea-4355-a9f0-c6df9989ee7d" --changeset-id "2f3b4a8c92d747d5c8a8b2f9cde6742e5d74b3b5"

# Example 2: Get a link to a specific changeset of an iModel in Cesium Sandcastle and open the URL in the browser
itp imodel view cesium-sandcastle --imodel-id "5e19bee0-3aea-4355-a9f0-c6df9989ee7d" --changeset-id "2f3b4a8c92d747d5c8a8b2f9cde6742e5d74b3b5" --open
```

## API Reference

[Cesium Sandcastle](https://cesium.com/docs/sandcastle/)
7 changes: 7 additions & 0 deletions docs/imodel/view/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# itp imodel view

Work with views for an iModel.

## Available Commands

- [itp imodel view cesium-sandcastle](imodel/view/cesium-sandcastle.md)
21 changes: 18 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "itp",
"description": "Work seamlessly with the iTwin Platform",
"version": "0.0.3",
"version": "0.0.4",
"author": "Bentley systems, Incorporated",
"bin": {
"itp": "./bin/run.js"
Expand All @@ -21,14 +21,16 @@
"dotenv": "^16.4.5",
"fs-extra": "^8.1.0",
"jwt-decode": "^4.0.0",
"open": "^10.1.0"
"open": "^10.1.0",
"pako": "^2.1.0"
},
"devDependencies": {
"@oclif/prettier-config": "^0.2.1",
"@oclif/test": "^4",
"@types/chai": "^4",
"@types/mocha": "^10",
"@types/node": "^18.19.64",
"@types/pako": "^2.0.3",
"chai": "^4",
"eslint": "^8",
"eslint-config-oclif": "^5",
Expand Down
6 changes: 3 additions & 3 deletions src/commands/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,26 +51,26 @@ export default class ApiRequest extends BaseCommand {

const mappedHeaders: Record<string, string> = flags.header?.reduce((acc, header) => {
const [key, value] = header.split(":");
acc[key] = value;
acc[key] = value.trim();
return acc;
}, {} as Record<string, string>) || {};

const query: Query[] | undefined = flags.query?.map((query) => {
const [key, value] = query.split(":");
return { key, value };
return { key: key.trim(), value: value.trim() };
}) || undefined;

const client = await this.getITwinApiClient();

const requestOptions = {
apiPath: flags.path,
apiVersionHeader: flags["version-header"],
body: flags.body ? JSON.parse(flags.body) : undefined,
headers: mappedHeaders,
method: flags.method as "DELETE" | "GET" | "PATCH" | "POST" | "PUT",
query
};


if (flags["empty-response"]) {
await client.sendRequestNoResponse(requestOptions);
return this.logAndReturnResult({result: "success"});
Expand Down
180 changes: 180 additions & 0 deletions src/commands/imodel/view/cesium-sandcastle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/

import { Flags } from "@oclif/core";
import open from 'open';
import { deflate } from "pako";

import BaseCommand from "../../../extensions/base-command.js";
import { link, links } from "../../../services/general-models/links.js";

export default class CesiumSandcastle extends BaseCommand {
static description = "Setup iModel and get url to view it in Cesium Sandcastle";

static flags = {
"changeset-id": Flags.string({
description: "Changeset id to be viewed in Cesium Sandcastle.",
required: true
}),
"imodel-id": Flags.string({
Comment thread
Nadegamra marked this conversation as resolved.
char: "m",
description: "iModel id to be viewed in Cesium Sandcastle.",
required: true
}),
"open": Flags.boolean({
description: "Open the URL in the browser.",
required: false,
}),
};

async createExport(iModelId: string, changesetId: string): Promise<ExportInfo> {
const args = [
"--method", "POST",
"--path", "mesh-export",
"--version-header", "application/vnd.bentley.itwin-platform.v1+json",
"--body", JSON.stringify({
changesetId,
exportType: "CESIUM",
iModelId
}),
];

const created = await this.runCommand<ExportCreateResponse>("api", args);
return created.export;
}

async getExports(iModelId: string) : Promise<ExportInfo[]> {
const exportArgs = [
"--method", "GET",
"--path", "mesh-export/",
"--version-header", "application/vnd.bentley.itwin-platform.v1+json",
"--query", `iModelId: ${iModelId}`,
"--header", "Prefer: return=representation"
];
const response = await this.runCommand<ExportResponse>("api", exportArgs);
return response.exports;
}

async getOrCreateExport(iModelId: string, changesetId: string): Promise<ExportInfo> {
this.log(`Getting existing exports for iModel: ${iModelId} and changeset: ${changesetId}`);
let existingExports = await this.getExports(iModelId);
const existingExport = existingExports.find((exp) => exp.request.exportType === "CESIUM" && exp.request.changesetId === changesetId);

if (existingExport !== undefined) {
this.log(`Found existing export with id: ${existingExport.id}`);
return existingExport;
}

this.log(`Creating new export for iModel: ${iModelId} and changeset: ${changesetId}`);
let newExport = await this.createExport(iModelId, changesetId);
while (newExport.status !== "Complete") {
this.log(`Export status is ${newExport.status}. Waiting for export to complete...`);
// eslint-disable-next-line no-await-in-loop
existingExports = await this.getExports(iModelId);
newExport = existingExports.find((exp) => exp.id === newExport.id)!;
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {setTimeout(resolve, 5000)});
}

this.log(`Export completed successfully`);
return newExport;
}

async run() {
const { flags } = await this.parse(CesiumSandcastle);

const exportInfo : ExportInfo = await this.getOrCreateExport(flags["imodel-id"], flags["changeset-id"]);

this.log(`Extracting tileset URL from export info`);
const tilesetUrl : string = extractTileSetUrl(exportInfo);

const data = [
jsData(tilesetUrl),
htmlData(),
];

const url = `https://sandcastle.cesium.com/#c=${makeCompressedBase64String(data)}`;

this.log(`Cesium Sandcastle URL:`);
this.log(url);

if (flags.open) {
this.log(`Opening URL in browser...`);
open(url);
}

return this.logAndReturnResult({ url });
}
}


type ExportResponse = {
_links: links
exports: ExportInfo[],
}

type ExportCreateResponse = {
export: ExportInfo
}

type ExportInfo = {
_links: {
mesh: link
},
displayName: string,
error?: string,
id: string,
lastModified: Date,
request: ExportRequest,
status: "Complete" | "InProgress" | "Invalid" | "NotStarted",
}

type ExportRequest = {
changesetId: string,
exportType: "3DFT" | "3DTiles" | "CESIUM" | "IMODEL",
iModelId: string,
}

function extractTileSetUrl(exportInfo: ExportInfo): string {
if(exportInfo._links.mesh.href === undefined) {
throw new Error(`No tileset url found for export info id: ${exportInfo.id}`);
}

const urlParts = exportInfo._links.mesh.href.split("?");
return urlParts[0] + "/tileset.json?" + urlParts[1];
}

function makeCompressedBase64String(data: string[]) : string {
let jsonString = JSON.stringify(data);
jsonString = jsonString.slice(2, 2 + jsonString.length - 4);
let base64String = Buffer.from(
deflate(jsonString, { raw: true })
).toString('base64');
base64String = base64String.replace(/=+$/, ''); // remove padding

return base64String;
}

function htmlData() : string {
return `
<style>
@import url(../templates/bucket.css);
</style>

<div id="cesiumContainer" class="fullSize"></div>
`;
}

function jsData(tilesetUrl: string) : string {
return `
const viewer = new Cesium.Viewer("cesiumContainer");
viewer.scene.globe.show = true;
viewer.scene.debugShowFramesPerSecond = true;
const tilesetUrl = '${tilesetUrl}';
const tileset = await Cesium.Cesium3DTileset.fromUrl(tilesetUrl);
viewer.scene.primitives.add(tileset);
viewer.zoomTo(tileset);
`;
}