/////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2002-2021, Open Design Alliance (the "Alliance"). // All rights reserved. // // This software and its documentation and related materials are owned by // the Alliance. The software may only be incorporated into application // programs owned by members of the Alliance, subject to a signed // Membership Agreement and Supplemental Software License Agreement with the // Alliance. The structure and organization of this software are the valuable // trade secrets of the Alliance and its suppliers. The software is also // protected by copyright law and international treaty provisions. Application // programs incorporating this software must include the following statement // with their copyright notices: // // This application incorporates Open Design Alliance software pursuant to a // license agreement with Open Design Alliance. // Open Design Alliance Copyright (C) 2002-2021 by Open Design Alliance. // All rights reserved. // // By use of this software, its documentation or related materials, you // acknowledge and accept the above terms. /////////////////////////////////////////////////////////////////////////////// import { IHttpClient } from "./IHttpClient"; import { IAssociatedFileData, IAssemblyVersionInfo } from "./IAssembly"; import { IShortUserDescription } from "./IUser"; import { Model } from "./Model"; import { ClashTest } from "./ClashTest"; import { json, downloadProgress, downloadPartOfFile, waitFor, userFullName, userInitials } from "./impl/Utils"; import { FetchError } from "./impl/FetchError"; /** * The class representing a `assembly` entity. */ export class Assembly { private _data: any; private _useVersion: number | undefined; public httpClient: IHttpClient; public path: string; /** * @param data - An object that implements assembly data storage. * @param httpClient - Http client. */ constructor(data: any, httpClient: IHttpClient) { this.path = `/assemblies/${data.id}`; this.httpClient = httpClient; this.data = data; } public appendVersionParam(relativePath: string): string { if (this._useVersion === undefined) return relativePath; const delimiter = relativePath.includes("?") ? "&" : "?"; return `${relativePath}${delimiter}version=${this._useVersion}`; } protected internalGet(relativePath: string, signal?: AbortSignal) { relativePath = this.appendVersionParam(relativePath); return this.httpClient.get(`${this.path}${relativePath}`, signal); } protected internalPost( relativePath: string, body?: ArrayBuffer | Blob | globalThis.File | FormData | object | string | null ) { relativePath = this.appendVersionParam(relativePath); return this.httpClient.post(`${this.path}${relativePath}`, body); } protected internalPut( relativePath: string, body?: ArrayBuffer | Blob | globalThis.File | FormData | object | string | null ) { relativePath = this.appendVersionParam(relativePath); return this.httpClient.put(`${this.path}${relativePath}`, body); } protected internalDelete(relativePath: string) { relativePath = this.appendVersionParam(relativePath); return this.httpClient.delete(`${this.path}${relativePath}`); } async partialDownloadResource( dataId: string, onProgress?: (progress: number, downloaded: Uint8Array) => void, signal?: AbortSignal ): Promise { let pathname = `${this.path}/downloads/${dataId}`; pathname = this.appendVersionParam(pathname); // TODO: replace with $get to handle fetch errors const response = await fetch(`${this.httpClient.serverUrl}${pathname}`, { headers: this.httpClient.headers, signal, }); // TODO: use ReadableStream pipeTo() const contentLength = response.headers.get("Content-Length") ?? ""; const total = parseInt(contentLength, 10); const reader = response.body.getReader(); let loaded = 0; while (true) { const { done, value } = await reader.read(); if (done) break; loaded += value.byteLength; if (typeof onProgress === "function") onProgress(loaded / total, value); } } downloadFileRange( requestId: number, records: any | null, dataId: string, onProgress?: (progress: number, downloaded: Uint8Array, requestId: number) => void, signal?: AbortSignal ): Promise { let pathname = `${this.path}/downloads/${dataId}?requestId=${requestId}`; pathname = this.appendVersionParam(pathname); return downloadPartOfFile( requestId, records, `${this.httpClient.serverUrl}${pathname}`, this.httpClient.headers, onProgress, signal ); } // Reserved for future use get activeVersion(): number { return this.data.activeVersion; } /** * List of unique files from which the assembly was created. * * @readonly */ get associatedFiles(): IAssociatedFileData[] { return this.data.associatedFiles; } /** * Assembly creation time (UTC) in the format specified in ISO 8601. * * @readonly */ get created(): string { return this.data.created; } /** * Raw assembly data received from the server. * * @readonly */ get data(): any { return this._data; } private set data(value: any) { this._data = value; this._data.owner.avatarUrl = `${this.httpClient.serverUrl}/users/${this._data.owner.userId}/avatar`; this._data.owner.fullName = userFullName(this._data.owner); this._data.owner.initials = userInitials(this._data.owner.fullName); // associatedFiles since 23.12 this._data.associatedFiles ??= []; this._data.associatedFiles.forEach((file) => (file.link = `${this.httpClient.serverUrl}/files/${file.fileId}`)); } /** * List of file IDs from which the assembly was created. * * @readonly */ get files(): string[] { return this.data.files; } /** * Assembly geometry data type: * * - `vsfx` - VSFX, assembly can be opened in `VisualizeJS` viewer. * * Returns an empty string if geometry data has not yet been extracted. */ get geometryType(): string { return this.status === "done" ? "vsfx" : ""; } /** * Unique assembly ID. * * @readonly */ get id(): string { return this.data.id; } /** * Assembly name. */ get name(): string { return this.data.name; } set name(value: string) { this.data.name = value; } // Reserved for future use get originalAssemblyId(): string { return this.data.originalAssemblyId; } /** * Assembly owner information. * * @property {string} userId - User ID. * @property {string} userName - User name. * @property {string} name - First name. * @property {string} lastName - Last name. * @property {string} fullName - Full name. * @property {string} initials - Initials. * @property {string} email - User email. * @property {string} avatarUrl - User avatar image URL. * @readonly */ get owner(): IShortUserDescription { return this.data.owner; } // Reserved for future use get previewUrl(): string { return this.data.previewUrl ?? ""; } /** * List of assembly related job IDs. * * @readonly */ get relatedJobs(): string[] { return this.data.relatedJobs; } /** * Assembly geometry data and properties status. Can be `waiting`, `inprogress`, `done` or `failed`. * * An assemblies without geometry data cannot be opened in viewer. * * @readonly */ get status(): string { return this.data.status; } /** * Assembly type. Returns an `assembly`. * * @readonly */ get type(): string { return "assembly"; } // Reserved for future use get version(): number { return this.data.version; } get versions(): IAssemblyVersionInfo[] { return this.data.versions; } /** * Refresh assembly data. * * @async */ async checkout(): Promise { this.data = await json(this.internalGet("")); return this; } /** * Updates assembly data on the server. * * @async * @param data - Raw assembly data. */ async update(data: any): Promise { this.data = await json(this.internalPut("", data)); return this; } /** * Delete the assembly from the server. * * @async * @returns Returns the raw data of a deleted assembly. */ delete(): Promise { return json(this.internalDelete("")); } /** * Save assembly data changes to the server. Call this method to update assembly data on the * server after any changes. * * @async */ save(): Promise { return this.update(this.data); } // Reserved for future use setPreview(image?: ArrayBuffer | Blob | globalThis.File | FormData | string | null) { console.warn("Assembly does not support preview"); return Promise.resolve(this); } /** * Returns list of assembly models. * * @async */ getModels(): Promise { return json(this.internalGet("/geometry")).then((array) => array.map((data) => new Model(data, this))); } /** * Transformation matrix. * * @typedef {any} Transform * @property {any} translate - Translation part, move along the X, Y, Z axes. * @property {number} translate.x - The value for substruct for X axes. * @property {number} translate.y - The value for substruct for Y axes. * @property {number} translate.z - The value for substruct for Z axes. * @property {any} rotation - Rotation part, rotate by the specified angle, around the * specified vector. * @property {number} rotation.x - X coordinate of the rotation vector. * @property {number} rotation.y - Y coordinate of the rotation vector. * @property {number} rotation.z - Z coordinate of the rotation vector. * @property {number} rotation.angle - The angle of rotation. * @property {number} scale - Scaling part, scale with multiplier, from center of extends. */ /** * Returns a model transformation. * * @param handle - Model handle. */ getModelTransformMatrix(handle: string): any { return this.data.transform[handle]; } /** * Set or delete a model transformation. * * @async * @param handle - Model handle. * @param transform - Transformation matrix. To delete transformation provide this to `undefined`. */ setModelTransformMatrix(handle: string, transform: any): Promise { const obj = { ...this.data.transform }; obj[handle] = transform; return this.update({ transform: obj }); } /** * Returns the properties for an objects in the assembly. * * @async * @param handles - Object original handle or handles array. Leave this parameter undefined * to get properties for all objects in the assembly. * @returns {Promise} */ getProperties(handles?: string | string[]): Promise { return json(this.internalGet(handles !== undefined ? `/properties?handles=${handles}` : "/properties")); } /** * Returns the list of original handles for an objects in the file that match the specified * patterns. Search patterns may be combined using query operators. * * @async * @example Simple search pattern. * searchPattern = { * key: "Category", * value: "OST_Stairs", * }; * * @example Search patterns combination. * searchPattern = { * $or: [ * { * $and: [ * { key: "Category", value: "OST_GenericModel" }, * { key: "Level", value: "03 - Floor" }, * ], * }, * { key: "Category", value: "OST_Stairs" }, * ], * }; * * @param searchPattern - Search pattern or combination of the patterns, see example below. */ searchProperties(searchPattern: any): Promise { return json(this.internalPost("/properties/search", searchPattern)); } /** * Returns the cda.json for an assembly. * * @async */ getCdaTree(): Promise { return json(this.internalGet(`/properties/tree`)); } // Reserved for future use getViewpoints(): Promise<[]> { console.warn("Assembly does not support viewpoints"); return Promise.resolve([]); } saveViewpoint(viewpoint: any): Promise { console.warn("Assembly does not support viewpoints"); return Promise.resolve({}); } deleteViewpoint(guid: any): Promise { console.warn("Assembly does not support viewpoints"); return Promise.resolve({}); } getSnapshot(guid: any): Promise { console.warn("Assembly does not support viewpoints"); return Promise.resolve({}); } getSnapshotData(guid: any, bitmapGuid: any): Promise { console.warn("Assembly does not support viewpoints"); return Promise.resolve(""); } /** * Download assembly resource data, such as geometry data. * * @async * @param dataId - Resource ID. * @param onProgress - Download progress callback. * @param signal - An AbortSignal object instance. Allows to communicate with a fetch request * and abort it if desired. */ downloadResource( dataId: string, onProgress?: (progress: number) => void, signal?: AbortSignal ): Promise { return this.internalGet(`/downloads/${dataId}`, signal) .then((response) => downloadProgress(response, onProgress)) .then((response) => response.arrayBuffer()); } // Reserved for future use getReferences(signal?: AbortSignal) { return Promise.resolve({ fileId: "", references: [] }); } /** * Wait for assembly to be created. Assembly is created when it changes to `done` or `failed` status. * * @async * @param params - An object containing waiting parameters. * @param params.timeout - The time, in milliseconds that the function should wait assembly. * If assembly is not created during this time, the `TimeoutError` exception will be thrown. * @param params.interval - The time, in milliseconds, the function should delay in between * checking assembly status. * @param params.signal- An AbortController * signal object instance, which can be used to abort waiting as desired. * @param params.onCheckout - Waiting progress callback. Return `true` to cancel waiting. * @returns {Promise} */ waitForDone(params?: { timeout?: number; interval?: number; signal?: AbortSignal; onCheckout?: (assembly: Assembly, ready: boolean) => boolean; }): Promise { const checkDone = () => this.checkout().then((assembly) => { const ready = ["done", "failed"].includes(assembly.status); const cancel = params?.onCheckout?.(assembly, ready); return cancel || ready; }); return waitFor(checkDone, params).then(() => this); } /** * Returns a list of assembly clash tests. * * @async * @param {number} start - The starting index in the test list. Used for paging. * @param {number} limit - The maximum number of tests that should be returned per request. * Used for paging. * @param {string} name - Filter the tests by part of the name. * @param {string | string[]} ids - List of tests IDs to return. You can specify multiple IDs * on one `string` by separating them with a "|". * @param {bool} sortByDesc - Allows to specify the descending order of the result. By * default tests are sorted by name in ascending order. * @param {string} sortField - Allows to specify sort field. */ getClashTests( start?: number, limit?: number, name?: string, ids?: string | string[], sortByDesc?: boolean, sortField?: string ): Promise<{ allSize: number; start: number; limit: number; result: ClashTest[]; size: number }> { const searchParams = new URLSearchParams(); if (start > 0) searchParams.set("start", start.toString()); if (limit > 0) searchParams.set("limit", limit.toString()); if (name) searchParams.set("name", name); if (ids) { if (Array.isArray(ids)) ids = ids.join("|"); if (typeof ids === "string") ids = ids.trim(); if (ids) searchParams.set("id", ids); } if (sortByDesc !== undefined) searchParams.set("sortBy", sortByDesc ? "desc" : "asc"); if (sortField) searchParams.set("sortField", sortField); let queryString = searchParams.toString(); if (queryString) queryString = "?" + queryString; return this.internalGet(`/clashes${queryString}`) .then((response) => response.json()) .then((tests) => { return { ...tests, result: tests.result.map((data) => new ClashTest(data, this.path, this.httpClient)), }; }); } /** * Returns the assembly clash test information. * * @async * @param testId - Test ID. */ getClashTest(testId: string): Promise { return this.internalGet(`/clashes/${testId}`) .then((response) => response.json()) .then((data) => new ClashTest(data, this.path, this.httpClient)); } /** * Create assembly clash test. Assembly must be in a `done` state, otherwise the test will fail. * * @async * @param name - Test name. * @param selectionTypeA - The type of first selection set for clash detection. Can be `all`, * `handles`, `models` or `searchquery`. * @param selectionTypeB - The type of second selection set for clash detection. Can be * `all`, `handles`, `models` or `searchquery`. * @param selectionSetA - First selection set for clash detection. * @param selectionSetB - Second selection set for clash detection. * @param params - An object containing test parameters. * @param params.tolerance - The distance of separation between entities at which test begins * detecting clashes. * @param params.clearance - The type of the clashes that the test detects: `true` for * `Сlearance clash` or `false` for `Hard clash`. * @param params.waitForDone - Wait for test to complete. * @param params.timeout - The time, in milliseconds that the function should wait test. If * test is not complete during this time, the `TimeoutError` exception will be thrown. * @param params.interval - The time, in milliseconds, the function should delay in between * checking test status. * @param params.signal- An AbortController * signal object instance, which can be used to abort waiting as desired. */ createClashTest( name: string, selectionTypeA: string, selectionTypeB: string, selectionSetA?: string | string[], selectionSetB?: string | string[], params?: { tolerance?: number | string; clearance?: boolean; waitForDone?: boolean; timeout?: number; interval?: number; signal?: AbortSignal; } ): Promise { const { tolerance, clearance, waitForDone } = params ?? {}; if (!Array.isArray(selectionSetA)) selectionSetA = [selectionSetA]; if (!Array.isArray(selectionSetB)) selectionSetB = [selectionSetB]; return this.internalPost("/clashes", { name, selectionTypeA, selectionTypeB, selectionSetA, selectionSetB, tolerance, clearance, }) .then((response) => response.json()) .then((data) => new ClashTest(data, this.path, this.httpClient)) .then((result) => (waitForDone ? result.waitForDone(params) : result)); } /** * Delete assembly clash test. * * @async * @param testId - Test ID. * @returns Returns the raw data of a deleted test. */ deleteClashTest(testId: string): Promise { return this.internalDelete(`/clashes/${testId}`).then((response) => response.json()); } // Reserved for future use updateVersion( files?: string[], params: { waitForDone?: boolean; timeout?: number; interval?: number; signal?: AbortSignal; onProgress?: (progress: number, file: globalThis.File) => void; } = { waitForDone: false, } ): Promise { return Promise.reject(new Error("Assembly version support will be implemeted in a future release")); } getVersions(): Promise { return Promise.resolve(undefined); } getVersion(version: number): Promise { return Promise.reject(new FetchError(404)); } deleteVersion(version: number): Promise { return Promise.reject(new FetchError(404)); } setActiveVersion(version: number) { return this.update({ activeVersion: version }); } useVersion(version?: number) { this._useVersion = undefined; return this; } }