3dviewer-sdk 1.0.28 → 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);
@@ -984,7 +1063,11 @@ var ModelTreeModule = class {
984
1063
  }
985
1064
  selectNodes(nodeIds) {
986
1065
  const normalizedNodeIds = this.normalizeNodeIds(nodeIds);
987
- console.log("[3dviewer-sdk] modelTree.selectNodes", { nodeIds, normalizedNodeIds });
1066
+ console.log("[3dviewer-sdk] modelTree.selectNodes post message", {
1067
+ nodeIds,
1068
+ normalizedNodeIds,
1069
+ willClearSelection: normalizedNodeIds.length === 0
1070
+ });
988
1071
  this.postTreeSelectNode({
989
1072
  nodeIds: normalizedNodeIds
990
1073
  });
@@ -1263,12 +1346,50 @@ var Viewer3D = class {
1263
1346
  this.containerEl = null;
1264
1347
  this.iframeEl = null;
1265
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
+ };
1266
1356
  this.emitter = new Emitter();
1267
1357
  this.handleMessage = (event) => {
1268
- var _a, _b, _c, _d, _e;
1358
+ var _a, _b, _c;
1269
1359
  const data = event.data;
1270
1360
  if (!data || typeof data !== "object") return;
1271
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
+ }
1272
1393
  case "viewer-home-click" /* HOME_CLICK */:
1273
1394
  this._emit("camera:home", { timestamp: Date.now() });
1274
1395
  break;
@@ -1283,9 +1404,6 @@ var Viewer3D = class {
1283
1404
  });
1284
1405
  break;
1285
1406
  }
1286
- case "viewer-node-select" /* NODE_SELECT */:
1287
- this._emit("node:select", { nodeId: String((_b = (_a = data.payload) == null ? void 0 : _a.nodeId) != null ? _b : ""), timestamp: Date.now() });
1288
- break;
1289
1407
  case "viewer-node-selection-change" /* NODE_SELECTION_CHANGE */: {
1290
1408
  const payload = data.payload;
1291
1409
  const rawNodeIds = payload == null ? void 0 : payload.nodeIds;
@@ -1297,7 +1415,9 @@ var Viewer3D = class {
1297
1415
  break;
1298
1416
  }
1299
1417
  case "viewer-pan-change" /* PAN_CHANGE */:
1300
- 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
+ });
1301
1421
  break;
1302
1422
  case "viewer-pdf-plan-mode" /* PDF_PLAN_MODE */: {
1303
1423
  const payload = data.payload;
@@ -1355,7 +1475,8 @@ var Viewer3D = class {
1355
1475
  }
1356
1476
  case "viewer-tree-node-ids" /* TREE_NODE_IDS */: {
1357
1477
  const payload = data.payload;
1358
- if (!payload || !payload.requestId || !Array.isArray(payload.nodeIds)) break;
1478
+ if (!payload || !payload.requestId || !Array.isArray(payload.nodeIds))
1479
+ break;
1359
1480
  this._emit("modelTree:node-ids", {
1360
1481
  requestId: String(payload.requestId),
1361
1482
  nodeIds: payload.nodeIds.map(String),
@@ -1365,7 +1486,8 @@ var Viewer3D = class {
1365
1486
  }
1366
1487
  case "viewer-tree-nodes" /* TREE_NODES */: {
1367
1488
  const payload = data.payload;
1368
- if (!payload || !payload.requestId || !Array.isArray(payload.nodes)) break;
1489
+ if (!payload || !payload.requestId || !Array.isArray(payload.nodes))
1490
+ break;
1369
1491
  this._emit("modelTree:nodes", {
1370
1492
  requestId: String(payload.requestId),
1371
1493
  nodes: payload.nodes.filter((node) => node && typeof node === "object").map((node) => {
@@ -1391,7 +1513,8 @@ var Viewer3D = class {
1391
1513
  }
1392
1514
  case "viewer-sheets-list" /* SHEETS_LIST */: {
1393
1515
  const payload = data.payload;
1394
- if (!payload || !payload.requestId || !Array.isArray(payload.sheets)) break;
1516
+ if (!payload || !payload.requestId || !Array.isArray(payload.sheets))
1517
+ break;
1395
1518
  this._emit("sheets:list", {
1396
1519
  requestId: String(payload.requestId),
1397
1520
  sheets: payload.sheets.map((sheet) => {
@@ -1403,17 +1526,18 @@ var Viewer3D = class {
1403
1526
  viewId: sheet.viewId ? String(sheet.viewId) : void 0
1404
1527
  };
1405
1528
  }),
1406
- activeSheetId: (_d = payload.activeSheetId) != null ? _d : null,
1529
+ activeSheetId: (_b = payload.activeSheetId) != null ? _b : null,
1407
1530
  timestamp: Number(payload.timestamp) || Date.now()
1408
1531
  });
1409
1532
  break;
1410
1533
  }
1411
1534
  case "viewer-object-properties-list" /* OBJECT_PROPERTIES_LIST */: {
1412
1535
  const payload = data.payload;
1413
- if (!payload || !payload.requestId || !Array.isArray(payload.properties)) break;
1536
+ if (!payload || !payload.requestId || !Array.isArray(payload.properties))
1537
+ break;
1414
1538
  this._emit("object-properties:list", {
1415
1539
  requestId: String(payload.requestId),
1416
- selectionKey: String((_e = payload.selectionKey) != null ? _e : ""),
1540
+ selectionKey: String((_c = payload.selectionKey) != null ? _c : ""),
1417
1541
  nodeIds: Array.isArray(payload.nodeIds) ? payload.nodeIds.map(String) : [],
1418
1542
  persistentIds: Array.isArray(payload.persistentIds) ? payload.persistentIds.map(String) : [],
1419
1543
  properties: payload.properties.filter((item) => item && typeof item === "object").map((item) => ({ ...item })),
@@ -1423,7 +1547,8 @@ var Viewer3D = class {
1423
1547
  }
1424
1548
  case "viewer-linked-objects-list" /* LINKED_OBJECTS_LIST */: {
1425
1549
  const payload = data.payload;
1426
- if (!payload || !payload.requestId || !Array.isArray(payload.linkedObjects)) break;
1550
+ if (!payload || !payload.requestId || !Array.isArray(payload.linkedObjects))
1551
+ break;
1427
1552
  this._emit("linked-objects:list", {
1428
1553
  requestId: String(payload.requestId),
1429
1554
  linkedObjects: payload.linkedObjects.filter((item) => item && typeof item === "object").map((item) => ({ ...item })),
@@ -1433,11 +1558,12 @@ var Viewer3D = class {
1433
1558
  }
1434
1559
  case "viewer-states-objects-list" /* STATES_OBJECTS_LIST */: {
1435
1560
  const payload = data.payload;
1436
- if (!payload || !payload.requestId || !Array.isArray(payload.statesObjects)) break;
1561
+ if (!payload || !payload.requestId || !Array.isArray(payload.statesObjects))
1562
+ break;
1437
1563
  this._emit("states-objects:list", {
1438
1564
  requestId: String(payload.requestId),
1439
1565
  statesObjects: payload.statesObjects.map((item) => {
1440
- var _a2, _b2, _c2, _d2, _e2, _f, _g;
1566
+ var _a2, _b2, _c2, _d, _e, _f, _g;
1441
1567
  return {
1442
1568
  id: String((_a2 = item.id) != null ? _a2 : ""),
1443
1569
  name: String((_b2 = item.name) != null ? _b2 : ""),
@@ -1450,7 +1576,7 @@ var Viewer3D = class {
1450
1576
  };
1451
1577
  }) : [],
1452
1578
  states: {
1453
- 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 : ""),
1454
1580
  type: String((_g = (_f = item.states) == null ? void 0 : _f.type) != null ? _g : "")
1455
1581
  }
1456
1582
  };
@@ -1461,7 +1587,8 @@ var Viewer3D = class {
1461
1587
  }
1462
1588
  case "viewer-markup-list" /* MARKUP_LIST */: {
1463
1589
  const payload = data.payload;
1464
- if (!payload || !payload.requestId || !Array.isArray(payload.markups)) break;
1590
+ if (!payload || !payload.requestId || !Array.isArray(payload.markups))
1591
+ break;
1465
1592
  this._emit("markup:list", {
1466
1593
  requestId: String(payload.requestId),
1467
1594
  markups: payload.markups.map((markup) => {
@@ -1530,6 +1657,36 @@ var Viewer3D = class {
1530
1657
  var _a;
1531
1658
  return (_a = this.options.url) != null ? _a : null;
1532
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
+ }
1533
1690
  // ===== lifecycle =====
1534
1691
  init() {
1535
1692
  if (this.initialized) return;
@@ -1541,7 +1698,12 @@ var Viewer3D = class {
1541
1698
  async render(file) {
1542
1699
  this.ensureInit();
1543
1700
  if (this.iframeEl) return;
1701
+ this.resetReadyState();
1544
1702
  if (!this.options.url) return this.files.render(file);
1703
+ this.setLoadingState({
1704
+ loading: true,
1705
+ phase: "loading"
1706
+ });
1545
1707
  const iframe = document.createElement("iframe");
1546
1708
  iframe.src = this.withInitialOptions(this.options.url);
1547
1709
  iframe.style.border = "none";
@@ -1549,13 +1711,19 @@ var Viewer3D = class {
1549
1711
  iframe.style.height = this.options.height || "100%";
1550
1712
  iframe.width = this.options.width || "100%";
1551
1713
  iframe.height = this.options.height || "100%";
1552
- if (this.options.sandbox) iframe.setAttribute("sandbox", this.options.sandbox);
1714
+ if (this.options.sandbox)
1715
+ iframe.setAttribute("sandbox", this.options.sandbox);
1553
1716
  this.containerEl.appendChild(iframe);
1554
1717
  this.iframeEl = iframe;
1555
1718
  }
1556
1719
  open(url) {
1557
1720
  this.ensureInit();
1558
1721
  this.options.url = url;
1722
+ this.resetReadyState();
1723
+ this.setLoadingState({
1724
+ loading: true,
1725
+ phase: "loading"
1726
+ });
1559
1727
  const finalUrl = this.withInitialOptions(url);
1560
1728
  if (!this.iframeEl) {
1561
1729
  this.render();
@@ -1574,16 +1742,27 @@ var Viewer3D = class {
1574
1742
  this.iframeEl = null;
1575
1743
  this.containerEl = null;
1576
1744
  this.initialized = false;
1745
+ this.rejectReadyWaiters(
1746
+ new Error("Viewer was destroyed before becoming ready")
1747
+ );
1577
1748
  }
1578
1749
  ensureInit() {
1579
- 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");
1580
1752
  }
1581
1753
  withInitialOptions(url) {
1754
+ var _a;
1582
1755
  try {
1583
1756
  const parsedUrl = new URL(url, window.location.href);
1584
1757
  const initialToolbar = this.normalizeInitialToolbar();
1585
1758
  if (initialToolbar) {
1586
- 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");
1587
1766
  }
1588
1767
  return parsedUrl.toString();
1589
1768
  } catch {
@@ -1597,17 +1776,25 @@ var Viewer3D = class {
1597
1776
  return { [initialToolbar]: true };
1598
1777
  }
1599
1778
  if (Array.isArray(initialToolbar)) {
1600
- return initialToolbar.reduce((result, target) => {
1601
- result[target] = true;
1602
- return result;
1603
- }, {});
1779
+ return initialToolbar.reduce(
1780
+ (result, target) => {
1781
+ result[target] = true;
1782
+ return result;
1783
+ },
1784
+ {}
1785
+ );
1604
1786
  }
1605
- const entries = Object.entries(initialToolbar).filter(([, enabled]) => enabled === true);
1787
+ const entries = Object.entries(initialToolbar).filter(
1788
+ ([, enabled]) => enabled === true
1789
+ );
1606
1790
  if (entries.length === 0) return null;
1607
- return entries.reduce((result, [target]) => {
1608
- result[target] = true;
1609
- return result;
1610
- }, {});
1791
+ return entries.reduce(
1792
+ (result, [target]) => {
1793
+ result[target] = true;
1794
+ return result;
1795
+ },
1796
+ {}
1797
+ );
1611
1798
  }
1612
1799
  // ===== typed internal events used by modules =====
1613
1800
  _on(event, cb) {
@@ -1619,6 +1806,30 @@ var Viewer3D = class {
1619
1806
  _emit(event, payload) {
1620
1807
  this.emitter.emit(event, payload);
1621
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
+ }
1622
1833
  // ===== postMessage bridge =====
1623
1834
  postToViewer(type, payload) {
1624
1835
  var _a;