@ericchen1990/pano-view 3.7.0 → 3.9.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.mts CHANGED
@@ -209,7 +209,7 @@ type SphereTileScene = Omit<SphereTileProps, "loadMode" | "visible" | "onReady"
209
209
 
210
210
  type AssetUrlContext = {
211
211
  defaultUrl: string;
212
- kind: "tile" | "preview" | "hotspot";
212
+ kind: "tile" | "preview" | "hotspot" | "map";
213
213
  sceneId: string;
214
214
  hotspotId?: string;
215
215
  };
@@ -531,6 +531,122 @@ type ProjectHotspotDefinition = HotspotDefinition & {
531
531
  };
532
532
  declare function resolveHotspotAssets(hotspot: ProjectHotspotDefinition, baseUrl: string, sceneId: string, resolver?: AssetUrlResolver): ProjectHotspotDefinition;
533
533
 
534
+ /**
535
+ * Shared drag inertia and edge bounce for map pan and scene-menu scrolling.
536
+ * Axis motion is pure; hosts own pointer capture and rAF loops.
537
+ */
538
+ type PanoDragMotion = {
539
+ /** Continue sliding after release from pointer velocity. Defaults to true. */
540
+ inertia?: boolean;
541
+ /** Rubber-band past edges while dragging; spring back on release. Defaults to true. */
542
+ bounce?: boolean;
543
+ };
544
+
545
+ type PanoMapCorner = "top-left" | "top-right" | "bottom-left" | "bottom-right";
546
+ type PanoMapGradientStop = {
547
+ offset: number;
548
+ color: string;
549
+ opacity?: number;
550
+ };
551
+ type PanoMapSectorGradient = {
552
+ type: "radial" | "linear";
553
+ stops: PanoMapGradientStop[];
554
+ };
555
+ type PanoMapSectorDefinition = {
556
+ /** Sector opening angle in degrees. */
557
+ angle: number;
558
+ /** Sector radius in CSS pixels; does not scale with the floor image. */
559
+ radius: number;
560
+ /** When true, sector angle follows the current horizontal FOV. */
561
+ followFov?: boolean;
562
+ /** Solid fill color when `gradient` is omitted. */
563
+ color?: string;
564
+ /**
565
+ * Overall fill opacity in [0, 1].
566
+ * Applies in both solid and gradient modes; gradient stop opacities only
567
+ * describe the relative fade shape and are normalized against this value.
568
+ */
569
+ opacity?: number;
570
+ /** Optional sector outline color. */
571
+ strokeColor?: string;
572
+ /** Outline width in CSS pixels. */
573
+ strokeWidth?: number;
574
+ /** Outline opacity in [0, 1]. Defaults to 1 when stroke is set. */
575
+ strokeOpacity?: number;
576
+ /**
577
+ * Optional fill gradient. When omitted, the sector uses a solid `color`.
578
+ * Stop colors are independent of `color` unless the author keeps them in sync.
579
+ */
580
+ gradient?: PanoMapSectorGradient;
581
+ };
582
+ type PanoMapSpotDefinition = {
583
+ id: string;
584
+ sceneId: string;
585
+ /** Normalized image X in [0, 1], origin at the top-left. */
586
+ x: number;
587
+ /** Normalized image Y in [0, 1], origin at the top-left. */
588
+ y: number;
589
+ /**
590
+ * Clockwise degrees from image up when viewer yaw is 0.
591
+ * Runtime sector rotation is `heading + yaw`.
592
+ */
593
+ heading: number;
594
+ icon?: string;
595
+ };
596
+ type PanoMapControlsDefinition = {
597
+ /** Show expand/collapse on a docked panel. */
598
+ collapse?: boolean;
599
+ /** Show zoom-in and zoom-out buttons. */
600
+ zoom?: boolean;
601
+ };
602
+ type PanoMapDockedPresentation = {
603
+ mode: "docked";
604
+ corner?: PanoMapCorner;
605
+ width?: number;
606
+ height?: number;
607
+ offsetX?: number;
608
+ offsetY?: number;
609
+ };
610
+ type PanoMapDialogPresentation = {
611
+ mode: "dialog";
612
+ corner?: PanoMapCorner;
613
+ offsetX?: number;
614
+ offsetY?: number;
615
+ width?: number;
616
+ height?: number;
617
+ };
618
+ type PanoMapPresentation = PanoMapDockedPresentation | PanoMapDialogPresentation;
619
+ type PanoMapDefinition = {
620
+ image: string;
621
+ /**
622
+ * Pointer interaction with the floor image.
623
+ * - `fixed`: fit-only; no wheel/pan zoom (zoom buttons are also suppressed).
624
+ * - `zoom`: wheel/pan zoom and optional zoom buttons.
625
+ */
626
+ interaction?: "fixed" | "zoom";
627
+ presentation?: PanoMapPresentation;
628
+ controls?: PanoMapControlsDefinition;
629
+ /** Spot icon edge length in CSS pixels. */
630
+ iconSize?: number;
631
+ /**
632
+ * Maximum zoom multiplier relative to the contain-fit scale.
633
+ * Only applies when `interaction` is `"zoom"`. Defaults to 8.
634
+ */
635
+ maxScale?: number;
636
+ /**
637
+ * Pan inertia and edge bounce when `interaction` is `"zoom"`.
638
+ * Omit for both on; set `false` or flag fields to disable.
639
+ */
640
+ dragMotion?: PanoDragMotion | false;
641
+ spots: PanoMapSpotDefinition[];
642
+ sector?: PanoMapSectorDefinition;
643
+ };
644
+ /** Parse an optional project floor plan. Scene IDs must already be collected. */
645
+ declare function parsePanoMap(value: unknown, sceneIds: Set<string>, options?: {
646
+ draft?: boolean;
647
+ }): PanoMapDefinition | undefined;
648
+ declare function resolvePanoMapAssets(map: PanoMapDefinition, baseUrl: string, resolveAssetUrl?: AssetUrlResolver): PanoMapDefinition;
649
+
534
650
  type PanoProjectSceneDefinition = {
535
651
  id: string;
536
652
  title?: string;
@@ -553,6 +669,7 @@ type PanoProjectDefinition = {
553
669
  initialSceneId?: string;
554
670
  scenes: PanoProjectSceneDefinition[];
555
671
  popups?: ProjectPopupDefinition[];
672
+ map?: PanoMapDefinition;
556
673
  };
557
674
  type LoadedProjectScene = {
558
675
  scene: TileScene | SphereTileScene;
@@ -565,6 +682,7 @@ type LoadedPanoProject = {
565
682
  initialSceneId: string;
566
683
  scenes: LoadedProjectScene[];
567
684
  popups?: ProjectPopupDefinition[];
685
+ map?: PanoMapDefinition;
568
686
  };
569
687
  type LoadPanoProjectOptions = JsonRequestOptions & {
570
688
  /** Directory used for project-relative references. Required for object input. */
@@ -578,4 +696,4 @@ declare function parsePanoProject(value: unknown, options?: {
578
696
  }): PanoProjectDefinition;
579
697
  declare function loadPanoProject(source: string | PanoProjectDefinition, options?: LoadPanoProjectOptions): Promise<LoadedPanoProject>;
580
698
 
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 };
699
+ 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 PanoDragMotion, type PanoMapControlsDefinition, type PanoMapCorner, type PanoMapDefinition, type PanoMapDialogPresentation, type PanoMapDockedPresentation, type PanoMapGradientStop, type PanoMapPresentation, type PanoMapSectorDefinition, type PanoMapSectorGradient, type PanoMapSpotDefinition, 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, parsePanoMap, parsePanoProject, projectHotspotTriggers, resolvePanoMapAssets, resolveHotspotAssets as resolveProjectHotspotAssets, resolveProjectPopupAssets, sceneFromViewerConfig, setHotspotAction };
package/dist/data.d.ts CHANGED
@@ -209,7 +209,7 @@ type SphereTileScene = Omit<SphereTileProps, "loadMode" | "visible" | "onReady"
209
209
 
210
210
  type AssetUrlContext = {
211
211
  defaultUrl: string;
212
- kind: "tile" | "preview" | "hotspot";
212
+ kind: "tile" | "preview" | "hotspot" | "map";
213
213
  sceneId: string;
214
214
  hotspotId?: string;
215
215
  };
@@ -531,6 +531,122 @@ type ProjectHotspotDefinition = HotspotDefinition & {
531
531
  };
532
532
  declare function resolveHotspotAssets(hotspot: ProjectHotspotDefinition, baseUrl: string, sceneId: string, resolver?: AssetUrlResolver): ProjectHotspotDefinition;
533
533
 
534
+ /**
535
+ * Shared drag inertia and edge bounce for map pan and scene-menu scrolling.
536
+ * Axis motion is pure; hosts own pointer capture and rAF loops.
537
+ */
538
+ type PanoDragMotion = {
539
+ /** Continue sliding after release from pointer velocity. Defaults to true. */
540
+ inertia?: boolean;
541
+ /** Rubber-band past edges while dragging; spring back on release. Defaults to true. */
542
+ bounce?: boolean;
543
+ };
544
+
545
+ type PanoMapCorner = "top-left" | "top-right" | "bottom-left" | "bottom-right";
546
+ type PanoMapGradientStop = {
547
+ offset: number;
548
+ color: string;
549
+ opacity?: number;
550
+ };
551
+ type PanoMapSectorGradient = {
552
+ type: "radial" | "linear";
553
+ stops: PanoMapGradientStop[];
554
+ };
555
+ type PanoMapSectorDefinition = {
556
+ /** Sector opening angle in degrees. */
557
+ angle: number;
558
+ /** Sector radius in CSS pixels; does not scale with the floor image. */
559
+ radius: number;
560
+ /** When true, sector angle follows the current horizontal FOV. */
561
+ followFov?: boolean;
562
+ /** Solid fill color when `gradient` is omitted. */
563
+ color?: string;
564
+ /**
565
+ * Overall fill opacity in [0, 1].
566
+ * Applies in both solid and gradient modes; gradient stop opacities only
567
+ * describe the relative fade shape and are normalized against this value.
568
+ */
569
+ opacity?: number;
570
+ /** Optional sector outline color. */
571
+ strokeColor?: string;
572
+ /** Outline width in CSS pixels. */
573
+ strokeWidth?: number;
574
+ /** Outline opacity in [0, 1]. Defaults to 1 when stroke is set. */
575
+ strokeOpacity?: number;
576
+ /**
577
+ * Optional fill gradient. When omitted, the sector uses a solid `color`.
578
+ * Stop colors are independent of `color` unless the author keeps them in sync.
579
+ */
580
+ gradient?: PanoMapSectorGradient;
581
+ };
582
+ type PanoMapSpotDefinition = {
583
+ id: string;
584
+ sceneId: string;
585
+ /** Normalized image X in [0, 1], origin at the top-left. */
586
+ x: number;
587
+ /** Normalized image Y in [0, 1], origin at the top-left. */
588
+ y: number;
589
+ /**
590
+ * Clockwise degrees from image up when viewer yaw is 0.
591
+ * Runtime sector rotation is `heading + yaw`.
592
+ */
593
+ heading: number;
594
+ icon?: string;
595
+ };
596
+ type PanoMapControlsDefinition = {
597
+ /** Show expand/collapse on a docked panel. */
598
+ collapse?: boolean;
599
+ /** Show zoom-in and zoom-out buttons. */
600
+ zoom?: boolean;
601
+ };
602
+ type PanoMapDockedPresentation = {
603
+ mode: "docked";
604
+ corner?: PanoMapCorner;
605
+ width?: number;
606
+ height?: number;
607
+ offsetX?: number;
608
+ offsetY?: number;
609
+ };
610
+ type PanoMapDialogPresentation = {
611
+ mode: "dialog";
612
+ corner?: PanoMapCorner;
613
+ offsetX?: number;
614
+ offsetY?: number;
615
+ width?: number;
616
+ height?: number;
617
+ };
618
+ type PanoMapPresentation = PanoMapDockedPresentation | PanoMapDialogPresentation;
619
+ type PanoMapDefinition = {
620
+ image: string;
621
+ /**
622
+ * Pointer interaction with the floor image.
623
+ * - `fixed`: fit-only; no wheel/pan zoom (zoom buttons are also suppressed).
624
+ * - `zoom`: wheel/pan zoom and optional zoom buttons.
625
+ */
626
+ interaction?: "fixed" | "zoom";
627
+ presentation?: PanoMapPresentation;
628
+ controls?: PanoMapControlsDefinition;
629
+ /** Spot icon edge length in CSS pixels. */
630
+ iconSize?: number;
631
+ /**
632
+ * Maximum zoom multiplier relative to the contain-fit scale.
633
+ * Only applies when `interaction` is `"zoom"`. Defaults to 8.
634
+ */
635
+ maxScale?: number;
636
+ /**
637
+ * Pan inertia and edge bounce when `interaction` is `"zoom"`.
638
+ * Omit for both on; set `false` or flag fields to disable.
639
+ */
640
+ dragMotion?: PanoDragMotion | false;
641
+ spots: PanoMapSpotDefinition[];
642
+ sector?: PanoMapSectorDefinition;
643
+ };
644
+ /** Parse an optional project floor plan. Scene IDs must already be collected. */
645
+ declare function parsePanoMap(value: unknown, sceneIds: Set<string>, options?: {
646
+ draft?: boolean;
647
+ }): PanoMapDefinition | undefined;
648
+ declare function resolvePanoMapAssets(map: PanoMapDefinition, baseUrl: string, resolveAssetUrl?: AssetUrlResolver): PanoMapDefinition;
649
+
534
650
  type PanoProjectSceneDefinition = {
535
651
  id: string;
536
652
  title?: string;
@@ -553,6 +669,7 @@ type PanoProjectDefinition = {
553
669
  initialSceneId?: string;
554
670
  scenes: PanoProjectSceneDefinition[];
555
671
  popups?: ProjectPopupDefinition[];
672
+ map?: PanoMapDefinition;
556
673
  };
557
674
  type LoadedProjectScene = {
558
675
  scene: TileScene | SphereTileScene;
@@ -565,6 +682,7 @@ type LoadedPanoProject = {
565
682
  initialSceneId: string;
566
683
  scenes: LoadedProjectScene[];
567
684
  popups?: ProjectPopupDefinition[];
685
+ map?: PanoMapDefinition;
568
686
  };
569
687
  type LoadPanoProjectOptions = JsonRequestOptions & {
570
688
  /** Directory used for project-relative references. Required for object input. */
@@ -578,4 +696,4 @@ declare function parsePanoProject(value: unknown, options?: {
578
696
  }): PanoProjectDefinition;
579
697
  declare function loadPanoProject(source: string | PanoProjectDefinition, options?: LoadPanoProjectOptions): Promise<LoadedPanoProject>;
580
698
 
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 };
699
+ 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 PanoDragMotion, type PanoMapControlsDefinition, type PanoMapCorner, type PanoMapDefinition, type PanoMapDialogPresentation, type PanoMapDockedPresentation, type PanoMapGradientStop, type PanoMapPresentation, type PanoMapSectorDefinition, type PanoMapSectorGradient, type PanoMapSpotDefinition, 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, parsePanoMap, parsePanoProject, projectHotspotTriggers, resolvePanoMapAssets, resolveHotspotAssets as resolveProjectHotspotAssets, resolveProjectPopupAssets, sceneFromViewerConfig, setHotspotAction };
package/dist/data.mjs CHANGED
@@ -809,10 +809,231 @@ function resolveHotspotAssets(hotspot, baseUrl, sceneId, resolver) {
809
809
  return result;
810
810
  }
811
811
 
812
- // src/data/project.ts
812
+ // src/data/map.ts
813
+ var CORNERS = /* @__PURE__ */ new Set([
814
+ "top-left",
815
+ "top-right",
816
+ "bottom-left",
817
+ "bottom-right"
818
+ ]);
819
+ function optionalNumber(record, key, options = {}) {
820
+ const value = record[key];
821
+ if (value === void 0) return void 0;
822
+ if (typeof value !== "number" || !Number.isFinite(value))
823
+ throw new Error(`${key}: expected a finite number`);
824
+ if (options.min !== void 0 && value < options.min)
825
+ throw new Error(`${key}: expected a number >= ${options.min}`);
826
+ if (options.max !== void 0 && value > options.max)
827
+ throw new Error(`${key}: expected a number <= ${options.max}`);
828
+ if (options.exclusiveMin !== void 0 && value <= options.exclusiveMin)
829
+ throw new Error(`${key}: expected a number > ${options.exclusiveMin}`);
830
+ if (options.exclusiveMax !== void 0 && value >= options.exclusiveMax)
831
+ throw new Error(`${key}: expected a number < ${options.exclusiveMax}`);
832
+ return value;
833
+ }
834
+ function requiredNumber(record, key, options = {}) {
835
+ const value = optionalNumber(record, key, options);
836
+ if (value === void 0) throw new Error(`${key}: expected a finite number`);
837
+ return value;
838
+ }
813
839
  function optionalString(record, key) {
814
840
  return record[key] === void 0 ? void 0 : requiredString(record[key], key);
815
841
  }
842
+ function parseGradientStop(value, field) {
843
+ if (!isRecord(value)) throw new Error(`${field}: expected an object`);
844
+ const offset = requiredNumber(value, "offset", { min: 0, max: 1 });
845
+ const color = requiredString(value.color, `${field}.color`);
846
+ const opacity = optionalNumber(value, "opacity", { min: 0, max: 1 });
847
+ return {
848
+ offset,
849
+ color,
850
+ ...opacity === void 0 ? {} : { opacity }
851
+ };
852
+ }
853
+ function parseSector(value, field) {
854
+ if (!isRecord(value)) throw new Error(`${field}: expected an object`);
855
+ const angle = requiredNumber(value, "angle", { exclusiveMin: 0, exclusiveMax: 360 });
856
+ const radius = requiredNumber(value, "radius", { exclusiveMin: 0 });
857
+ const followFov = value.followFov === void 0 ? void 0 : value.followFov === true ? true : value.followFov === false ? false : (() => {
858
+ throw new Error(`${field}.followFov: expected a boolean`);
859
+ })();
860
+ const color = optionalString(value, "color");
861
+ const opacity = optionalNumber(value, "opacity", { min: 0, max: 1 });
862
+ const strokeColor = optionalString(value, "strokeColor");
863
+ const strokeWidth = optionalNumber(value, "strokeWidth", { min: 0 });
864
+ const strokeOpacity = optionalNumber(value, "strokeOpacity", { min: 0, max: 1 });
865
+ let gradient;
866
+ if (value.gradient !== void 0) {
867
+ if (!isRecord(value.gradient))
868
+ throw new Error(`${field}.gradient: expected an object`);
869
+ const type = value.gradient.type;
870
+ if (type !== "radial" && type !== "linear")
871
+ throw new Error(`${field}.gradient.type: expected "radial" or "linear"`);
872
+ if (!Array.isArray(value.gradient.stops) || value.gradient.stops.length < 2)
873
+ throw new Error(`${field}.gradient.stops: expected at least two stops`);
874
+ gradient = {
875
+ type,
876
+ stops: value.gradient.stops.map(
877
+ (stop, index) => parseGradientStop(stop, `${field}.gradient.stops[${index}]`)
878
+ )
879
+ };
880
+ }
881
+ return {
882
+ angle,
883
+ radius,
884
+ ...followFov === void 0 ? {} : { followFov },
885
+ ...color === void 0 ? {} : { color },
886
+ ...opacity === void 0 ? {} : { opacity },
887
+ ...strokeColor === void 0 ? {} : { strokeColor },
888
+ ...strokeWidth === void 0 ? {} : { strokeWidth },
889
+ ...strokeOpacity === void 0 ? {} : { strokeOpacity },
890
+ ...gradient === void 0 ? {} : { gradient }
891
+ };
892
+ }
893
+ function parsePresentation(value, field) {
894
+ if (!isRecord(value)) throw new Error(`${field}: expected an object`);
895
+ const mode = value.mode;
896
+ if (mode !== "docked" && mode !== "dialog")
897
+ throw new Error(`${field}.mode: expected "docked" or "dialog"`);
898
+ const cornerValue = value.corner;
899
+ const corner = cornerValue === void 0 ? void 0 : typeof cornerValue === "string" && CORNERS.has(cornerValue) ? cornerValue : (() => {
900
+ throw new Error(
901
+ `${field}.corner: expected top-left, top-right, bottom-left, or bottom-right`
902
+ );
903
+ })();
904
+ const width = optionalNumber(value, "width", { exclusiveMin: 0 });
905
+ const height = optionalNumber(value, "height", { exclusiveMin: 0 });
906
+ const offsetX = optionalNumber(value, "offsetX", { min: 0 });
907
+ const offsetY = optionalNumber(value, "offsetY", { min: 0 });
908
+ return {
909
+ mode,
910
+ ...corner === void 0 ? {} : { corner },
911
+ ...width === void 0 ? {} : { width },
912
+ ...height === void 0 ? {} : { height },
913
+ ...offsetX === void 0 ? {} : { offsetX },
914
+ ...offsetY === void 0 ? {} : { offsetY }
915
+ };
916
+ }
917
+ function parseOptionalBoolean(value, field) {
918
+ if (value === void 0) return void 0;
919
+ if (value === true) return true;
920
+ if (value === false) return false;
921
+ throw new Error(`${field}: expected a boolean`);
922
+ }
923
+ function parseDragMotion(value, field) {
924
+ if (value === false) return false;
925
+ if (!isRecord(value)) throw new Error(`${field}: expected an object or false`);
926
+ const inertia = parseOptionalBoolean(value.inertia, `${field}.inertia`);
927
+ const bounce = parseOptionalBoolean(value.bounce, `${field}.bounce`);
928
+ const motion = {
929
+ ...inertia === void 0 ? {} : { inertia },
930
+ ...bounce === void 0 ? {} : { bounce }
931
+ };
932
+ return motion;
933
+ }
934
+ function parseControls(value, field) {
935
+ if (!isRecord(value)) throw new Error(`${field}: expected an object`);
936
+ const collapse = value.collapse === void 0 ? void 0 : value.collapse === true ? true : value.collapse === false ? false : (() => {
937
+ throw new Error(`${field}.collapse: expected a boolean`);
938
+ })();
939
+ const zoom = value.zoom === void 0 ? void 0 : value.zoom === true ? true : value.zoom === false ? false : (() => {
940
+ throw new Error(`${field}.zoom: expected a boolean`);
941
+ })();
942
+ return {
943
+ ...collapse === void 0 ? {} : { collapse },
944
+ ...zoom === void 0 ? {} : { zoom }
945
+ };
946
+ }
947
+ function parseSpot(value, field, sceneIds) {
948
+ if (!isRecord(value)) throw new Error(`${field}: expected an object`);
949
+ const id = requiredString(value.id, `${field}.id`);
950
+ const sceneId = requiredString(value.sceneId, `${field}.sceneId`);
951
+ if (!sceneIds.has(sceneId))
952
+ throw new Error(`${field}.sceneId: scene does not exist`);
953
+ const x = requiredNumber(value, "x", { min: 0, max: 1 });
954
+ const y = requiredNumber(value, "y", { min: 0, max: 1 });
955
+ const heading = requiredNumber(value, "heading");
956
+ const icon = optionalString(value, "icon");
957
+ if (icon) resolveResource("https://map.invalid/", icon);
958
+ return {
959
+ id,
960
+ sceneId,
961
+ x,
962
+ y,
963
+ heading,
964
+ ...icon === void 0 ? {} : { icon }
965
+ };
966
+ }
967
+ function parsePanoMap(value, sceneIds, options = {}) {
968
+ if (value === void 0) return void 0;
969
+ try {
970
+ if (!isRecord(value)) throw new Error("map: expected an object");
971
+ if (typeof value.image !== "string" || !options.draft && !value.image.trim())
972
+ throw new Error("map.image: expected a nonempty string");
973
+ const image = value.image;
974
+ if (image.trim()) resolveResource("https://map.invalid/", image);
975
+ const interaction = value.interaction === void 0 ? void 0 : value.interaction === "fixed" || value.interaction === "zoom" ? value.interaction : (() => {
976
+ throw new Error('map.interaction: expected "fixed" or "zoom"');
977
+ })();
978
+ const presentation = value.presentation === void 0 ? void 0 : parsePresentation(value.presentation, "map.presentation");
979
+ const controls = value.controls === void 0 ? void 0 : parseControls(value.controls, "map.controls");
980
+ const iconSize = optionalNumber(value, "iconSize", { exclusiveMin: 0 });
981
+ const maxScale = optionalNumber(value, "maxScale", { min: 1 });
982
+ const dragMotion = value.dragMotion === void 0 ? void 0 : parseDragMotion(value.dragMotion, "map.dragMotion");
983
+ if (!Array.isArray(value.spots)) throw new Error("map.spots: expected an array");
984
+ const spotIds = /* @__PURE__ */ new Set();
985
+ const spots = value.spots.map((spot, index) => {
986
+ const parsed = parseSpot(spot, `map.spots[${index}]`, sceneIds);
987
+ if (spotIds.has(parsed.id))
988
+ throw new Error(`map.spots[${index}].id: duplicate spot ID`);
989
+ spotIds.add(parsed.id);
990
+ return parsed;
991
+ });
992
+ const sector = value.sector === void 0 ? void 0 : parseSector(value.sector, "map.sector");
993
+ return {
994
+ image,
995
+ ...interaction === void 0 ? {} : { interaction },
996
+ ...presentation === void 0 ? {} : { presentation },
997
+ ...controls === void 0 ? {} : { controls },
998
+ ...iconSize === void 0 ? {} : { iconSize },
999
+ ...maxScale === void 0 ? {} : { maxScale },
1000
+ ...dragMotion === void 0 ? {} : { dragMotion },
1001
+ spots,
1002
+ ...sector === void 0 ? {} : { sector }
1003
+ };
1004
+ } catch (cause) {
1005
+ if (cause instanceof PanoDataError) throw cause;
1006
+ const message = cause instanceof Error ? cause.message : "Invalid map";
1007
+ throw new PanoDataError("INVALID_PROJECT", message, {
1008
+ field: message.split(":")[0],
1009
+ cause
1010
+ });
1011
+ }
1012
+ }
1013
+ function resolvePanoMapAssets(map, baseUrl, resolveAssetUrl) {
1014
+ const resolve = (path, spotId) => mapResource(
1015
+ path,
1016
+ {
1017
+ defaultUrl: resolveResource(baseUrl, path),
1018
+ kind: "map",
1019
+ sceneId: spotId ?? "",
1020
+ hotspotId: spotId
1021
+ },
1022
+ resolveAssetUrl
1023
+ );
1024
+ return {
1025
+ ...map,
1026
+ image: resolve(map.image),
1027
+ spots: map.spots.map(
1028
+ (spot) => spot.icon ? { ...spot, icon: resolve(spot.icon, spot.id) } : { ...spot }
1029
+ )
1030
+ };
1031
+ }
1032
+
1033
+ // src/data/project.ts
1034
+ function optionalString2(record, key) {
1035
+ return record[key] === void 0 ? void 0 : requiredString(record[key], key);
1036
+ }
816
1037
  function parsePanoProject(value, options = {}) {
817
1038
  if (!isRecord(value))
818
1039
  throw new PanoDataError("INVALID_PROJECT", "Expected a project object");
@@ -843,7 +1064,7 @@ function parsePanoProject(value, options = {}) {
843
1064
  throw new Error(
844
1065
  "source: provide exactly one of viewerUrl or viewerConfig"
845
1066
  );
846
- const baseUrl = optionalString(src, "baseUrl");
1067
+ const baseUrl = optionalString2(src, "baseUrl");
847
1068
  const source = src.viewerUrl !== void 0 ? {
848
1069
  viewerUrl: requiredString(src.viewerUrl, "source.viewerUrl"),
849
1070
  baseUrl
@@ -883,7 +1104,7 @@ function parsePanoProject(value, options = {}) {
883
1104
  );
884
1105
  return {
885
1106
  id,
886
- title: optionalString(entry, "title"),
1107
+ title: optionalString2(entry, "title"),
887
1108
  source,
888
1109
  initialView,
889
1110
  ...viewLimits === void 0 ? {} : { viewLimits },
@@ -904,7 +1125,7 @@ function parsePanoProject(value, options = {}) {
904
1125
  }
905
1126
  }
906
1127
  );
907
- const initialSceneId = optionalString(value, "initialSceneId") ?? scenes[0]?.id;
1128
+ const initialSceneId = optionalString2(value, "initialSceneId") ?? scenes[0]?.id;
908
1129
  if (initialSceneId !== void 0 && !ids.has(initialSceneId))
909
1130
  throw new PanoDataError(
910
1131
  "INVALID_REFERENCE",
@@ -932,12 +1153,14 @@ function parsePanoProject(value, options = {}) {
932
1153
  }
933
1154
  }
934
1155
  }
1156
+ const map = parsePanoMap(value.map, ids, options);
935
1157
  return {
936
1158
  schemaVersion: 1,
937
- title: optionalString(value, "title"),
1159
+ title: optionalString2(value, "title"),
938
1160
  initialSceneId,
939
1161
  scenes,
940
- ...popups === void 0 ? {} : { popups }
1162
+ ...popups === void 0 ? {} : { popups },
1163
+ ...map === void 0 ? {} : { map }
941
1164
  };
942
1165
  } catch (cause) {
943
1166
  if (cause instanceof PanoDataError) throw cause;
@@ -1036,7 +1259,14 @@ async function loadPanoProject(source, options = {}) {
1036
1259
  title: definition.title,
1037
1260
  initialSceneId: definition.initialSceneId,
1038
1261
  scenes,
1039
- ...definition.popups === void 0 ? {} : { popups: definition.popups.map((popup) => resolveProjectPopupAssets(popup, root, options.resolvePopupAssetUrl)) }
1262
+ ...definition.popups === void 0 ? {} : { popups: definition.popups.map((popup) => resolveProjectPopupAssets(popup, root, options.resolvePopupAssetUrl)) },
1263
+ ...definition.map === void 0 ? {} : {
1264
+ map: resolvePanoMapAssets(
1265
+ definition.map,
1266
+ root,
1267
+ options.resolveAssetUrl
1268
+ )
1269
+ }
1040
1270
  };
1041
1271
  } finally {
1042
1272
  signal?.removeEventListener("abort", cancel);
@@ -1047,8 +1277,10 @@ export {
1047
1277
  hotspotAction,
1048
1278
  loadPanoProject,
1049
1279
  loadSceneFromJson,
1280
+ parsePanoMap,
1050
1281
  parsePanoProject,
1051
1282
  projectHotspotTriggers,
1283
+ resolvePanoMapAssets,
1052
1284
  resolveHotspotAssets as resolveProjectHotspotAssets,
1053
1285
  resolveProjectPopupAssets,
1054
1286
  sceneFromViewerConfig,