UNPKG

@inweb/viewer-visualize

Version:

JavaScript library for rendering CAD and BIM files in a browser using VisualizeJS

226 lines (186 loc) 8.43 kB
/////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2002-2025, 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-2025 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 { Loader } from "@inweb/viewer-core"; import { Viewer } from "../Viewer"; import { UpdateController, UpdateType } from "./UpdateController"; const PENDING_REQUESTS_SIZE = 50; const PENDING_REQUESTS_TIMEOUT = 250; export class VSFXCloudPartialLoader extends Loader { public viewer: Viewer; public abortControllerForRequestMap: Map<number, AbortController>; constructor(viewer: Viewer) { super(); this.viewer = viewer; this.abortControllerForRequestMap = new Map(); } override isSupport(file: any): boolean { return ( typeof file === "object" && typeof file.database === "string" && typeof file.downloadResource === "function" && typeof file.downloadResourceRange === "function" && /.vsfx$/i.test(file.database) && (this.viewer.options.enablePartialMode === true || /.rcs$/i.test(file.name)) ); } override async load(model: any, format?: string): Promise<this> { if (!this.viewer.visualizeJs) return this; const visLib = this.viewer.visLib(); const visViewer = visLib.getViewer(); let servicePartAborted = false; const pendingRequestsMap = new Map(); let pendingRequestsTimerId = 0; const pendingRequestsAbortHandler = () => clearTimeout(pendingRequestsTimerId); const pendingRequestsAbortController = new AbortController(); this.abortControllerForRequestMap.set(0, pendingRequestsAbortController); const updateController = new UpdateController(); updateController.initialize(this.viewer); visViewer.memoryLimit = this.viewer.options.memoryLimit; const chunkLoadHandler = (progress: number, chunk: Uint8Array, requestId = 0) => { if (!this.viewer.visualizeJs) return; let state: boolean; try { state = visViewer.parseVsfxInPartialMode(requestId, chunk); } catch (error: any) { console.error("VSFX parse error.", error); throw error; } updateController.update(UpdateType.kDelay); this.viewer.emitEvent({ type: "geometryprogress", data: progress, file: model.file, model }); if (state) { updateController.update(UpdateType.kForce); this.viewer.syncOpenCloudVisualStyle(false); this.viewer.syncOptions(); this.viewer.syncOverlay(); this.viewer.resize(); this.viewer.emitEvent({ type: "databasechunk", data: chunk, file: model.file, model }); } else { this.viewer.emitEvent({ type: "geometrychunk", data: chunk, file: model.file, model }); } }; const downloadResourceRange = async (dataId: string, requestId: number, ranges: any) => { const abortCtrl = new AbortController(); this.abortControllerForRequestMap.set(requestId, abortCtrl); try { await model.downloadResourceRange(dataId, requestId, ranges, chunkLoadHandler, abortCtrl.signal); } catch (error: any) { this.viewer.emitEvent({ type: "geometryerror", data: error, file: model.file, model }); } finally { ranges.forEach((range) => visViewer.onRequestResponseComplete(range.requestId)); this.abortControllerForRequestMap.delete(requestId); updateController.update(UpdateType.kNormal); } }; const requestRecordsToRanges = (requestId: number, records: any): any => { const ranges = []; for (let i = 0; i < records.size(); i++) { const record = records.get(i); ranges.push({ requestId, begin: Number(record.begin), end: Number(record.end) - 1, // <- Visualize fix }); record.delete(); } return ranges; }; const objectHandler = { onServicePartReceived: (bHasIndex: boolean) => { if (bHasIndex) { servicePartAborted = true; this.abortController.abort(); } }, onRequest: (requestId: number, records: any) => { const ranges = requestRecordsToRanges(requestId, records); downloadResourceRange(model.database, requestId, ranges); }, onFullLoaded: () => { updateController.update(UpdateType.kNormal); }, onRequestResponseParsed: (requestId: number) => { this.abortControllerForRequestMap.delete(requestId); updateController.update(UpdateType.kNormal); }, onRequestAborted: (requestId: number) => { const abortCtrl = this.abortControllerForRequestMap.get(requestId); if (abortCtrl) abortCtrl.abort(); }, onRequestResourceFile: (requestId: number, _: string, records: any) => { const dataId = `${model.fileId}${model.file.type}`; const ranges = requestRecordsToRanges(requestId, records); let pendingRanges = []; let requestNumber = 0; const pendingRequest = pendingRequestsMap.get(dataId); if (pendingRequest) { pendingRanges = pendingRequest.ranges; requestNumber = pendingRequest.number; } // first several records of each file are processed without grouping (they usually require to be processed sequentially) if (requestNumber <= 5) { pendingRequestsMap.set(dataId, { ranges: [], number: requestNumber + 1 }); downloadResourceRange(dataId, requestId, ranges); return; } pendingRanges = pendingRanges.concat(ranges); // group requests to each file to launch a combined server request if (pendingRanges.length >= PENDING_REQUESTS_SIZE) { if (pendingRequestsTimerId) { window.clearTimeout(pendingRequestsTimerId); pendingRequestsTimerId = 0; } pendingRequestsMap.set(dataId, { ranges: [], number: requestNumber + 1 }); downloadResourceRange(dataId, requestId, pendingRanges); return; } pendingRequestsMap.set(dataId, { ranges: pendingRanges, number: requestNumber + 1 }); // set timeout to wait for the new requests, after that process the remaining requests if (pendingRequestsTimerId === 0) { pendingRequestsTimerId = window.setTimeout(() => { pendingRequestsAbortController.signal.removeEventListener("abort", pendingRequestsAbortHandler); pendingRequestsTimerId = 0; pendingRequestsMap.forEach((request, dataId) => { if (request.ranges.length > 0) { pendingRequestsMap.set(dataId, { ranges: [], number: request.number + 1 }); downloadResourceRange(dataId, requestId, request.ranges); } }); }, PENDING_REQUESTS_TIMEOUT); pendingRequestsAbortController.signal.addEventListener("abort", pendingRequestsAbortHandler, { once: true }); } }, }; visViewer.attachPartialResolver(objectHandler); try { await model.downloadResource(model.database, chunkLoadHandler, this.abortController.signal); } catch (error) { window.clearTimeout(pendingRequestsTimerId); if (!servicePartAborted) throw error; } return this; } override cancel(): void { super.cancel(); this.abortControllerForRequestMap.forEach((controller) => controller.abort()); } }