@ericchen1990/pano-view 3.5.0 → 3.6.0

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/data.d.ts CHANGED
@@ -45,6 +45,22 @@ type ViewerDocument = ViewerConfig & {
45
45
  schemaVersion: 1;
46
46
  };
47
47
 
48
+ /** Camera-center limits in degrees. A yaw min greater than max crosses ±180°. */
49
+ type PanoViewLimits = {
50
+ yaw?: {
51
+ min: number;
52
+ max: number;
53
+ };
54
+ pitch?: {
55
+ min: number;
56
+ max: number;
57
+ };
58
+ fov?: {
59
+ min: number;
60
+ max: number;
61
+ };
62
+ };
63
+
48
64
  type CubeFaceCode = "f" | "r" | "b" | "l" | "u" | "d";
49
65
  type TileAddress = {
50
66
  face: CubeFaceCode;
@@ -179,11 +195,14 @@ type SphereTileProps = {
179
195
  onLevelChange?: (level: number) => void;
180
196
  };
181
197
 
182
- type TileScene = Omit<TileProps, "loadMode" | "visible" | "onReady" | "onPreviewError" | "onLoadProgress" | "onTileError" | "onLevelChange"> & {
198
+ type SceneViewOptions = {
199
+ viewLimits?: PanoViewLimits;
200
+ };
201
+ type TileScene = Omit<TileProps, "loadMode" | "visible" | "onReady" | "onPreviewError" | "onLoadProgress" | "onTileError" | "onLevelChange"> & SceneViewOptions & {
183
202
  id: string;
184
203
  type: "tile";
185
204
  };
186
- type SphereTileScene = Omit<SphereTileProps, "loadMode" | "visible" | "onReady" | "onPreviewError" | "onLoadProgress" | "onTileError" | "onLevelChange"> & {
205
+ type SphereTileScene = Omit<SphereTileProps, "loadMode" | "visible" | "onReady" | "onPreviewError" | "onLoadProgress" | "onTileError" | "onLevelChange"> & SceneViewOptions & {
187
206
  id: string;
188
207
  type: "sphereTile";
189
208
  };
@@ -209,6 +228,37 @@ type LoadSceneFromJsonOptions = Omit<SceneFromViewerConfigOptions, "baseUrl"> &
209
228
  declare function sceneFromViewerConfig(value: unknown, options: SceneFromViewerConfigOptions): TileScene | SphereTileScene;
210
229
  declare function loadSceneFromJson(url: string, options: LoadSceneFromJsonOptions): Promise<TileScene | SphereTileScene>;
211
230
 
231
+ type ProjectPopupBlock = {
232
+ id: string;
233
+ } & ({
234
+ type: "text";
235
+ text: string;
236
+ } | {
237
+ type: "image";
238
+ src: string;
239
+ alt?: string;
240
+ } | {
241
+ type: "video";
242
+ src: string;
243
+ poster?: string;
244
+ } | {
245
+ type: "audio";
246
+ src: string;
247
+ });
248
+ type ProjectPopupDefinition = {
249
+ id: string;
250
+ title: string;
251
+ blocks: ProjectPopupBlock[];
252
+ };
253
+ type PopupAssetUrlContext = {
254
+ defaultUrl: string;
255
+ popupId: string;
256
+ blockId: string;
257
+ field: "src" | "poster";
258
+ };
259
+ type PopupAssetUrlResolver = (path: string, context: PopupAssetUrlContext) => string;
260
+ declare function resolveProjectPopupAssets(popup: ProjectPopupDefinition, baseUrl: string, resolver?: PopupAssetUrlResolver): ProjectPopupDefinition;
261
+
212
262
  type HotspotPosition = {
213
263
  /** Horizontal panorama angle in degrees. Positive values look right. */
214
264
  yaw: number;
@@ -398,6 +448,7 @@ type VideoHotspotDefinition = PointHotspotDefinitionBase & {
398
448
  type AudioHotspotDefinition = PointHotspotDefinitionBase & {
399
449
  type: "audio";
400
450
  src: string | string[];
451
+ format?: string | string[];
401
452
  playing?: boolean;
402
453
  loop?: boolean;
403
454
  muted?: boolean;
@@ -449,12 +500,36 @@ type PointHotspotDefinition = ImageHotspotDefinition | GraphicHotspotDefinition
449
500
  /** A serializable, discriminated hotspot collection item. */
450
501
  type HotspotDefinition = PointHotspotDefinition | PolygonHotspotDefinition | PolylineHotspotDefinition;
451
502
 
503
+ type JsonValue = null | boolean | number | string | JsonValue[] | JsonObject;
504
+ type JsonObject = {
505
+ [key: string]: JsonValue;
506
+ };
507
+ declare const projectHotspotTriggers: readonly ["click", "pointerenter", "pointerleave"];
508
+ type ProjectHotspotTrigger = typeof projectHotspotTriggers[number];
509
+ type ProjectCustomHotspotAction = {
510
+ type: "custom";
511
+ actionId: string;
512
+ params?: JsonObject;
513
+ };
514
+ declare function hotspotAction(hotspot: ProjectHotspotDefinition, trigger: ProjectHotspotTrigger): ProjectHotspotAction | undefined;
515
+ declare function setHotspotAction(hotspot: ProjectHotspotDefinition, trigger: ProjectHotspotTrigger, action?: ProjectHotspotAction): ProjectHotspotDefinition;
516
+ type ProjectHotspotAction = ProjectCustomHotspotAction | {
517
+ type: "scene";
518
+ sceneId: string;
519
+ } | {
520
+ type: "view";
521
+ view: PanoViewerState;
522
+ duration?: number;
523
+ } | {
524
+ type: "popup";
525
+ popupId: string;
526
+ };
452
527
  type ProjectHotspotDefinition = HotspotDefinition & {
453
- action?: {
454
- type: "scene";
455
- sceneId: string;
456
- };
528
+ /** Legacy click fallback. events.click takes precedence. */
529
+ action?: ProjectHotspotAction;
530
+ events?: Partial<Record<ProjectHotspotTrigger, ProjectHotspotAction>>;
457
531
  };
532
+ declare function resolveHotspotAssets(hotspot: ProjectHotspotDefinition, baseUrl: string, sceneId: string, resolver?: AssetUrlResolver): ProjectHotspotDefinition;
458
533
 
459
534
  type PanoProjectSceneDefinition = {
460
535
  id: string;
@@ -469,6 +544,7 @@ type PanoProjectSceneDefinition = {
469
544
  baseUrl?: string;
470
545
  };
471
546
  initialView?: Partial<PanoViewerState>;
547
+ viewLimits?: PanoViewLimits;
472
548
  hotspots?: ProjectHotspotDefinition[];
473
549
  };
474
550
  type PanoProjectDefinition = {
@@ -476,6 +552,7 @@ type PanoProjectDefinition = {
476
552
  title?: string;
477
553
  initialSceneId?: string;
478
554
  scenes: PanoProjectSceneDefinition[];
555
+ popups?: ProjectPopupDefinition[];
479
556
  };
480
557
  type LoadedProjectScene = {
481
558
  scene: TileScene | SphereTileScene;
@@ -487,13 +564,18 @@ type LoadedPanoProject = {
487
564
  title?: string;
488
565
  initialSceneId: string;
489
566
  scenes: LoadedProjectScene[];
567
+ popups?: ProjectPopupDefinition[];
490
568
  };
491
569
  type LoadPanoProjectOptions = JsonRequestOptions & {
492
570
  /** Directory used for project-relative references. Required for object input. */
493
571
  baseUrl?: string;
494
572
  resolveAssetUrl?: AssetUrlResolver;
573
+ resolvePopupAssetUrl?: PopupAssetUrlResolver;
495
574
  };
496
- declare function parsePanoProject(value: unknown): PanoProjectDefinition;
575
+ /** Drafts permit empty scenes and unfinished popup titles/media; loading and export always validate strictly. */
576
+ declare function parsePanoProject(value: unknown, options?: {
577
+ draft?: boolean;
578
+ }): PanoProjectDefinition;
497
579
  declare function loadPanoProject(source: string | PanoProjectDefinition, options?: LoadPanoProjectOptions): Promise<LoadedPanoProject>;
498
580
 
499
- export { type AssetUrlContext, type AssetUrlResolver, type CubemapViewerConfig, type JsonRequestOptions, type LoadPanoProjectOptions, type LoadSceneFromJsonOptions, type LoadedPanoProject, type LoadedProjectScene, PanoDataError, type PanoDataErrorCode, type PanoProjectDefinition, type PanoProjectSceneDefinition, type ProjectHotspotDefinition, type SceneFromViewerConfigOptions, type SphereViewerConfig, type ViewerConfig, type ViewerDocument, loadPanoProject, loadSceneFromJson, parsePanoProject, sceneFromViewerConfig };
581
+ export { type AssetUrlContext, type AssetUrlResolver, type CubemapViewerConfig, type JsonObject, type JsonRequestOptions, type JsonValue, type LoadPanoProjectOptions, type LoadSceneFromJsonOptions, type LoadedPanoProject, type LoadedProjectScene, PanoDataError, type PanoDataErrorCode, type PanoProjectDefinition, type PanoProjectSceneDefinition, type PanoViewLimits, type PopupAssetUrlContext, type PopupAssetUrlResolver, type ProjectCustomHotspotAction, type ProjectHotspotAction, type ProjectHotspotDefinition, type ProjectHotspotTrigger, type ProjectPopupBlock, type ProjectPopupDefinition, type SceneFromViewerConfigOptions, type SphereViewerConfig, type ViewerConfig, type ViewerDocument, hotspotAction, loadPanoProject, loadSceneFromJson, parsePanoProject, projectHotspotTriggers, resolveHotspotAssets as resolveProjectHotspotAssets, resolveProjectPopupAssets, sceneFromViewerConfig, setHotspotAction };
package/dist/data.mjs CHANGED
@@ -271,7 +271,152 @@ async function loadSceneFromJson(url, options) {
271
271
  });
272
272
  }
273
273
 
274
+ // src/view-limits.ts
275
+ function parseViewLimits(value) {
276
+ if (value === void 0) return void 0;
277
+ if (!value || typeof value !== "object" || Array.isArray(value))
278
+ throw new Error("viewLimits: expected an object");
279
+ const result = {};
280
+ for (const axis of ["yaw", "pitch", "fov"]) {
281
+ const input = value[axis];
282
+ if (input === void 0) continue;
283
+ if (!input || typeof input !== "object" || Array.isArray(input))
284
+ throw new Error(`viewLimits.${axis}: expected min and max`);
285
+ const { min, max } = input;
286
+ const lower = axis === "yaw" ? -180 : axis === "pitch" ? -90 : 1;
287
+ const upper = axis === "yaw" ? 180 : axis === "pitch" ? 90 : 179;
288
+ if (typeof min !== "number" || typeof max !== "number" || !Number.isFinite(min) || !Number.isFinite(max) || min < lower || min > upper || max < lower || max > upper)
289
+ throw new Error(`viewLimits.${axis}: expected finite min and max from ${lower} to ${upper}`);
290
+ if (axis !== "yaw" && min > max)
291
+ throw new Error(`viewLimits.${axis}: min must not exceed max`);
292
+ result[axis] = { min, max };
293
+ }
294
+ return result;
295
+ }
296
+
297
+ // src/data/popups.ts
298
+ function parsePopups(value, draft = false) {
299
+ if (value === void 0) return void 0;
300
+ try {
301
+ if (!Array.isArray(value)) throw new Error("popups: expected an array");
302
+ const ids = /* @__PURE__ */ new Set();
303
+ const string = (input, field, allowEmpty = false) => {
304
+ if (typeof input !== "string" || !allowEmpty && !input.trim())
305
+ throw new Error(`${field}: expected a ${allowEmpty ? "string" : "nonempty string"}`);
306
+ return input;
307
+ };
308
+ return value.map((entry, index) => {
309
+ const field = `popups[${index}]`;
310
+ if (!isRecord(entry)) throw new Error(`${field}: expected an object`);
311
+ const id = requiredString(entry.id, `${field}.id`);
312
+ if (ids.has(id)) throw new Error(`${field}.id: duplicate popup ID`);
313
+ ids.add(id);
314
+ const title = string(entry.title, `${field}.title`, draft);
315
+ if (!Array.isArray(entry.blocks)) throw new Error(`${field}.blocks: expected an array`);
316
+ const blockIds = /* @__PURE__ */ new Set();
317
+ const blocks = entry.blocks.map((block, i) => {
318
+ const path = `${field}.blocks[${i}]`;
319
+ if (!isRecord(block)) throw new Error(`${path}: expected an object`);
320
+ const id2 = requiredString(block.id, `${path}.id`);
321
+ if (blockIds.has(id2)) throw new Error(`${path}.id: duplicate block ID`);
322
+ blockIds.add(id2);
323
+ if (block.type === "text") return { id: id2, type: "text", text: string(block.text, `${path}.text`, true) };
324
+ const src = string(block.src, `${path}.src`, draft);
325
+ if (src) resolveResource("https://popup.invalid/", src);
326
+ if (block.type === "image") return {
327
+ id: id2,
328
+ type: "image",
329
+ src,
330
+ ...block.alt === void 0 ? {} : { alt: string(block.alt, `${path}.alt`, true) }
331
+ };
332
+ if (block.type === "audio") return { id: id2, type: "audio", src };
333
+ if (block.type === "video") {
334
+ const poster = block.poster === void 0 ? void 0 : string(block.poster, `${path}.poster`, true);
335
+ if (poster) resolveResource("https://popup.invalid/", poster);
336
+ return { id: id2, type: "video", src, ...poster ? { poster } : {} };
337
+ }
338
+ throw new Error(`${path}.type: unsupported content block`);
339
+ });
340
+ return { id, title, blocks };
341
+ });
342
+ } catch (cause) {
343
+ const message = cause instanceof Error ? cause.message : "Invalid popup content";
344
+ throw new PanoDataError("INVALID_PROJECT", message, { field: message.split(":")[0], cause });
345
+ }
346
+ }
347
+ function resolveProjectPopupAssets(popup, baseUrl, resolver) {
348
+ return { ...popup, blocks: popup.blocks.map((block) => {
349
+ if (block.type === "text") return { ...block };
350
+ const resolve = (path, field) => {
351
+ if (!path) throw new PanoDataError("INVALID_PROJECT", `${popup.id}/${block.id}.${field}: choose a resource`);
352
+ const context = { defaultUrl: resolveResource(baseUrl, path), popupId: popup.id, blockId: block.id, field };
353
+ const result = resolver ? resolver(path, context) : context.defaultUrl;
354
+ if (typeof result !== "string" || !result.startsWith("/") && !/^(https?:|blob:|data:)/i.test(result))
355
+ throw new PanoDataError("INVALID_CONFIG", "resolvePopupAssetUrl must return an absolute URL or a site-root path");
356
+ return result;
357
+ };
358
+ return { ...block, src: resolve(block.src, "src"), ...block.type === "video" && block.poster ? { poster: resolve(block.poster, "poster") } : {} };
359
+ }) };
360
+ }
361
+
362
+ // ../../shared/cursor-assets.ts
363
+ function mapCursorAssets(cursor, resolve) {
364
+ return cursor.replace(
365
+ /url\(\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|((?:\\.|[^)\\])*?))\s*\)/gi,
366
+ (_match, double, single, bare) => {
367
+ const path = (double ?? single ?? bare ?? "").replace(
368
+ /\\([\da-f]{1,6}\s?|.)/gi,
369
+ (_escape, value) => /^[\da-f]/i.test(value) ? String.fromCodePoint(
370
+ Math.min(parseInt(value.trim(), 16), 1114111)
371
+ ) : value
372
+ );
373
+ return `url(${JSON.stringify(resolve(path))})`;
374
+ }
375
+ );
376
+ }
377
+
274
378
  // src/data/hotspots.ts
379
+ var projectHotspotTriggers = ["click", "pointerenter", "pointerleave"];
380
+ function hotspotAction(hotspot, trigger) {
381
+ return hotspot.events?.[trigger] ?? (trigger === "click" ? hotspot.action : void 0);
382
+ }
383
+ function setHotspotAction(hotspot, trigger, action) {
384
+ const events = { ...hotspot.events };
385
+ const next = { ...hotspot, events };
386
+ if (trigger === "click") delete next.action;
387
+ if (action) events[trigger] = action;
388
+ else delete events[trigger];
389
+ if (!Object.keys(events).length) delete next.events;
390
+ return next;
391
+ }
392
+ function jsonValue(value, seen = /* @__PURE__ */ new Set()) {
393
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
394
+ if (typeof value === "number" && Number.isFinite(value)) return value;
395
+ if (typeof value !== "object" || !value || seen.has(value)) throw new Error("params: expected JSON data");
396
+ if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new Error("params: expected JSON data");
397
+ seen.add(value);
398
+ const result = Array.isArray(value) ? value.map((item) => jsonValue(item, seen)) : Object.fromEntries(Object.entries(value).map(([key, item]) => [key, jsonValue(item, seen)]));
399
+ seen.delete(value);
400
+ return result;
401
+ }
402
+ function parseAction(action) {
403
+ if (!isRecord(action)) throw new Error("action: expected an object");
404
+ if (action.type === "scene") return { type: "scene", sceneId: requiredString(action.sceneId, "action.sceneId") };
405
+ if (action.type === "popup") return { type: "popup", popupId: requiredString(action.popupId, "action.popupId") };
406
+ if (action.type === "custom") {
407
+ if (action.params !== void 0 && (!isRecord(action.params) || Array.isArray(action.params))) throw new Error("action.params: expected an object");
408
+ return { type: "custom", actionId: requiredString(action.actionId, "action.actionId"), ...action.params === void 0 ? {} : { params: jsonValue(action.params) } };
409
+ }
410
+ if (action.type === "view") {
411
+ const position = parsePosition(action.view, "action.view");
412
+ const fov = finite(action.view.fov, "action.view.fov");
413
+ if (fov <= 0 || fov >= 180) throw new Error("action.view.fov: expected 0 < fov < 180");
414
+ const duration = action.duration === void 0 ? void 0 : finite(action.duration, "action.duration");
415
+ if (duration !== void 0 && duration < 0) throw new Error("action.duration: expected a nonnegative number");
416
+ return { type: "view", view: { ...position, fov }, ...duration === void 0 ? {} : { duration } };
417
+ }
418
+ throw new Error("action.type: unsupported action");
419
+ }
275
420
  var common = [
276
421
  "label",
277
422
  "visible",
@@ -314,6 +459,7 @@ var fields = {
314
459
  ],
315
460
  audio: [
316
461
  "src",
462
+ "format",
317
463
  "playing",
318
464
  "loop",
319
465
  "muted",
@@ -582,9 +728,10 @@ function parseHotspot(value, sceneId, field) {
582
728
  result[key] = n;
583
729
  } else if (key === "fontWeight" && typeof item === "number")
584
730
  result[key] = finite(item, key);
585
- else if (key === "src" && kind === "audio" && Array.isArray(item)) {
586
- if (!item.length) throw new Error("src: expected audio sources");
587
- result[key] = item.map((src, i) => requiredString(src, `src[${i}]`));
731
+ else if ((key === "src" || key === "format") && kind === "audio" && Array.isArray(item)) {
732
+ if (!item.length)
733
+ throw new Error(`${key}: expected audio ${key === "src" ? "sources" : "formats"}`);
734
+ result[key] = item.map((entry, i) => requiredString(entry, `${key}[${i}]`));
588
735
  } else {
589
736
  if (typeof item !== "string")
590
737
  throw new Error(`${key}: expected a string`);
@@ -603,13 +750,19 @@ function parseHotspot(value, sceneId, field) {
603
750
  throw new Error("text: required");
604
751
  if (kind === "sequence" && !result.frameCount)
605
752
  throw new Error("frameCount: required");
606
- if (value.action !== void 0) {
607
- if (!isRecord(value.action) || value.action.type !== "scene")
608
- throw new Error("action: expected a scene action");
609
- result.action = {
610
- type: "scene",
611
- sceneId: requiredString(value.action.sceneId, "action.sceneId")
612
- };
753
+ if (value.action !== void 0) result.action = parseAction(value.action);
754
+ if (value.events !== void 0) {
755
+ if (!isRecord(value.events)) throw new Error("events: expected an object");
756
+ const events = {};
757
+ for (const key of Object.keys(value.events)) {
758
+ if (!projectHotspotTriggers.includes(key)) throw new Error(`events.${key}: unsupported trigger`);
759
+ try {
760
+ events[key] = parseAction(value.events[key]);
761
+ } catch (error) {
762
+ throw new Error(`events.${key}.${error instanceof Error ? error.message : "invalid action"}`);
763
+ }
764
+ }
765
+ result.events = events;
613
766
  }
614
767
  return result;
615
768
  } catch (cause) {
@@ -633,6 +786,11 @@ function resolveHotspotAssets(hotspot, baseUrl, sceneId, resolver) {
633
786
  resolver
634
787
  ) : path;
635
788
  const result = { ...hotspot };
789
+ if (result.cursor) result.cursor = mapCursorAssets(result.cursor, resolve);
790
+ if (result.tooltipAppearance?.background) result.tooltipAppearance = {
791
+ ...result.tooltipAppearance,
792
+ background: mapCursorAssets(result.tooltipAppearance.background, resolve)
793
+ };
636
794
  if ("src" in result)
637
795
  result.src = Array.isArray(result.src) ? result.src.map(resolve) : resolve(result.src);
638
796
  if ("poster" in result && result.poster)
@@ -655,7 +813,7 @@ function resolveHotspotAssets(hotspot, baseUrl, sceneId, resolver) {
655
813
  function optionalString(record, key) {
656
814
  return record[key] === void 0 ? void 0 : requiredString(record[key], key);
657
815
  }
658
- function parsePanoProject(value) {
816
+ function parsePanoProject(value, options = {}) {
659
817
  if (!isRecord(value))
660
818
  throw new PanoDataError("INVALID_PROJECT", "Expected a project object");
661
819
  if (value.schemaVersion !== 1)
@@ -665,7 +823,8 @@ function parsePanoProject(value) {
665
823
  { field: "schemaVersion" }
666
824
  );
667
825
  try {
668
- if (!Array.isArray(value.scenes) || !value.scenes.length)
826
+ const popups = parsePopups(value.popups, options.draft);
827
+ if (!Array.isArray(value.scenes) || !options.draft && !value.scenes.length)
669
828
  throw new Error("scenes: expected a nonempty array");
670
829
  const ids = /* @__PURE__ */ new Set();
671
830
  const scenes = value.scenes.map(
@@ -689,6 +848,7 @@ function parsePanoProject(value) {
689
848
  viewerUrl: requiredString(src.viewerUrl, "source.viewerUrl"),
690
849
  baseUrl
691
850
  } : { viewerConfig: parseConfig(src.viewerConfig), baseUrl };
851
+ const viewLimits = parseViewLimits(entry.viewLimits);
692
852
  let initialView;
693
853
  if (entry.initialView !== void 0) {
694
854
  if (!isRecord(entry.initialView))
@@ -726,6 +886,7 @@ function parsePanoProject(value) {
726
886
  title: optionalString(entry, "title"),
727
887
  source,
728
888
  initialView,
889
+ ...viewLimits === void 0 ? {} : { viewLimits },
729
890
  hotspots
730
891
  };
731
892
  } catch (cause) {
@@ -743,8 +904,8 @@ function parsePanoProject(value) {
743
904
  }
744
905
  }
745
906
  );
746
- const initialSceneId = optionalString(value, "initialSceneId") ?? scenes[0].id;
747
- if (!ids.has(initialSceneId))
907
+ const initialSceneId = optionalString(value, "initialSceneId") ?? scenes[0]?.id;
908
+ if (initialSceneId !== void 0 && !ids.has(initialSceneId))
748
909
  throw new PanoDataError(
749
910
  "INVALID_REFERENCE",
750
911
  "initialSceneId: scene does not exist",
@@ -752,22 +913,31 @@ function parsePanoProject(value) {
752
913
  );
753
914
  for (const [index, scene] of scenes.entries()) {
754
915
  for (const [i, hotspot] of (scene.hotspots ?? []).entries()) {
755
- if (hotspot.action && !ids.has(hotspot.action.sceneId))
756
- throw new PanoDataError(
757
- "INVALID_REFERENCE",
758
- "Hotspot target scene does not exist",
759
- {
916
+ for (const trigger of projectHotspotTriggers) {
917
+ const action = hotspotAction(hotspot, trigger);
918
+ if (action?.type === "popup" && !popups?.some((popup) => popup.id === action.popupId))
919
+ throw new PanoDataError("INVALID_REFERENCE", "Hotspot target popup does not exist", {
760
920
  sceneId: scene.id,
761
- field: `scenes[${index}].hotspots[${i}].action.sceneId`
762
- }
763
- );
921
+ field: `scenes[${index}].hotspots[${i}].${hotspot.events?.[trigger] ? `events.${trigger}` : "action"}.popupId`
922
+ });
923
+ if (action?.type === "scene" && !ids.has(action.sceneId))
924
+ throw new PanoDataError(
925
+ "INVALID_REFERENCE",
926
+ "Hotspot target scene does not exist",
927
+ {
928
+ sceneId: scene.id,
929
+ field: `scenes[${index}].hotspots[${i}].${hotspot.events?.[trigger] ? `events.${trigger}` : "action"}.sceneId`
930
+ }
931
+ );
932
+ }
764
933
  }
765
934
  }
766
935
  return {
767
936
  schemaVersion: 1,
768
937
  title: optionalString(value, "title"),
769
938
  initialSceneId,
770
- scenes
939
+ scenes,
940
+ ...popups === void 0 ? {} : { popups }
771
941
  };
772
942
  } catch (cause) {
773
943
  if (cause instanceof PanoDataError) throw cause;
@@ -828,6 +998,7 @@ async function loadPanoProject(source, options = {}) {
828
998
  baseUrl: sourceBase ?? root
829
999
  });
830
1000
  aborted(controller.signal);
1001
+ if (item.viewLimits !== void 0) scene.viewLimits = item.viewLimits;
831
1002
  scenes[index] = {
832
1003
  scene,
833
1004
  title: item.title,
@@ -864,7 +1035,8 @@ async function loadPanoProject(source, options = {}) {
864
1035
  return {
865
1036
  title: definition.title,
866
1037
  initialSceneId: definition.initialSceneId,
867
- scenes
1038
+ scenes,
1039
+ ...definition.popups === void 0 ? {} : { popups: definition.popups.map((popup) => resolveProjectPopupAssets(popup, root, options.resolvePopupAssetUrl)) }
868
1040
  };
869
1041
  } finally {
870
1042
  signal?.removeEventListener("abort", cancel);
@@ -872,9 +1044,14 @@ async function loadPanoProject(source, options = {}) {
872
1044
  }
873
1045
  export {
874
1046
  PanoDataError,
1047
+ hotspotAction,
875
1048
  loadPanoProject,
876
1049
  loadSceneFromJson,
877
1050
  parsePanoProject,
878
- sceneFromViewerConfig
1051
+ projectHotspotTriggers,
1052
+ resolveHotspotAssets as resolveProjectHotspotAssets,
1053
+ resolveProjectPopupAssets,
1054
+ sceneFromViewerConfig,
1055
+ setHotspotAction
879
1056
  };
880
1057
  //# sourceMappingURL=data.mjs.map