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.mjs CHANGED
@@ -139,7 +139,7 @@ var NodeModule = class {
139
139
  constructor(viewer) {
140
140
  this.viewer = viewer;
141
141
  this.on = {
142
- select: (cb) => this.viewer._on("node:select", cb),
142
+ // SDK no longer supports first-node-only select events; listen to selectionChange for the full node list.
143
143
  selectionChange: (cb) => this.viewer._on("node:selection-change", cb)
144
144
  };
145
145
  }
@@ -186,52 +186,75 @@ var FilesModule = class {
186
186
  // Upload file to conversion server and keep generated baseFileId in session.
187
187
  async upload(file) {
188
188
  const target = this.resolveFile(file);
189
- return this.withOperation({ stage: "uploading", message: "Uploading file..." }, async () => {
190
- var _a;
191
- this.viewer._emit("files:upload:start", { fileName: target.name });
192
- await this.uploadInternal(target);
193
- const baseFileId = ((_a = this.getUploadSessionForFile(target)) == null ? void 0 : _a.baseFileId) || "";
194
- this.viewer._emit("files:upload:success", { fileName: target.name, baseFileId });
195
- return { fileName: target.name, baseFileId };
196
- });
189
+ return this.withOperation(
190
+ { stage: "uploading", message: "Uploading file..." },
191
+ async () => {
192
+ var _a;
193
+ this.viewer._emit("files:upload:start", { fileName: target.name });
194
+ await this.uploadInternal(target);
195
+ const baseFileId = ((_a = this.getUploadSessionForFile(target)) == null ? void 0 : _a.baseFileId) || "";
196
+ this.viewer._emit("files:upload:success", {
197
+ fileName: target.name,
198
+ baseFileId
199
+ });
200
+ return { fileName: target.name, baseFileId };
201
+ }
202
+ );
197
203
  }
198
204
  // Trigger conversion flow and resolve final viewer metadata.
199
205
  async convert(file, options = {}) {
200
206
  const target = this.resolveFile(file);
201
- return this.withOperation({ stage: "converting", message: "Converting file..." }, async () => {
202
- this.viewer._emit("files:conversion:start", { fileName: target.name });
203
- try {
204
- const prepared = await this.convertInternal(target, options);
205
- this.viewer._emit("files:conversion:success", prepared);
206
- return prepared;
207
- } catch (e) {
208
- this.viewer._emit("files:conversion:error", { fileName: target.name, error: this.toErrorMessage(e) });
209
- throw e;
207
+ return this.withOperation(
208
+ { stage: "converting", message: "Converting file..." },
209
+ async () => {
210
+ this.viewer._emit("files:conversion:start", { fileName: target.name });
211
+ try {
212
+ const prepared = await this.convertInternal(target, options);
213
+ this.viewer._emit("files:conversion:success", prepared);
214
+ return prepared;
215
+ } catch (e) {
216
+ this.viewer._emit("files:conversion:error", {
217
+ fileName: target.name,
218
+ error: this.toErrorMessage(e)
219
+ });
220
+ throw e;
221
+ }
210
222
  }
211
- });
223
+ );
212
224
  }
213
225
  // Convenience API: upload first, then convert in one call.
214
226
  async prepare(file, options = {}) {
215
227
  const target = this.resolveFile(file);
216
- return this.withOperation({ stage: "uploading", message: "Preparing file..." }, async () => {
217
- await this.uploadInternal(target);
218
- const prepared = await this.convertInternal(target, options);
219
- return prepared;
220
- });
228
+ return this.withOperation(
229
+ { stage: "uploading", message: "Preparing file..." },
230
+ async () => {
231
+ await this.uploadInternal(target);
232
+ const prepared = await this.convertInternal(target, options);
233
+ return prepared;
234
+ }
235
+ );
221
236
  }
222
237
  // Trigger the newer downloadUrl-based conversion flow.
223
238
  async convertV2(options) {
224
- return this.withOperation({ stage: "converting", message: "Converting file..." }, async () => {
225
- this.viewer._emit("files:conversion:start", { fileName: options.filename });
226
- try {
227
- const prepared = await this.convertV2Internal(options);
228
- this.viewer._emit("files:conversion:success", prepared);
229
- return prepared;
230
- } catch (e) {
231
- this.viewer._emit("files:conversion:error", { fileName: options.filename, error: this.toErrorMessage(e) });
232
- throw e;
239
+ return this.withOperation(
240
+ { stage: "converting", message: "Converting file..." },
241
+ async () => {
242
+ this.viewer._emit("files:conversion:start", {
243
+ fileName: options.filename
244
+ });
245
+ try {
246
+ const prepared = await this.convertV2Internal(options);
247
+ this.viewer._emit("files:conversion:success", prepared);
248
+ return prepared;
249
+ } catch (e) {
250
+ this.viewer._emit("files:conversion:error", {
251
+ fileName: options.filename,
252
+ error: this.toErrorMessage(e)
253
+ });
254
+ throw e;
255
+ }
233
256
  }
234
- });
257
+ );
235
258
  }
236
259
  // Check stream file info by one or more baseFileId values.
237
260
  async checkFileInfo(baseFileIds) {
@@ -247,7 +270,9 @@ var FilesModule = class {
247
270
  body: JSON.stringify(payload)
248
271
  });
249
272
  if (!response.ok) {
250
- throw new Error(`Check file info failed (${response.status} ${response.statusText})`);
273
+ throw new Error(
274
+ `Check file info failed (${response.status} ${response.statusText})`
275
+ );
251
276
  }
252
277
  const text = await response.text();
253
278
  if (!text) return null;
@@ -265,27 +290,34 @@ var FilesModule = class {
265
290
  this.viewer.open(url);
266
291
  this.viewer._emit("files:render:success", { url });
267
292
  } catch (e) {
268
- this.viewer._emit("files:render:error", { url, error: this.toErrorMessage(e) });
293
+ this.viewer._emit("files:render:error", {
294
+ url,
295
+ error: this.toErrorMessage(e)
296
+ });
269
297
  throw e;
270
298
  }
271
299
  }
272
300
  // Full pipeline: upload + convert + open iframe.
273
301
  async render(file, options = {}) {
274
302
  const target = this.resolveFile(file);
275
- return this.withOperation({ stage: "uploading", message: "Uploading + converting + opening..." }, async () => {
276
- await this.upload(target);
277
- const prepared = await this.convert(target, options);
278
- this.updateState({ stage: "rendering", message: "Opening viewer..." });
279
- this.open(prepared);
280
- this.viewer._emit("files:load:success", prepared);
281
- return prepared;
282
- });
303
+ return this.withOperation(
304
+ { stage: "uploading", message: "Uploading + converting + opening..." },
305
+ async () => {
306
+ await this.upload(target);
307
+ const prepared = await this.convert(target, options);
308
+ this.updateState({ stage: "rendering", message: "Opening viewer..." });
309
+ this.open(prepared);
310
+ this.viewer._emit("files:load:success", prepared);
311
+ return prepared;
312
+ }
313
+ );
283
314
  }
284
315
  // Resolve file argument, fallback to options.file, and persist it back.
285
316
  resolveFile(file) {
286
317
  const optFile = this.viewer.getOptions().file;
287
318
  const target = file || optFile;
288
- if (!target) throw new Error("No file provided. Pass a File or set options.file");
319
+ if (!target)
320
+ throw new Error("No file provided. Pass a File or set options.file");
289
321
  this.viewer.patchOptions({ file: target });
290
322
  return target;
291
323
  }
@@ -306,7 +338,8 @@ var FilesModule = class {
306
338
  if (viewerUrl) {
307
339
  try {
308
340
  const pathname = new URL(viewerUrl, window.location.href).pathname;
309
- if (pathname && pathname !== "/") return this.normalizeViewerPath(pathname);
341
+ if (pathname && pathname !== "/")
342
+ return this.normalizeViewerPath(pathname);
310
343
  } catch {
311
344
  }
312
345
  }
@@ -323,14 +356,18 @@ var FilesModule = class {
323
356
  const configuredViewerBaseUrl = this.config.baseUrl || this.viewer.getOptions().baseUrl;
324
357
  if (configuredViewerBaseUrl) {
325
358
  try {
326
- return this.normalizeBaseUrl(new URL(configuredViewerBaseUrl, window.location.href).origin);
359
+ return this.normalizeBaseUrl(
360
+ new URL(configuredViewerBaseUrl, window.location.href).origin
361
+ );
327
362
  } catch {
328
363
  }
329
364
  }
330
365
  const viewerUrl = this.viewer.getOptions().url;
331
366
  if (viewerUrl) {
332
367
  try {
333
- return this.normalizeBaseUrl(new URL(viewerUrl, window.location.href).origin);
368
+ return this.normalizeBaseUrl(
369
+ new URL(viewerUrl, window.location.href).origin
370
+ );
334
371
  } catch {
335
372
  }
336
373
  }
@@ -351,7 +388,8 @@ var FilesModule = class {
351
388
  }
352
389
  // Create a UUID-like baseFileId when caller does not provide one.
353
390
  createBaseFileId() {
354
- if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
391
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto)
392
+ return crypto.randomUUID();
355
393
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
356
394
  const r = Math.floor(Math.random() * 16);
357
395
  const v = c === "x" ? r : r & 3 | 8;
@@ -382,15 +420,25 @@ var FilesModule = class {
382
420
  const session = existing || this.createUploadSession(file);
383
421
  const hostConversion = this.resolveHostConversion();
384
422
  const path = this.getUploadPath();
385
- const url = `${hostConversion}/api/File/upload?path=${encodeURIComponent(path)}`;
423
+ const url = `${hostConversion}/api/File/upload?path=${encodeURIComponent(
424
+ path
425
+ )}`;
386
426
  const formData = new FormData();
387
427
  formData.append("file", file, file.name);
388
- const res = await fetch(url, { method: "POST", body: formData, headers: { Accept: "text/plain" } });
389
- if (!res.ok) throw new Error(`Upload failed (${res.status} ${res.statusText})`);
428
+ const res = await fetch(url, {
429
+ method: "POST",
430
+ body: formData,
431
+ headers: { Accept: "text/plain" }
432
+ });
433
+ if (!res.ok)
434
+ throw new Error(`Upload failed (${res.status} ${res.statusText})`);
390
435
  this.lastUploadSession = session;
391
436
  } catch (e) {
392
437
  const msg = this.toErrorMessage(e);
393
- this.viewer._emit("files:upload:error", { fileName: file.name, error: msg });
438
+ this.viewer._emit("files:upload:error", {
439
+ fileName: file.name,
440
+ error: msg
441
+ });
394
442
  throw e;
395
443
  }
396
444
  }
@@ -544,11 +592,20 @@ var FilesModule = class {
544
592
  const baseMajorRev = (_b = cacheResult.baseMajorRev) != null ? _b : 0;
545
593
  const baseMinorRev = (_c = cacheResult.baseMinorRev) != null ? _c : 0;
546
594
  const fileName = cacheResult.filename || cacheResult.fileName || file.name;
547
- const cacheListItem = { baseFileId, baseMajorRev, baseMinorRev, fileName };
595
+ const cacheListItem = {
596
+ baseFileId,
597
+ baseMajorRev,
598
+ baseMinorRev,
599
+ fileName
600
+ };
548
601
  if (cacheResult.cacheStatus !== 2) {
549
- throw new Error(`Conversion not ready after first request (cacheStatus=${(_d = cacheResult.cacheStatus) != null ? _d : "unknown"})`);
602
+ throw new Error(
603
+ `Conversion not ready after first request (cacheStatus=${(_d = cacheResult.cacheStatus) != null ? _d : "unknown"})`
604
+ );
550
605
  }
551
- const query = new URLSearchParams({ fileList: JSON.stringify([cacheListItem]) }).toString();
606
+ const query = new URLSearchParams({
607
+ fileList: JSON.stringify([cacheListItem])
608
+ }).toString();
552
609
  const viewerBase = this.resolveViewerOrigin();
553
610
  const viewerPath = this.resolveViewerPath();
554
611
  const url = `${viewerBase}${viewerPath}?${query}`;
@@ -563,10 +620,19 @@ var FilesModule = class {
563
620
  const baseMinorRev = (_e = (_d = cacheResult.baseMinorRev) != null ? _d : options.baseMinorRev) != null ? _e : 0;
564
621
  const fileName = cacheResult.filename || cacheResult.fileName || options.filename;
565
622
  if (cacheResult.cacheStatus !== void 0 && cacheResult.cacheStatus !== 2) {
566
- throw new Error(`Conversion not ready after v2 request (cacheStatus=${cacheResult.cacheStatus})`);
623
+ throw new Error(
624
+ `Conversion not ready after v2 request (cacheStatus=${cacheResult.cacheStatus})`
625
+ );
567
626
  }
568
- const cacheListItem = { baseFileId, baseMajorRev, baseMinorRev, fileName };
569
- const query = new URLSearchParams({ fileList: JSON.stringify([cacheListItem]) }).toString();
627
+ const cacheListItem = {
628
+ baseFileId,
629
+ baseMajorRev,
630
+ baseMinorRev,
631
+ fileName
632
+ };
633
+ const query = new URLSearchParams({
634
+ fileList: JSON.stringify([cacheListItem])
635
+ }).toString();
570
636
  const viewerBase = this.resolveViewerOrigin();
571
637
  const viewerPath = this.resolveViewerPath();
572
638
  const url = `${viewerBase}${viewerPath}?${query}`;
@@ -577,6 +643,15 @@ var FilesModule = class {
577
643
  const elapsedMs = this.operationStartTime > 0 ? Date.now() - this.operationStartTime : 0;
578
644
  this.state = { ...this.state, ...next, elapsedMs };
579
645
  this.viewer._emit("files:state", this.state);
646
+ this.viewer._emit("loading:change", {
647
+ loading: this.state.isLoading,
648
+ phase: this.toViewerLoadingPhase(this.state.stage)
649
+ });
650
+ }
651
+ toViewerLoadingPhase(stage) {
652
+ if (stage === "error") return "error";
653
+ if (stage === "idle" || stage === "completed") return "ready";
654
+ return "loading";
580
655
  }
581
656
  // Shared wrapper to handle loading state lifecycle and top-level errors.
582
657
  async withOperation(initial, run) {
@@ -588,7 +663,11 @@ var FilesModule = class {
588
663
  });
589
664
  try {
590
665
  const result = await run();
591
- this.updateState({ isLoading: false, stage: "completed", message: "Completed" });
666
+ this.updateState({
667
+ isLoading: false,
668
+ stage: "completed",
669
+ message: "Completed"
670
+ });
592
671
  return result;
593
672
  } catch (e) {
594
673
  const msg = this.toErrorMessage(e);
@@ -1241,12 +1320,50 @@ var Viewer3D = class {
1241
1320
  this.containerEl = null;
1242
1321
  this.iframeEl = null;
1243
1322
  this.initialized = false;
1323
+ this.viewerReady = false;
1324
+ this.readyPayload = null;
1325
+ this.readyWaiters = [];
1326
+ this.loadingState = {
1327
+ loading: false,
1328
+ phase: "ready"
1329
+ };
1244
1330
  this.emitter = new Emitter();
1245
1331
  this.handleMessage = (event) => {
1246
- var _a, _b, _c, _d, _e;
1332
+ var _a, _b, _c;
1247
1333
  const data = event.data;
1248
1334
  if (!data || typeof data !== "object") return;
1249
1335
  switch (data.type) {
1336
+ case "viewer-loading-change" /* LOADING_CHANGE */: {
1337
+ const payload = data.payload;
1338
+ if (!payload) break;
1339
+ this.setLoadingState({
1340
+ loading: Boolean(payload.loading),
1341
+ phase: payload.phase,
1342
+ viewId: payload.viewId ? String(payload.viewId) : void 0,
1343
+ modelFileId: payload.modelFileId ? String(payload.modelFileId) : void 0,
1344
+ timestamp: payload.timestamp ? Number(payload.timestamp) : void 0
1345
+ });
1346
+ break;
1347
+ }
1348
+ case "viewer-ready" /* READY */: {
1349
+ const payload = data.payload;
1350
+ const readyPayload = {
1351
+ ready: true,
1352
+ viewId: (payload == null ? void 0 : payload.viewId) ? String(payload.viewId) : void 0,
1353
+ modelFileId: (payload == null ? void 0 : payload.modelFileId) ? String(payload.modelFileId) : void 0,
1354
+ timestamp: Number(payload == null ? void 0 : payload.timestamp) || Date.now()
1355
+ };
1356
+ this.setLoadingState({
1357
+ loading: false,
1358
+ phase: "ready",
1359
+ viewId: readyPayload.viewId,
1360
+ modelFileId: readyPayload.modelFileId,
1361
+ timestamp: readyPayload.timestamp
1362
+ });
1363
+ this.resolveReady(readyPayload);
1364
+ this._emit("viewer:ready", readyPayload);
1365
+ break;
1366
+ }
1250
1367
  case "viewer-home-click" /* HOME_CLICK */:
1251
1368
  this._emit("camera:home", { timestamp: Date.now() });
1252
1369
  break;
@@ -1261,9 +1378,6 @@ var Viewer3D = class {
1261
1378
  });
1262
1379
  break;
1263
1380
  }
1264
- case "viewer-node-select" /* NODE_SELECT */:
1265
- this._emit("node:select", { nodeId: String((_b = (_a = data.payload) == null ? void 0 : _a.nodeId) != null ? _b : ""), timestamp: Date.now() });
1266
- break;
1267
1381
  case "viewer-node-selection-change" /* NODE_SELECTION_CHANGE */: {
1268
1382
  const payload = data.payload;
1269
1383
  const rawNodeIds = payload == null ? void 0 : payload.nodeIds;
@@ -1275,7 +1389,9 @@ var Viewer3D = class {
1275
1389
  break;
1276
1390
  }
1277
1391
  case "viewer-pan-change" /* PAN_CHANGE */:
1278
- this._emit("interaction:pan-change", { enabled: Boolean((_c = data.payload) == null ? void 0 : _c.enabled) });
1392
+ this._emit("interaction:pan-change", {
1393
+ enabled: Boolean((_a = data.payload) == null ? void 0 : _a.enabled)
1394
+ });
1279
1395
  break;
1280
1396
  case "viewer-pdf-plan-mode" /* PDF_PLAN_MODE */: {
1281
1397
  const payload = data.payload;
@@ -1333,7 +1449,8 @@ var Viewer3D = class {
1333
1449
  }
1334
1450
  case "viewer-tree-node-ids" /* TREE_NODE_IDS */: {
1335
1451
  const payload = data.payload;
1336
- if (!payload || !payload.requestId || !Array.isArray(payload.nodeIds)) break;
1452
+ if (!payload || !payload.requestId || !Array.isArray(payload.nodeIds))
1453
+ break;
1337
1454
  this._emit("modelTree:node-ids", {
1338
1455
  requestId: String(payload.requestId),
1339
1456
  nodeIds: payload.nodeIds.map(String),
@@ -1343,7 +1460,8 @@ var Viewer3D = class {
1343
1460
  }
1344
1461
  case "viewer-tree-nodes" /* TREE_NODES */: {
1345
1462
  const payload = data.payload;
1346
- if (!payload || !payload.requestId || !Array.isArray(payload.nodes)) break;
1463
+ if (!payload || !payload.requestId || !Array.isArray(payload.nodes))
1464
+ break;
1347
1465
  this._emit("modelTree:nodes", {
1348
1466
  requestId: String(payload.requestId),
1349
1467
  nodes: payload.nodes.filter((node) => node && typeof node === "object").map((node) => {
@@ -1369,7 +1487,8 @@ var Viewer3D = class {
1369
1487
  }
1370
1488
  case "viewer-sheets-list" /* SHEETS_LIST */: {
1371
1489
  const payload = data.payload;
1372
- if (!payload || !payload.requestId || !Array.isArray(payload.sheets)) break;
1490
+ if (!payload || !payload.requestId || !Array.isArray(payload.sheets))
1491
+ break;
1373
1492
  this._emit("sheets:list", {
1374
1493
  requestId: String(payload.requestId),
1375
1494
  sheets: payload.sheets.map((sheet) => {
@@ -1381,17 +1500,18 @@ var Viewer3D = class {
1381
1500
  viewId: sheet.viewId ? String(sheet.viewId) : void 0
1382
1501
  };
1383
1502
  }),
1384
- activeSheetId: (_d = payload.activeSheetId) != null ? _d : null,
1503
+ activeSheetId: (_b = payload.activeSheetId) != null ? _b : null,
1385
1504
  timestamp: Number(payload.timestamp) || Date.now()
1386
1505
  });
1387
1506
  break;
1388
1507
  }
1389
1508
  case "viewer-object-properties-list" /* OBJECT_PROPERTIES_LIST */: {
1390
1509
  const payload = data.payload;
1391
- if (!payload || !payload.requestId || !Array.isArray(payload.properties)) break;
1510
+ if (!payload || !payload.requestId || !Array.isArray(payload.properties))
1511
+ break;
1392
1512
  this._emit("object-properties:list", {
1393
1513
  requestId: String(payload.requestId),
1394
- selectionKey: String((_e = payload.selectionKey) != null ? _e : ""),
1514
+ selectionKey: String((_c = payload.selectionKey) != null ? _c : ""),
1395
1515
  nodeIds: Array.isArray(payload.nodeIds) ? payload.nodeIds.map(String) : [],
1396
1516
  persistentIds: Array.isArray(payload.persistentIds) ? payload.persistentIds.map(String) : [],
1397
1517
  properties: payload.properties.filter((item) => item && typeof item === "object").map((item) => ({ ...item })),
@@ -1401,7 +1521,8 @@ var Viewer3D = class {
1401
1521
  }
1402
1522
  case "viewer-linked-objects-list" /* LINKED_OBJECTS_LIST */: {
1403
1523
  const payload = data.payload;
1404
- if (!payload || !payload.requestId || !Array.isArray(payload.linkedObjects)) break;
1524
+ if (!payload || !payload.requestId || !Array.isArray(payload.linkedObjects))
1525
+ break;
1405
1526
  this._emit("linked-objects:list", {
1406
1527
  requestId: String(payload.requestId),
1407
1528
  linkedObjects: payload.linkedObjects.filter((item) => item && typeof item === "object").map((item) => ({ ...item })),
@@ -1411,11 +1532,12 @@ var Viewer3D = class {
1411
1532
  }
1412
1533
  case "viewer-states-objects-list" /* STATES_OBJECTS_LIST */: {
1413
1534
  const payload = data.payload;
1414
- if (!payload || !payload.requestId || !Array.isArray(payload.statesObjects)) break;
1535
+ if (!payload || !payload.requestId || !Array.isArray(payload.statesObjects))
1536
+ break;
1415
1537
  this._emit("states-objects:list", {
1416
1538
  requestId: String(payload.requestId),
1417
1539
  statesObjects: payload.statesObjects.map((item) => {
1418
- var _a2, _b2, _c2, _d2, _e2, _f, _g;
1540
+ var _a2, _b2, _c2, _d, _e, _f, _g;
1419
1541
  return {
1420
1542
  id: String((_a2 = item.id) != null ? _a2 : ""),
1421
1543
  name: String((_b2 = item.name) != null ? _b2 : ""),
@@ -1428,7 +1550,7 @@ var Viewer3D = class {
1428
1550
  };
1429
1551
  }) : [],
1430
1552
  states: {
1431
- color: String((_e2 = (_d2 = item.states) == null ? void 0 : _d2.color) != null ? _e2 : ""),
1553
+ color: String((_e = (_d = item.states) == null ? void 0 : _d.color) != null ? _e : ""),
1432
1554
  type: String((_g = (_f = item.states) == null ? void 0 : _f.type) != null ? _g : "")
1433
1555
  }
1434
1556
  };
@@ -1439,7 +1561,8 @@ var Viewer3D = class {
1439
1561
  }
1440
1562
  case "viewer-markup-list" /* MARKUP_LIST */: {
1441
1563
  const payload = data.payload;
1442
- if (!payload || !payload.requestId || !Array.isArray(payload.markups)) break;
1564
+ if (!payload || !payload.requestId || !Array.isArray(payload.markups))
1565
+ break;
1443
1566
  this._emit("markup:list", {
1444
1567
  requestId: String(payload.requestId),
1445
1568
  markups: payload.markups.map((markup) => {
@@ -1508,6 +1631,36 @@ var Viewer3D = class {
1508
1631
  var _a;
1509
1632
  return (_a = this.options.url) != null ? _a : null;
1510
1633
  }
1634
+ on(event, cb) {
1635
+ return this._on(event, cb);
1636
+ }
1637
+ off(event, cb) {
1638
+ this._off(event, cb);
1639
+ }
1640
+ getLoadingState() {
1641
+ return { ...this.loadingState };
1642
+ }
1643
+ ready(options) {
1644
+ if (this.viewerReady && this.readyPayload) {
1645
+ return Promise.resolve({ ...this.readyPayload });
1646
+ }
1647
+ const timeoutMs = options == null ? void 0 : options.timeoutMs;
1648
+ return new Promise((resolve, reject) => {
1649
+ const waiter = {
1650
+ resolve: (payload) => resolve({ ...payload }),
1651
+ reject
1652
+ };
1653
+ if (typeof timeoutMs === "number" && timeoutMs > 0) {
1654
+ waiter.timer = setTimeout(() => {
1655
+ this.readyWaiters = this.readyWaiters.filter(
1656
+ (item) => item !== waiter
1657
+ );
1658
+ reject(new Error("Timeout while waiting for viewer to be ready"));
1659
+ }, timeoutMs);
1660
+ }
1661
+ this.readyWaiters.push(waiter);
1662
+ });
1663
+ }
1511
1664
  // ===== lifecycle =====
1512
1665
  init() {
1513
1666
  if (this.initialized) return;
@@ -1519,7 +1672,12 @@ var Viewer3D = class {
1519
1672
  async render(file) {
1520
1673
  this.ensureInit();
1521
1674
  if (this.iframeEl) return;
1675
+ this.resetReadyState();
1522
1676
  if (!this.options.url) return this.files.render(file);
1677
+ this.setLoadingState({
1678
+ loading: true,
1679
+ phase: "loading"
1680
+ });
1523
1681
  const iframe = document.createElement("iframe");
1524
1682
  iframe.src = this.withInitialOptions(this.options.url);
1525
1683
  iframe.style.border = "none";
@@ -1527,13 +1685,19 @@ var Viewer3D = class {
1527
1685
  iframe.style.height = this.options.height || "100%";
1528
1686
  iframe.width = this.options.width || "100%";
1529
1687
  iframe.height = this.options.height || "100%";
1530
- if (this.options.sandbox) iframe.setAttribute("sandbox", this.options.sandbox);
1688
+ if (this.options.sandbox)
1689
+ iframe.setAttribute("sandbox", this.options.sandbox);
1531
1690
  this.containerEl.appendChild(iframe);
1532
1691
  this.iframeEl = iframe;
1533
1692
  }
1534
1693
  open(url) {
1535
1694
  this.ensureInit();
1536
1695
  this.options.url = url;
1696
+ this.resetReadyState();
1697
+ this.setLoadingState({
1698
+ loading: true,
1699
+ phase: "loading"
1700
+ });
1537
1701
  const finalUrl = this.withInitialOptions(url);
1538
1702
  if (!this.iframeEl) {
1539
1703
  this.render();
@@ -1552,16 +1716,27 @@ var Viewer3D = class {
1552
1716
  this.iframeEl = null;
1553
1717
  this.containerEl = null;
1554
1718
  this.initialized = false;
1719
+ this.rejectReadyWaiters(
1720
+ new Error("Viewer was destroyed before becoming ready")
1721
+ );
1555
1722
  }
1556
1723
  ensureInit() {
1557
- if (!this.initialized) throw new Error("Call viewer.init() before using viewer");
1724
+ if (!this.initialized)
1725
+ throw new Error("Call viewer.init() before using viewer");
1558
1726
  }
1559
1727
  withInitialOptions(url) {
1728
+ var _a;
1560
1729
  try {
1561
1730
  const parsedUrl = new URL(url, window.location.href);
1562
1731
  const initialToolbar = this.normalizeInitialToolbar();
1563
1732
  if (initialToolbar) {
1564
- parsedUrl.searchParams.set("useToolbar", JSON.stringify(initialToolbar));
1733
+ parsedUrl.searchParams.set(
1734
+ "useToolbar",
1735
+ JSON.stringify(initialToolbar)
1736
+ );
1737
+ }
1738
+ if (((_a = this.options.ui) == null ? void 0 : _a.loadingIndicator) === false) {
1739
+ parsedUrl.searchParams.set("loadingIndicator", "false");
1565
1740
  }
1566
1741
  return parsedUrl.toString();
1567
1742
  } catch {
@@ -1575,17 +1750,25 @@ var Viewer3D = class {
1575
1750
  return { [initialToolbar]: true };
1576
1751
  }
1577
1752
  if (Array.isArray(initialToolbar)) {
1578
- return initialToolbar.reduce((result, target) => {
1579
- result[target] = true;
1580
- return result;
1581
- }, {});
1753
+ return initialToolbar.reduce(
1754
+ (result, target) => {
1755
+ result[target] = true;
1756
+ return result;
1757
+ },
1758
+ {}
1759
+ );
1582
1760
  }
1583
- const entries = Object.entries(initialToolbar).filter(([, enabled]) => enabled === true);
1761
+ const entries = Object.entries(initialToolbar).filter(
1762
+ ([, enabled]) => enabled === true
1763
+ );
1584
1764
  if (entries.length === 0) return null;
1585
- return entries.reduce((result, [target]) => {
1586
- result[target] = true;
1587
- return result;
1588
- }, {});
1765
+ return entries.reduce(
1766
+ (result, [target]) => {
1767
+ result[target] = true;
1768
+ return result;
1769
+ },
1770
+ {}
1771
+ );
1589
1772
  }
1590
1773
  // ===== typed internal events used by modules =====
1591
1774
  _on(event, cb) {
@@ -1597,6 +1780,30 @@ var Viewer3D = class {
1597
1780
  _emit(event, payload) {
1598
1781
  this.emitter.emit(event, payload);
1599
1782
  }
1783
+ resetReadyState() {
1784
+ this.viewerReady = false;
1785
+ this.readyPayload = null;
1786
+ }
1787
+ setLoadingState(payload) {
1788
+ this.loadingState = { ...payload };
1789
+ this._emit("loading:change", this.loadingState);
1790
+ }
1791
+ resolveReady(payload) {
1792
+ this.viewerReady = true;
1793
+ this.readyPayload = { ...payload };
1794
+ this.readyWaiters.forEach((waiter) => {
1795
+ if (waiter.timer) clearTimeout(waiter.timer);
1796
+ waiter.resolve(payload);
1797
+ });
1798
+ this.readyWaiters = [];
1799
+ }
1800
+ rejectReadyWaiters(error) {
1801
+ this.readyWaiters.forEach((waiter) => {
1802
+ if (waiter.timer) clearTimeout(waiter.timer);
1803
+ waiter.reject(error);
1804
+ });
1805
+ this.readyWaiters = [];
1806
+ }
1600
1807
  // ===== postMessage bridge =====
1601
1808
  postToViewer(type, payload) {
1602
1809
  var _a;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "3dviewer-sdk",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [