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.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);
@@ -958,7 +1037,11 @@ var ModelTreeModule = class {
958
1037
  }
959
1038
  selectNodes(nodeIds) {
960
1039
  const normalizedNodeIds = this.normalizeNodeIds(nodeIds);
961
- console.log("[3dviewer-sdk] modelTree.selectNodes", { nodeIds, normalizedNodeIds });
1040
+ console.log("[3dviewer-sdk] modelTree.selectNodes post message", {
1041
+ nodeIds,
1042
+ normalizedNodeIds,
1043
+ willClearSelection: normalizedNodeIds.length === 0
1044
+ });
962
1045
  this.postTreeSelectNode({
963
1046
  nodeIds: normalizedNodeIds
964
1047
  });
@@ -1237,12 +1320,50 @@ var Viewer3D = class {
1237
1320
  this.containerEl = null;
1238
1321
  this.iframeEl = null;
1239
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
+ };
1240
1330
  this.emitter = new Emitter();
1241
1331
  this.handleMessage = (event) => {
1242
- var _a, _b, _c, _d, _e;
1332
+ var _a, _b, _c;
1243
1333
  const data = event.data;
1244
1334
  if (!data || typeof data !== "object") return;
1245
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
+ }
1246
1367
  case "viewer-home-click" /* HOME_CLICK */:
1247
1368
  this._emit("camera:home", { timestamp: Date.now() });
1248
1369
  break;
@@ -1257,9 +1378,6 @@ var Viewer3D = class {
1257
1378
  });
1258
1379
  break;
1259
1380
  }
1260
- case "viewer-node-select" /* NODE_SELECT */:
1261
- this._emit("node:select", { nodeId: String((_b = (_a = data.payload) == null ? void 0 : _a.nodeId) != null ? _b : ""), timestamp: Date.now() });
1262
- break;
1263
1381
  case "viewer-node-selection-change" /* NODE_SELECTION_CHANGE */: {
1264
1382
  const payload = data.payload;
1265
1383
  const rawNodeIds = payload == null ? void 0 : payload.nodeIds;
@@ -1271,7 +1389,9 @@ var Viewer3D = class {
1271
1389
  break;
1272
1390
  }
1273
1391
  case "viewer-pan-change" /* PAN_CHANGE */:
1274
- 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
+ });
1275
1395
  break;
1276
1396
  case "viewer-pdf-plan-mode" /* PDF_PLAN_MODE */: {
1277
1397
  const payload = data.payload;
@@ -1329,7 +1449,8 @@ var Viewer3D = class {
1329
1449
  }
1330
1450
  case "viewer-tree-node-ids" /* TREE_NODE_IDS */: {
1331
1451
  const payload = data.payload;
1332
- if (!payload || !payload.requestId || !Array.isArray(payload.nodeIds)) break;
1452
+ if (!payload || !payload.requestId || !Array.isArray(payload.nodeIds))
1453
+ break;
1333
1454
  this._emit("modelTree:node-ids", {
1334
1455
  requestId: String(payload.requestId),
1335
1456
  nodeIds: payload.nodeIds.map(String),
@@ -1339,7 +1460,8 @@ var Viewer3D = class {
1339
1460
  }
1340
1461
  case "viewer-tree-nodes" /* TREE_NODES */: {
1341
1462
  const payload = data.payload;
1342
- if (!payload || !payload.requestId || !Array.isArray(payload.nodes)) break;
1463
+ if (!payload || !payload.requestId || !Array.isArray(payload.nodes))
1464
+ break;
1343
1465
  this._emit("modelTree:nodes", {
1344
1466
  requestId: String(payload.requestId),
1345
1467
  nodes: payload.nodes.filter((node) => node && typeof node === "object").map((node) => {
@@ -1365,7 +1487,8 @@ var Viewer3D = class {
1365
1487
  }
1366
1488
  case "viewer-sheets-list" /* SHEETS_LIST */: {
1367
1489
  const payload = data.payload;
1368
- if (!payload || !payload.requestId || !Array.isArray(payload.sheets)) break;
1490
+ if (!payload || !payload.requestId || !Array.isArray(payload.sheets))
1491
+ break;
1369
1492
  this._emit("sheets:list", {
1370
1493
  requestId: String(payload.requestId),
1371
1494
  sheets: payload.sheets.map((sheet) => {
@@ -1377,17 +1500,18 @@ var Viewer3D = class {
1377
1500
  viewId: sheet.viewId ? String(sheet.viewId) : void 0
1378
1501
  };
1379
1502
  }),
1380
- activeSheetId: (_d = payload.activeSheetId) != null ? _d : null,
1503
+ activeSheetId: (_b = payload.activeSheetId) != null ? _b : null,
1381
1504
  timestamp: Number(payload.timestamp) || Date.now()
1382
1505
  });
1383
1506
  break;
1384
1507
  }
1385
1508
  case "viewer-object-properties-list" /* OBJECT_PROPERTIES_LIST */: {
1386
1509
  const payload = data.payload;
1387
- if (!payload || !payload.requestId || !Array.isArray(payload.properties)) break;
1510
+ if (!payload || !payload.requestId || !Array.isArray(payload.properties))
1511
+ break;
1388
1512
  this._emit("object-properties:list", {
1389
1513
  requestId: String(payload.requestId),
1390
- selectionKey: String((_e = payload.selectionKey) != null ? _e : ""),
1514
+ selectionKey: String((_c = payload.selectionKey) != null ? _c : ""),
1391
1515
  nodeIds: Array.isArray(payload.nodeIds) ? payload.nodeIds.map(String) : [],
1392
1516
  persistentIds: Array.isArray(payload.persistentIds) ? payload.persistentIds.map(String) : [],
1393
1517
  properties: payload.properties.filter((item) => item && typeof item === "object").map((item) => ({ ...item })),
@@ -1397,7 +1521,8 @@ var Viewer3D = class {
1397
1521
  }
1398
1522
  case "viewer-linked-objects-list" /* LINKED_OBJECTS_LIST */: {
1399
1523
  const payload = data.payload;
1400
- if (!payload || !payload.requestId || !Array.isArray(payload.linkedObjects)) break;
1524
+ if (!payload || !payload.requestId || !Array.isArray(payload.linkedObjects))
1525
+ break;
1401
1526
  this._emit("linked-objects:list", {
1402
1527
  requestId: String(payload.requestId),
1403
1528
  linkedObjects: payload.linkedObjects.filter((item) => item && typeof item === "object").map((item) => ({ ...item })),
@@ -1407,11 +1532,12 @@ var Viewer3D = class {
1407
1532
  }
1408
1533
  case "viewer-states-objects-list" /* STATES_OBJECTS_LIST */: {
1409
1534
  const payload = data.payload;
1410
- if (!payload || !payload.requestId || !Array.isArray(payload.statesObjects)) break;
1535
+ if (!payload || !payload.requestId || !Array.isArray(payload.statesObjects))
1536
+ break;
1411
1537
  this._emit("states-objects:list", {
1412
1538
  requestId: String(payload.requestId),
1413
1539
  statesObjects: payload.statesObjects.map((item) => {
1414
- var _a2, _b2, _c2, _d2, _e2, _f, _g;
1540
+ var _a2, _b2, _c2, _d, _e, _f, _g;
1415
1541
  return {
1416
1542
  id: String((_a2 = item.id) != null ? _a2 : ""),
1417
1543
  name: String((_b2 = item.name) != null ? _b2 : ""),
@@ -1424,7 +1550,7 @@ var Viewer3D = class {
1424
1550
  };
1425
1551
  }) : [],
1426
1552
  states: {
1427
- 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 : ""),
1428
1554
  type: String((_g = (_f = item.states) == null ? void 0 : _f.type) != null ? _g : "")
1429
1555
  }
1430
1556
  };
@@ -1435,7 +1561,8 @@ var Viewer3D = class {
1435
1561
  }
1436
1562
  case "viewer-markup-list" /* MARKUP_LIST */: {
1437
1563
  const payload = data.payload;
1438
- if (!payload || !payload.requestId || !Array.isArray(payload.markups)) break;
1564
+ if (!payload || !payload.requestId || !Array.isArray(payload.markups))
1565
+ break;
1439
1566
  this._emit("markup:list", {
1440
1567
  requestId: String(payload.requestId),
1441
1568
  markups: payload.markups.map((markup) => {
@@ -1504,6 +1631,36 @@ var Viewer3D = class {
1504
1631
  var _a;
1505
1632
  return (_a = this.options.url) != null ? _a : null;
1506
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
+ }
1507
1664
  // ===== lifecycle =====
1508
1665
  init() {
1509
1666
  if (this.initialized) return;
@@ -1515,7 +1672,12 @@ var Viewer3D = class {
1515
1672
  async render(file) {
1516
1673
  this.ensureInit();
1517
1674
  if (this.iframeEl) return;
1675
+ this.resetReadyState();
1518
1676
  if (!this.options.url) return this.files.render(file);
1677
+ this.setLoadingState({
1678
+ loading: true,
1679
+ phase: "loading"
1680
+ });
1519
1681
  const iframe = document.createElement("iframe");
1520
1682
  iframe.src = this.withInitialOptions(this.options.url);
1521
1683
  iframe.style.border = "none";
@@ -1523,13 +1685,19 @@ var Viewer3D = class {
1523
1685
  iframe.style.height = this.options.height || "100%";
1524
1686
  iframe.width = this.options.width || "100%";
1525
1687
  iframe.height = this.options.height || "100%";
1526
- if (this.options.sandbox) iframe.setAttribute("sandbox", this.options.sandbox);
1688
+ if (this.options.sandbox)
1689
+ iframe.setAttribute("sandbox", this.options.sandbox);
1527
1690
  this.containerEl.appendChild(iframe);
1528
1691
  this.iframeEl = iframe;
1529
1692
  }
1530
1693
  open(url) {
1531
1694
  this.ensureInit();
1532
1695
  this.options.url = url;
1696
+ this.resetReadyState();
1697
+ this.setLoadingState({
1698
+ loading: true,
1699
+ phase: "loading"
1700
+ });
1533
1701
  const finalUrl = this.withInitialOptions(url);
1534
1702
  if (!this.iframeEl) {
1535
1703
  this.render();
@@ -1548,16 +1716,27 @@ var Viewer3D = class {
1548
1716
  this.iframeEl = null;
1549
1717
  this.containerEl = null;
1550
1718
  this.initialized = false;
1719
+ this.rejectReadyWaiters(
1720
+ new Error("Viewer was destroyed before becoming ready")
1721
+ );
1551
1722
  }
1552
1723
  ensureInit() {
1553
- 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");
1554
1726
  }
1555
1727
  withInitialOptions(url) {
1728
+ var _a;
1556
1729
  try {
1557
1730
  const parsedUrl = new URL(url, window.location.href);
1558
1731
  const initialToolbar = this.normalizeInitialToolbar();
1559
1732
  if (initialToolbar) {
1560
- 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");
1561
1740
  }
1562
1741
  return parsedUrl.toString();
1563
1742
  } catch {
@@ -1571,17 +1750,25 @@ var Viewer3D = class {
1571
1750
  return { [initialToolbar]: true };
1572
1751
  }
1573
1752
  if (Array.isArray(initialToolbar)) {
1574
- return initialToolbar.reduce((result, target) => {
1575
- result[target] = true;
1576
- return result;
1577
- }, {});
1753
+ return initialToolbar.reduce(
1754
+ (result, target) => {
1755
+ result[target] = true;
1756
+ return result;
1757
+ },
1758
+ {}
1759
+ );
1578
1760
  }
1579
- const entries = Object.entries(initialToolbar).filter(([, enabled]) => enabled === true);
1761
+ const entries = Object.entries(initialToolbar).filter(
1762
+ ([, enabled]) => enabled === true
1763
+ );
1580
1764
  if (entries.length === 0) return null;
1581
- return entries.reduce((result, [target]) => {
1582
- result[target] = true;
1583
- return result;
1584
- }, {});
1765
+ return entries.reduce(
1766
+ (result, [target]) => {
1767
+ result[target] = true;
1768
+ return result;
1769
+ },
1770
+ {}
1771
+ );
1585
1772
  }
1586
1773
  // ===== typed internal events used by modules =====
1587
1774
  _on(event, cb) {
@@ -1593,6 +1780,30 @@ var Viewer3D = class {
1593
1780
  _emit(event, payload) {
1594
1781
  this.emitter.emit(event, payload);
1595
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
+ }
1596
1807
  // ===== postMessage bridge =====
1597
1808
  postToViewer(type, payload) {
1598
1809
  var _a;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "3dviewer-sdk",
3
- "version": "1.0.28",
3
+ "version": "1.1.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [