3dviewer-sdk 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -165,7 +165,7 @@ var NodeModule = class {
165
165
  constructor(viewer) {
166
166
  this.viewer = viewer;
167
167
  this.on = {
168
- select: (cb) => this.viewer._on("node:select", cb),
168
+ // SDK no longer supports first-node-only select events; listen to selectionChange for the full node list.
169
169
  selectionChange: (cb) => this.viewer._on("node:selection-change", cb)
170
170
  };
171
171
  }
@@ -212,52 +212,75 @@ var FilesModule = class {
212
212
  // Upload file to conversion server and keep generated baseFileId in session.
213
213
  async upload(file) {
214
214
  const target = this.resolveFile(file);
215
- return this.withOperation({ stage: "uploading", message: "Uploading file..." }, async () => {
216
- var _a;
217
- this.viewer._emit("files:upload:start", { fileName: target.name });
218
- await this.uploadInternal(target);
219
- const baseFileId = ((_a = this.getUploadSessionForFile(target)) == null ? void 0 : _a.baseFileId) || "";
220
- this.viewer._emit("files:upload:success", { fileName: target.name, baseFileId });
221
- return { fileName: target.name, baseFileId };
222
- });
215
+ return this.withOperation(
216
+ { stage: "uploading", message: "Uploading file..." },
217
+ async () => {
218
+ var _a;
219
+ this.viewer._emit("files:upload:start", { fileName: target.name });
220
+ await this.uploadInternal(target);
221
+ const baseFileId = ((_a = this.getUploadSessionForFile(target)) == null ? void 0 : _a.baseFileId) || "";
222
+ this.viewer._emit("files:upload:success", {
223
+ fileName: target.name,
224
+ baseFileId
225
+ });
226
+ return { fileName: target.name, baseFileId };
227
+ }
228
+ );
223
229
  }
224
230
  // Trigger conversion flow and resolve final viewer metadata.
225
231
  async convert(file, options = {}) {
226
232
  const target = this.resolveFile(file);
227
- return this.withOperation({ stage: "converting", message: "Converting file..." }, async () => {
228
- this.viewer._emit("files:conversion:start", { fileName: target.name });
229
- try {
230
- const prepared = await this.convertInternal(target, options);
231
- this.viewer._emit("files:conversion:success", prepared);
232
- return prepared;
233
- } catch (e) {
234
- this.viewer._emit("files:conversion:error", { fileName: target.name, error: this.toErrorMessage(e) });
235
- throw e;
233
+ return this.withOperation(
234
+ { stage: "converting", message: "Converting file..." },
235
+ async () => {
236
+ this.viewer._emit("files:conversion:start", { fileName: target.name });
237
+ try {
238
+ const prepared = await this.convertInternal(target, options);
239
+ this.viewer._emit("files:conversion:success", prepared);
240
+ return prepared;
241
+ } catch (e) {
242
+ this.viewer._emit("files:conversion:error", {
243
+ fileName: target.name,
244
+ error: this.toErrorMessage(e)
245
+ });
246
+ throw e;
247
+ }
236
248
  }
237
- });
249
+ );
238
250
  }
239
251
  // Convenience API: upload first, then convert in one call.
240
252
  async prepare(file, options = {}) {
241
253
  const target = this.resolveFile(file);
242
- return this.withOperation({ stage: "uploading", message: "Preparing file..." }, async () => {
243
- await this.uploadInternal(target);
244
- const prepared = await this.convertInternal(target, options);
245
- return prepared;
246
- });
254
+ return this.withOperation(
255
+ { stage: "uploading", message: "Preparing file..." },
256
+ async () => {
257
+ await this.uploadInternal(target);
258
+ const prepared = await this.convertInternal(target, options);
259
+ return prepared;
260
+ }
261
+ );
247
262
  }
248
263
  // Trigger the newer downloadUrl-based conversion flow.
249
264
  async convertV2(options) {
250
- return this.withOperation({ stage: "converting", message: "Converting file..." }, async () => {
251
- this.viewer._emit("files:conversion:start", { fileName: options.filename });
252
- try {
253
- const prepared = await this.convertV2Internal(options);
254
- this.viewer._emit("files:conversion:success", prepared);
255
- return prepared;
256
- } catch (e) {
257
- this.viewer._emit("files:conversion:error", { fileName: options.filename, error: this.toErrorMessage(e) });
258
- throw e;
265
+ return this.withOperation(
266
+ { stage: "converting", message: "Converting file..." },
267
+ async () => {
268
+ this.viewer._emit("files:conversion:start", {
269
+ fileName: options.filename
270
+ });
271
+ try {
272
+ const prepared = await this.convertV2Internal(options);
273
+ this.viewer._emit("files:conversion:success", prepared);
274
+ return prepared;
275
+ } catch (e) {
276
+ this.viewer._emit("files:conversion:error", {
277
+ fileName: options.filename,
278
+ error: this.toErrorMessage(e)
279
+ });
280
+ throw e;
281
+ }
259
282
  }
260
- });
283
+ );
261
284
  }
262
285
  // Check stream file info by one or more baseFileId values.
263
286
  async checkFileInfo(baseFileIds) {
@@ -273,7 +296,9 @@ var FilesModule = class {
273
296
  body: JSON.stringify(payload)
274
297
  });
275
298
  if (!response.ok) {
276
- throw new Error(`Check file info failed (${response.status} ${response.statusText})`);
299
+ throw new Error(
300
+ `Check file info failed (${response.status} ${response.statusText})`
301
+ );
277
302
  }
278
303
  const text = await response.text();
279
304
  if (!text) return null;
@@ -291,27 +316,34 @@ var FilesModule = class {
291
316
  this.viewer.open(url);
292
317
  this.viewer._emit("files:render:success", { url });
293
318
  } catch (e) {
294
- this.viewer._emit("files:render:error", { url, error: this.toErrorMessage(e) });
319
+ this.viewer._emit("files:render:error", {
320
+ url,
321
+ error: this.toErrorMessage(e)
322
+ });
295
323
  throw e;
296
324
  }
297
325
  }
298
326
  // Full pipeline: upload + convert + open iframe.
299
327
  async render(file, options = {}) {
300
328
  const target = this.resolveFile(file);
301
- return this.withOperation({ stage: "uploading", message: "Uploading + converting + opening..." }, async () => {
302
- await this.upload(target);
303
- const prepared = await this.convert(target, options);
304
- this.updateState({ stage: "rendering", message: "Opening viewer..." });
305
- this.open(prepared);
306
- this.viewer._emit("files:load:success", prepared);
307
- return prepared;
308
- });
329
+ return this.withOperation(
330
+ { stage: "uploading", message: "Uploading + converting + opening..." },
331
+ async () => {
332
+ await this.upload(target);
333
+ const prepared = await this.convert(target, options);
334
+ this.updateState({ stage: "rendering", message: "Opening viewer..." });
335
+ this.open(prepared);
336
+ this.viewer._emit("files:load:success", prepared);
337
+ return prepared;
338
+ }
339
+ );
309
340
  }
310
341
  // Resolve file argument, fallback to options.file, and persist it back.
311
342
  resolveFile(file) {
312
343
  const optFile = this.viewer.getOptions().file;
313
344
  const target = file || optFile;
314
- if (!target) throw new Error("No file provided. Pass a File or set options.file");
345
+ if (!target)
346
+ throw new Error("No file provided. Pass a File or set options.file");
315
347
  this.viewer.patchOptions({ file: target });
316
348
  return target;
317
349
  }
@@ -332,7 +364,8 @@ var FilesModule = class {
332
364
  if (viewerUrl) {
333
365
  try {
334
366
  const pathname = new URL(viewerUrl, window.location.href).pathname;
335
- if (pathname && pathname !== "/") return this.normalizeViewerPath(pathname);
367
+ if (pathname && pathname !== "/")
368
+ return this.normalizeViewerPath(pathname);
336
369
  } catch {
337
370
  }
338
371
  }
@@ -349,14 +382,18 @@ var FilesModule = class {
349
382
  const configuredViewerBaseUrl = this.config.baseUrl || this.viewer.getOptions().baseUrl;
350
383
  if (configuredViewerBaseUrl) {
351
384
  try {
352
- return this.normalizeBaseUrl(new URL(configuredViewerBaseUrl, window.location.href).origin);
385
+ return this.normalizeBaseUrl(
386
+ new URL(configuredViewerBaseUrl, window.location.href).origin
387
+ );
353
388
  } catch {
354
389
  }
355
390
  }
356
391
  const viewerUrl = this.viewer.getOptions().url;
357
392
  if (viewerUrl) {
358
393
  try {
359
- return this.normalizeBaseUrl(new URL(viewerUrl, window.location.href).origin);
394
+ return this.normalizeBaseUrl(
395
+ new URL(viewerUrl, window.location.href).origin
396
+ );
360
397
  } catch {
361
398
  }
362
399
  }
@@ -377,7 +414,8 @@ var FilesModule = class {
377
414
  }
378
415
  // Create a UUID-like baseFileId when caller does not provide one.
379
416
  createBaseFileId() {
380
- if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
417
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto)
418
+ return crypto.randomUUID();
381
419
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
382
420
  const r = Math.floor(Math.random() * 16);
383
421
  const v = c === "x" ? r : r & 3 | 8;
@@ -408,15 +446,25 @@ var FilesModule = class {
408
446
  const session = existing || this.createUploadSession(file);
409
447
  const hostConversion = this.resolveHostConversion();
410
448
  const path = this.getUploadPath();
411
- const url = `${hostConversion}/api/File/upload?path=${encodeURIComponent(path)}`;
449
+ const url = `${hostConversion}/api/File/upload?path=${encodeURIComponent(
450
+ path
451
+ )}`;
412
452
  const formData = new FormData();
413
453
  formData.append("file", file, file.name);
414
- const res = await fetch(url, { method: "POST", body: formData, headers: { Accept: "text/plain" } });
415
- if (!res.ok) throw new Error(`Upload failed (${res.status} ${res.statusText})`);
454
+ const res = await fetch(url, {
455
+ method: "POST",
456
+ body: formData,
457
+ headers: { Accept: "text/plain" }
458
+ });
459
+ if (!res.ok)
460
+ throw new Error(`Upload failed (${res.status} ${res.statusText})`);
416
461
  this.lastUploadSession = session;
417
462
  } catch (e) {
418
463
  const msg = this.toErrorMessage(e);
419
- this.viewer._emit("files:upload:error", { fileName: file.name, error: msg });
464
+ this.viewer._emit("files:upload:error", {
465
+ fileName: file.name,
466
+ error: msg
467
+ });
420
468
  throw e;
421
469
  }
422
470
  }
@@ -570,11 +618,20 @@ var FilesModule = class {
570
618
  const baseMajorRev = (_b = cacheResult.baseMajorRev) != null ? _b : 0;
571
619
  const baseMinorRev = (_c = cacheResult.baseMinorRev) != null ? _c : 0;
572
620
  const fileName = cacheResult.filename || cacheResult.fileName || file.name;
573
- const cacheListItem = { baseFileId, baseMajorRev, baseMinorRev, fileName };
621
+ const cacheListItem = {
622
+ baseFileId,
623
+ baseMajorRev,
624
+ baseMinorRev,
625
+ fileName
626
+ };
574
627
  if (cacheResult.cacheStatus !== 2) {
575
- throw new Error(`Conversion not ready after first request (cacheStatus=${(_d = cacheResult.cacheStatus) != null ? _d : "unknown"})`);
628
+ throw new Error(
629
+ `Conversion not ready after first request (cacheStatus=${(_d = cacheResult.cacheStatus) != null ? _d : "unknown"})`
630
+ );
576
631
  }
577
- const query = new URLSearchParams({ fileList: JSON.stringify([cacheListItem]) }).toString();
632
+ const query = new URLSearchParams({
633
+ fileList: JSON.stringify([cacheListItem])
634
+ }).toString();
578
635
  const viewerBase = this.resolveViewerOrigin();
579
636
  const viewerPath = this.resolveViewerPath();
580
637
  const url = `${viewerBase}${viewerPath}?${query}`;
@@ -589,10 +646,19 @@ var FilesModule = class {
589
646
  const baseMinorRev = (_e = (_d = cacheResult.baseMinorRev) != null ? _d : options.baseMinorRev) != null ? _e : 0;
590
647
  const fileName = cacheResult.filename || cacheResult.fileName || options.filename;
591
648
  if (cacheResult.cacheStatus !== void 0 && cacheResult.cacheStatus !== 2) {
592
- throw new Error(`Conversion not ready after v2 request (cacheStatus=${cacheResult.cacheStatus})`);
649
+ throw new Error(
650
+ `Conversion not ready after v2 request (cacheStatus=${cacheResult.cacheStatus})`
651
+ );
593
652
  }
594
- const cacheListItem = { baseFileId, baseMajorRev, baseMinorRev, fileName };
595
- const query = new URLSearchParams({ fileList: JSON.stringify([cacheListItem]) }).toString();
653
+ const cacheListItem = {
654
+ baseFileId,
655
+ baseMajorRev,
656
+ baseMinorRev,
657
+ fileName
658
+ };
659
+ const query = new URLSearchParams({
660
+ fileList: JSON.stringify([cacheListItem])
661
+ }).toString();
596
662
  const viewerBase = this.resolveViewerOrigin();
597
663
  const viewerPath = this.resolveViewerPath();
598
664
  const url = `${viewerBase}${viewerPath}?${query}`;
@@ -603,6 +669,15 @@ var FilesModule = class {
603
669
  const elapsedMs = this.operationStartTime > 0 ? Date.now() - this.operationStartTime : 0;
604
670
  this.state = { ...this.state, ...next, elapsedMs };
605
671
  this.viewer._emit("files:state", this.state);
672
+ this.viewer._emit("loading:change", {
673
+ loading: this.state.isLoading,
674
+ phase: this.toViewerLoadingPhase(this.state.stage)
675
+ });
676
+ }
677
+ toViewerLoadingPhase(stage) {
678
+ if (stage === "error") return "error";
679
+ if (stage === "idle" || stage === "completed") return "ready";
680
+ return "loading";
606
681
  }
607
682
  // Shared wrapper to handle loading state lifecycle and top-level errors.
608
683
  async withOperation(initial, run) {
@@ -614,7 +689,11 @@ var FilesModule = class {
614
689
  });
615
690
  try {
616
691
  const result = await run();
617
- this.updateState({ isLoading: false, stage: "completed", message: "Completed" });
692
+ this.updateState({
693
+ isLoading: false,
694
+ stage: "completed",
695
+ message: "Completed"
696
+ });
618
697
  return result;
619
698
  } catch (e) {
620
699
  const msg = this.toErrorMessage(e);
@@ -1267,12 +1346,50 @@ var Viewer3D = class {
1267
1346
  this.containerEl = null;
1268
1347
  this.iframeEl = null;
1269
1348
  this.initialized = false;
1349
+ this.viewerReady = false;
1350
+ this.readyPayload = null;
1351
+ this.readyWaiters = [];
1352
+ this.loadingState = {
1353
+ loading: false,
1354
+ phase: "ready"
1355
+ };
1270
1356
  this.emitter = new Emitter();
1271
1357
  this.handleMessage = (event) => {
1272
- var _a, _b, _c, _d, _e;
1358
+ var _a, _b, _c;
1273
1359
  const data = event.data;
1274
1360
  if (!data || typeof data !== "object") return;
1275
1361
  switch (data.type) {
1362
+ case "viewer-loading-change" /* LOADING_CHANGE */: {
1363
+ const payload = data.payload;
1364
+ if (!payload) break;
1365
+ this.setLoadingState({
1366
+ loading: Boolean(payload.loading),
1367
+ phase: payload.phase,
1368
+ viewId: payload.viewId ? String(payload.viewId) : void 0,
1369
+ modelFileId: payload.modelFileId ? String(payload.modelFileId) : void 0,
1370
+ timestamp: payload.timestamp ? Number(payload.timestamp) : void 0
1371
+ });
1372
+ break;
1373
+ }
1374
+ case "viewer-ready" /* READY */: {
1375
+ const payload = data.payload;
1376
+ const readyPayload = {
1377
+ ready: true,
1378
+ viewId: (payload == null ? void 0 : payload.viewId) ? String(payload.viewId) : void 0,
1379
+ modelFileId: (payload == null ? void 0 : payload.modelFileId) ? String(payload.modelFileId) : void 0,
1380
+ timestamp: Number(payload == null ? void 0 : payload.timestamp) || Date.now()
1381
+ };
1382
+ this.setLoadingState({
1383
+ loading: false,
1384
+ phase: "ready",
1385
+ viewId: readyPayload.viewId,
1386
+ modelFileId: readyPayload.modelFileId,
1387
+ timestamp: readyPayload.timestamp
1388
+ });
1389
+ this.resolveReady(readyPayload);
1390
+ this._emit("viewer:ready", readyPayload);
1391
+ break;
1392
+ }
1276
1393
  case "viewer-home-click" /* HOME_CLICK */:
1277
1394
  this._emit("camera:home", { timestamp: Date.now() });
1278
1395
  break;
@@ -1287,9 +1404,6 @@ var Viewer3D = class {
1287
1404
  });
1288
1405
  break;
1289
1406
  }
1290
- case "viewer-node-select" /* NODE_SELECT */:
1291
- this._emit("node:select", { nodeId: String((_b = (_a = data.payload) == null ? void 0 : _a.nodeId) != null ? _b : ""), timestamp: Date.now() });
1292
- break;
1293
1407
  case "viewer-node-selection-change" /* NODE_SELECTION_CHANGE */: {
1294
1408
  const payload = data.payload;
1295
1409
  const rawNodeIds = payload == null ? void 0 : payload.nodeIds;
@@ -1301,7 +1415,9 @@ var Viewer3D = class {
1301
1415
  break;
1302
1416
  }
1303
1417
  case "viewer-pan-change" /* PAN_CHANGE */:
1304
- this._emit("interaction:pan-change", { enabled: Boolean((_c = data.payload) == null ? void 0 : _c.enabled) });
1418
+ this._emit("interaction:pan-change", {
1419
+ enabled: Boolean((_a = data.payload) == null ? void 0 : _a.enabled)
1420
+ });
1305
1421
  break;
1306
1422
  case "viewer-pdf-plan-mode" /* PDF_PLAN_MODE */: {
1307
1423
  const payload = data.payload;
@@ -1359,7 +1475,8 @@ var Viewer3D = class {
1359
1475
  }
1360
1476
  case "viewer-tree-node-ids" /* TREE_NODE_IDS */: {
1361
1477
  const payload = data.payload;
1362
- if (!payload || !payload.requestId || !Array.isArray(payload.nodeIds)) break;
1478
+ if (!payload || !payload.requestId || !Array.isArray(payload.nodeIds))
1479
+ break;
1363
1480
  this._emit("modelTree:node-ids", {
1364
1481
  requestId: String(payload.requestId),
1365
1482
  nodeIds: payload.nodeIds.map(String),
@@ -1369,7 +1486,8 @@ var Viewer3D = class {
1369
1486
  }
1370
1487
  case "viewer-tree-nodes" /* TREE_NODES */: {
1371
1488
  const payload = data.payload;
1372
- if (!payload || !payload.requestId || !Array.isArray(payload.nodes)) break;
1489
+ if (!payload || !payload.requestId || !Array.isArray(payload.nodes))
1490
+ break;
1373
1491
  this._emit("modelTree:nodes", {
1374
1492
  requestId: String(payload.requestId),
1375
1493
  nodes: payload.nodes.filter((node) => node && typeof node === "object").map((node) => {
@@ -1395,7 +1513,8 @@ var Viewer3D = class {
1395
1513
  }
1396
1514
  case "viewer-sheets-list" /* SHEETS_LIST */: {
1397
1515
  const payload = data.payload;
1398
- if (!payload || !payload.requestId || !Array.isArray(payload.sheets)) break;
1516
+ if (!payload || !payload.requestId || !Array.isArray(payload.sheets))
1517
+ break;
1399
1518
  this._emit("sheets:list", {
1400
1519
  requestId: String(payload.requestId),
1401
1520
  sheets: payload.sheets.map((sheet) => {
@@ -1407,17 +1526,18 @@ var Viewer3D = class {
1407
1526
  viewId: sheet.viewId ? String(sheet.viewId) : void 0
1408
1527
  };
1409
1528
  }),
1410
- activeSheetId: (_d = payload.activeSheetId) != null ? _d : null,
1529
+ activeSheetId: (_b = payload.activeSheetId) != null ? _b : null,
1411
1530
  timestamp: Number(payload.timestamp) || Date.now()
1412
1531
  });
1413
1532
  break;
1414
1533
  }
1415
1534
  case "viewer-object-properties-list" /* OBJECT_PROPERTIES_LIST */: {
1416
1535
  const payload = data.payload;
1417
- if (!payload || !payload.requestId || !Array.isArray(payload.properties)) break;
1536
+ if (!payload || !payload.requestId || !Array.isArray(payload.properties))
1537
+ break;
1418
1538
  this._emit("object-properties:list", {
1419
1539
  requestId: String(payload.requestId),
1420
- selectionKey: String((_e = payload.selectionKey) != null ? _e : ""),
1540
+ selectionKey: String((_c = payload.selectionKey) != null ? _c : ""),
1421
1541
  nodeIds: Array.isArray(payload.nodeIds) ? payload.nodeIds.map(String) : [],
1422
1542
  persistentIds: Array.isArray(payload.persistentIds) ? payload.persistentIds.map(String) : [],
1423
1543
  properties: payload.properties.filter((item) => item && typeof item === "object").map((item) => ({ ...item })),
@@ -1427,7 +1547,8 @@ var Viewer3D = class {
1427
1547
  }
1428
1548
  case "viewer-linked-objects-list" /* LINKED_OBJECTS_LIST */: {
1429
1549
  const payload = data.payload;
1430
- if (!payload || !payload.requestId || !Array.isArray(payload.linkedObjects)) break;
1550
+ if (!payload || !payload.requestId || !Array.isArray(payload.linkedObjects))
1551
+ break;
1431
1552
  this._emit("linked-objects:list", {
1432
1553
  requestId: String(payload.requestId),
1433
1554
  linkedObjects: payload.linkedObjects.filter((item) => item && typeof item === "object").map((item) => ({ ...item })),
@@ -1437,11 +1558,12 @@ var Viewer3D = class {
1437
1558
  }
1438
1559
  case "viewer-states-objects-list" /* STATES_OBJECTS_LIST */: {
1439
1560
  const payload = data.payload;
1440
- if (!payload || !payload.requestId || !Array.isArray(payload.statesObjects)) break;
1561
+ if (!payload || !payload.requestId || !Array.isArray(payload.statesObjects))
1562
+ break;
1441
1563
  this._emit("states-objects:list", {
1442
1564
  requestId: String(payload.requestId),
1443
1565
  statesObjects: payload.statesObjects.map((item) => {
1444
- var _a2, _b2, _c2, _d2, _e2, _f, _g;
1566
+ var _a2, _b2, _c2, _d, _e, _f, _g;
1445
1567
  return {
1446
1568
  id: String((_a2 = item.id) != null ? _a2 : ""),
1447
1569
  name: String((_b2 = item.name) != null ? _b2 : ""),
@@ -1454,7 +1576,7 @@ var Viewer3D = class {
1454
1576
  };
1455
1577
  }) : [],
1456
1578
  states: {
1457
- color: String((_e2 = (_d2 = item.states) == null ? void 0 : _d2.color) != null ? _e2 : ""),
1579
+ color: String((_e = (_d = item.states) == null ? void 0 : _d.color) != null ? _e : ""),
1458
1580
  type: String((_g = (_f = item.states) == null ? void 0 : _f.type) != null ? _g : "")
1459
1581
  }
1460
1582
  };
@@ -1465,7 +1587,8 @@ var Viewer3D = class {
1465
1587
  }
1466
1588
  case "viewer-markup-list" /* MARKUP_LIST */: {
1467
1589
  const payload = data.payload;
1468
- if (!payload || !payload.requestId || !Array.isArray(payload.markups)) break;
1590
+ if (!payload || !payload.requestId || !Array.isArray(payload.markups))
1591
+ break;
1469
1592
  this._emit("markup:list", {
1470
1593
  requestId: String(payload.requestId),
1471
1594
  markups: payload.markups.map((markup) => {
@@ -1534,6 +1657,36 @@ var Viewer3D = class {
1534
1657
  var _a;
1535
1658
  return (_a = this.options.url) != null ? _a : null;
1536
1659
  }
1660
+ on(event, cb) {
1661
+ return this._on(event, cb);
1662
+ }
1663
+ off(event, cb) {
1664
+ this._off(event, cb);
1665
+ }
1666
+ getLoadingState() {
1667
+ return { ...this.loadingState };
1668
+ }
1669
+ ready(options) {
1670
+ if (this.viewerReady && this.readyPayload) {
1671
+ return Promise.resolve({ ...this.readyPayload });
1672
+ }
1673
+ const timeoutMs = options == null ? void 0 : options.timeoutMs;
1674
+ return new Promise((resolve, reject) => {
1675
+ const waiter = {
1676
+ resolve: (payload) => resolve({ ...payload }),
1677
+ reject
1678
+ };
1679
+ if (typeof timeoutMs === "number" && timeoutMs > 0) {
1680
+ waiter.timer = setTimeout(() => {
1681
+ this.readyWaiters = this.readyWaiters.filter(
1682
+ (item) => item !== waiter
1683
+ );
1684
+ reject(new Error("Timeout while waiting for viewer to be ready"));
1685
+ }, timeoutMs);
1686
+ }
1687
+ this.readyWaiters.push(waiter);
1688
+ });
1689
+ }
1537
1690
  // ===== lifecycle =====
1538
1691
  init() {
1539
1692
  if (this.initialized) return;
@@ -1545,7 +1698,12 @@ var Viewer3D = class {
1545
1698
  async render(file) {
1546
1699
  this.ensureInit();
1547
1700
  if (this.iframeEl) return;
1701
+ this.resetReadyState();
1548
1702
  if (!this.options.url) return this.files.render(file);
1703
+ this.setLoadingState({
1704
+ loading: true,
1705
+ phase: "loading"
1706
+ });
1549
1707
  const iframe = document.createElement("iframe");
1550
1708
  iframe.src = this.withInitialOptions(this.options.url);
1551
1709
  iframe.style.border = "none";
@@ -1553,13 +1711,19 @@ var Viewer3D = class {
1553
1711
  iframe.style.height = this.options.height || "100%";
1554
1712
  iframe.width = this.options.width || "100%";
1555
1713
  iframe.height = this.options.height || "100%";
1556
- if (this.options.sandbox) iframe.setAttribute("sandbox", this.options.sandbox);
1714
+ if (this.options.sandbox)
1715
+ iframe.setAttribute("sandbox", this.options.sandbox);
1557
1716
  this.containerEl.appendChild(iframe);
1558
1717
  this.iframeEl = iframe;
1559
1718
  }
1560
1719
  open(url) {
1561
1720
  this.ensureInit();
1562
1721
  this.options.url = url;
1722
+ this.resetReadyState();
1723
+ this.setLoadingState({
1724
+ loading: true,
1725
+ phase: "loading"
1726
+ });
1563
1727
  const finalUrl = this.withInitialOptions(url);
1564
1728
  if (!this.iframeEl) {
1565
1729
  this.render();
@@ -1578,16 +1742,27 @@ var Viewer3D = class {
1578
1742
  this.iframeEl = null;
1579
1743
  this.containerEl = null;
1580
1744
  this.initialized = false;
1745
+ this.rejectReadyWaiters(
1746
+ new Error("Viewer was destroyed before becoming ready")
1747
+ );
1581
1748
  }
1582
1749
  ensureInit() {
1583
- if (!this.initialized) throw new Error("Call viewer.init() before using viewer");
1750
+ if (!this.initialized)
1751
+ throw new Error("Call viewer.init() before using viewer");
1584
1752
  }
1585
1753
  withInitialOptions(url) {
1754
+ var _a;
1586
1755
  try {
1587
1756
  const parsedUrl = new URL(url, window.location.href);
1588
1757
  const initialToolbar = this.normalizeInitialToolbar();
1589
1758
  if (initialToolbar) {
1590
- parsedUrl.searchParams.set("useToolbar", JSON.stringify(initialToolbar));
1759
+ parsedUrl.searchParams.set(
1760
+ "useToolbar",
1761
+ JSON.stringify(initialToolbar)
1762
+ );
1763
+ }
1764
+ if (((_a = this.options.ui) == null ? void 0 : _a.loadingIndicator) === false) {
1765
+ parsedUrl.searchParams.set("loadingIndicator", "false");
1591
1766
  }
1592
1767
  return parsedUrl.toString();
1593
1768
  } catch {
@@ -1601,17 +1776,25 @@ var Viewer3D = class {
1601
1776
  return { [initialToolbar]: true };
1602
1777
  }
1603
1778
  if (Array.isArray(initialToolbar)) {
1604
- return initialToolbar.reduce((result, target) => {
1605
- result[target] = true;
1606
- return result;
1607
- }, {});
1779
+ return initialToolbar.reduce(
1780
+ (result, target) => {
1781
+ result[target] = true;
1782
+ return result;
1783
+ },
1784
+ {}
1785
+ );
1608
1786
  }
1609
- const entries = Object.entries(initialToolbar).filter(([, enabled]) => enabled === true);
1787
+ const entries = Object.entries(initialToolbar).filter(
1788
+ ([, enabled]) => enabled === true
1789
+ );
1610
1790
  if (entries.length === 0) return null;
1611
- return entries.reduce((result, [target]) => {
1612
- result[target] = true;
1613
- return result;
1614
- }, {});
1791
+ return entries.reduce(
1792
+ (result, [target]) => {
1793
+ result[target] = true;
1794
+ return result;
1795
+ },
1796
+ {}
1797
+ );
1615
1798
  }
1616
1799
  // ===== typed internal events used by modules =====
1617
1800
  _on(event, cb) {
@@ -1623,6 +1806,30 @@ var Viewer3D = class {
1623
1806
  _emit(event, payload) {
1624
1807
  this.emitter.emit(event, payload);
1625
1808
  }
1809
+ resetReadyState() {
1810
+ this.viewerReady = false;
1811
+ this.readyPayload = null;
1812
+ }
1813
+ setLoadingState(payload) {
1814
+ this.loadingState = { ...payload };
1815
+ this._emit("loading:change", this.loadingState);
1816
+ }
1817
+ resolveReady(payload) {
1818
+ this.viewerReady = true;
1819
+ this.readyPayload = { ...payload };
1820
+ this.readyWaiters.forEach((waiter) => {
1821
+ if (waiter.timer) clearTimeout(waiter.timer);
1822
+ waiter.resolve(payload);
1823
+ });
1824
+ this.readyWaiters = [];
1825
+ }
1826
+ rejectReadyWaiters(error) {
1827
+ this.readyWaiters.forEach((waiter) => {
1828
+ if (waiter.timer) clearTimeout(waiter.timer);
1829
+ waiter.reject(error);
1830
+ });
1831
+ this.readyWaiters = [];
1832
+ }
1626
1833
  // ===== postMessage bridge =====
1627
1834
  postToViewer(type, payload) {
1628
1835
  var _a;