|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. |
| 3 | +* See LICENSE.md in the project root for license terms and full copyright notice. |
| 4 | +*--------------------------------------------------------------------------------------------*/ |
| 5 | + |
| 6 | +import { Flags } from "@oclif/core"; |
| 7 | +import open from 'open'; |
| 8 | +import { deflate } from "pako"; |
| 9 | + |
| 10 | +import BaseCommand from "../../../extensions/base-command.js"; |
| 11 | +import { link, links } from "../../../services/general-models/links.js"; |
| 12 | + |
| 13 | +export default class CesiumSandcastle extends BaseCommand { |
| 14 | + static description = "Setup iModel and get url to view it in Cesium Sandcastle"; |
| 15 | + |
| 16 | + static flags = { |
| 17 | + "changeset-id": Flags.string({ |
| 18 | + description: "Changeset id to be viewed in Cesium Sandcastle.", |
| 19 | + required: true |
| 20 | + }), |
| 21 | + "imodel-id": Flags.string({ |
| 22 | + char: "m", |
| 23 | + description: "iModel id to be viewed in Cesium Sandcastle.", |
| 24 | + required: true |
| 25 | + }), |
| 26 | + "open": Flags.boolean({ |
| 27 | + description: "Open the URL in the browser.", |
| 28 | + required: false, |
| 29 | + }), |
| 30 | + }; |
| 31 | + |
| 32 | + async createExport(iModelId: string, changesetId: string): Promise<ExportInfo> { |
| 33 | + const args = [ |
| 34 | + "--method", "POST", |
| 35 | + "--path", "mesh-export", |
| 36 | + "--version-header", "application/vnd.bentley.itwin-platform.v1+json", |
| 37 | + "--body", JSON.stringify({ |
| 38 | + changesetId, |
| 39 | + exportType: "CESIUM", |
| 40 | + iModelId |
| 41 | + }), |
| 42 | + ]; |
| 43 | + |
| 44 | + const created = await this.runCommand<ExportCreateResponse>("api", args); |
| 45 | + return created.export; |
| 46 | + } |
| 47 | + |
| 48 | + async getExports(iModelId: string) : Promise<ExportInfo[]> { |
| 49 | + const exportArgs = [ |
| 50 | + "--method", "GET", |
| 51 | + "--path", "mesh-export/", |
| 52 | + "--version-header", "application/vnd.bentley.itwin-platform.v1+json", |
| 53 | + "--query", `iModelId: ${iModelId}`, |
| 54 | + "--header", "Prefer: return=representation" |
| 55 | + ]; |
| 56 | + const response = await this.runCommand<ExportResponse>("api", exportArgs); |
| 57 | + return response.exports; |
| 58 | + } |
| 59 | + |
| 60 | + async getOrCreateExport(iModelId: string, changesetId: string): Promise<ExportInfo> { |
| 61 | + this.log(`Getting existing exports for iModel: ${iModelId} and changeset: ${changesetId}`); |
| 62 | + let existingExports = await this.getExports(iModelId); |
| 63 | + const existingExport = existingExports.find((exp) => exp.request.exportType === "CESIUM" && exp.request.changesetId === changesetId); |
| 64 | + |
| 65 | + if (existingExport !== undefined) { |
| 66 | + this.log(`Found existing export with id: ${existingExport.id}`); |
| 67 | + return existingExport; |
| 68 | + } |
| 69 | + |
| 70 | + this.log(`Creating new export for iModel: ${iModelId} and changeset: ${changesetId}`); |
| 71 | + let newExport = await this.createExport(iModelId, changesetId); |
| 72 | + while (newExport.status !== "Complete") { |
| 73 | + this.log(`Export status is ${newExport.status}. Waiting for export to complete...`); |
| 74 | + // eslint-disable-next-line no-await-in-loop |
| 75 | + existingExports = await this.getExports(iModelId); |
| 76 | + newExport = existingExports.find((exp) => exp.id === newExport.id)!; |
| 77 | + // eslint-disable-next-line no-await-in-loop |
| 78 | + await new Promise((resolve) => {setTimeout(resolve, 5000)}); |
| 79 | + } |
| 80 | + |
| 81 | + this.log(`Export completed successfully`); |
| 82 | + return newExport; |
| 83 | + } |
| 84 | + |
| 85 | + async run() { |
| 86 | + const { flags } = await this.parse(CesiumSandcastle); |
| 87 | + |
| 88 | + const exportInfo : ExportInfo = await this.getOrCreateExport(flags["imodel-id"], flags["changeset-id"]); |
| 89 | + |
| 90 | + this.log(`Extracting tileset URL from export info`); |
| 91 | + const tilesetUrl : string = extractTileSetUrl(exportInfo); |
| 92 | + |
| 93 | + const data = [ |
| 94 | + jsData(tilesetUrl), |
| 95 | + htmlData(), |
| 96 | + ]; |
| 97 | + |
| 98 | + const url = `https://sandcastle.cesium.com/#c=${makeCompressedBase64String(data)}`; |
| 99 | + |
| 100 | + this.log(`Cesium Sandcastle URL:`); |
| 101 | + this.log(url); |
| 102 | + |
| 103 | + if (flags.open) { |
| 104 | + this.log(`Opening URL in browser...`); |
| 105 | + open(url); |
| 106 | + } |
| 107 | + |
| 108 | + return this.logAndReturnResult({ url }); |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | + |
| 113 | +type ExportResponse = { |
| 114 | + _links: links |
| 115 | + exports: ExportInfo[], |
| 116 | +} |
| 117 | + |
| 118 | +type ExportCreateResponse = { |
| 119 | + export: ExportInfo |
| 120 | +} |
| 121 | + |
| 122 | +type ExportInfo = { |
| 123 | + _links: { |
| 124 | + mesh: link |
| 125 | + }, |
| 126 | + displayName: string, |
| 127 | + error?: string, |
| 128 | + id: string, |
| 129 | + lastModified: Date, |
| 130 | + request: ExportRequest, |
| 131 | + status: "Complete" | "InProgress" | "Invalid" | "NotStarted", |
| 132 | +} |
| 133 | + |
| 134 | +type ExportRequest = { |
| 135 | + changesetId: string, |
| 136 | + exportType: "3DFT" | "3DTiles" | "CESIUM" | "IMODEL", |
| 137 | + iModelId: string, |
| 138 | +} |
| 139 | + |
| 140 | +function extractTileSetUrl(exportInfo: ExportInfo): string { |
| 141 | + if(exportInfo._links.mesh.href === undefined) { |
| 142 | + throw new Error(`No tileset url found for export info id: ${exportInfo.id}`); |
| 143 | + } |
| 144 | + |
| 145 | + const urlParts = exportInfo._links.mesh.href.split("?"); |
| 146 | + return urlParts[0] + "/tileset.json?" + urlParts[1]; |
| 147 | +} |
| 148 | + |
| 149 | +function makeCompressedBase64String(data: string[]) : string { |
| 150 | + let jsonString = JSON.stringify(data); |
| 151 | + jsonString = jsonString.slice(2, 2 + jsonString.length - 4); |
| 152 | + let base64String = Buffer.from( |
| 153 | + deflate(jsonString, { raw: true }) |
| 154 | + ).toString('base64'); |
| 155 | + base64String = base64String.replace(/=+$/, ''); // remove padding |
| 156 | + |
| 157 | + return base64String; |
| 158 | +} |
| 159 | + |
| 160 | +function htmlData() : string { |
| 161 | + return ` |
| 162 | +<style> |
| 163 | +@import url(../templates/bucket.css); |
| 164 | +</style> |
| 165 | +
|
| 166 | +<div id="cesiumContainer" class="fullSize"></div> |
| 167 | +`; |
| 168 | +} |
| 169 | + |
| 170 | +function jsData(tilesetUrl: string) : string { |
| 171 | + return ` |
| 172 | +const viewer = new Cesium.Viewer("cesiumContainer"); |
| 173 | +viewer.scene.globe.show = true; |
| 174 | +viewer.scene.debugShowFramesPerSecond = true; |
| 175 | +const tilesetUrl = '${tilesetUrl}'; |
| 176 | +const tileset = await Cesium.Cesium3DTileset.fromUrl(tilesetUrl); |
| 177 | +viewer.scene.primitives.add(tileset); |
| 178 | +viewer.zoomTo(tileset); |
| 179 | +`; |
| 180 | +} |
0 commit comments