@ericchen1990/pano-view 3.6.7 → 3.8.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
@@ -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,106 @@ type ProjectHotspotDefinition = HotspotDefinition & {
531
531
  };
532
532
  declare function resolveHotspotAssets(hotspot: ProjectHotspotDefinition, baseUrl: string, sceneId: string, resolver?: AssetUrlResolver): ProjectHotspotDefinition;
533
533
 
534
+ type PanoMapCorner = "top-left" | "top-right" | "bottom-left" | "bottom-right";
535
+ type PanoMapGradientStop = {
536
+ offset: number;
537
+ color: string;
538
+ opacity?: number;
539
+ };
540
+ type PanoMapSectorGradient = {
541
+ type: "radial" | "linear";
542
+ stops: PanoMapGradientStop[];
543
+ };
544
+ type PanoMapSectorDefinition = {
545
+ /** Sector opening angle in degrees. */
546
+ angle: number;
547
+ /** Sector radius in CSS pixels; does not scale with the floor image. */
548
+ radius: number;
549
+ /** When true, sector angle follows the current horizontal FOV. */
550
+ followFov?: boolean;
551
+ /** Solid fill color when `gradient` is omitted. */
552
+ color?: string;
553
+ /**
554
+ * Overall fill opacity in [0, 1].
555
+ * Applies in both solid and gradient modes; gradient stop opacities only
556
+ * describe the relative fade shape and are normalized against this value.
557
+ */
558
+ opacity?: number;
559
+ /** Optional sector outline color. */
560
+ strokeColor?: string;
561
+ /** Outline width in CSS pixels. */
562
+ strokeWidth?: number;
563
+ /** Outline opacity in [0, 1]. Defaults to 1 when stroke is set. */
564
+ strokeOpacity?: number;
565
+ /**
566
+ * Optional fill gradient. When omitted, the sector uses a solid `color`.
567
+ * Stop colors are independent of `color` unless the author keeps them in sync.
568
+ */
569
+ gradient?: PanoMapSectorGradient;
570
+ };
571
+ type PanoMapSpotDefinition = {
572
+ id: string;
573
+ sceneId: string;
574
+ /** Normalized image X in [0, 1], origin at the top-left. */
575
+ x: number;
576
+ /** Normalized image Y in [0, 1], origin at the top-left. */
577
+ y: number;
578
+ /**
579
+ * Clockwise degrees from image up when viewer yaw is 0.
580
+ * Runtime sector rotation is `heading + yaw`.
581
+ */
582
+ heading: number;
583
+ icon?: string;
584
+ };
585
+ type PanoMapControlsDefinition = {
586
+ /** Show expand/collapse on a docked panel. */
587
+ collapse?: boolean;
588
+ /** Show zoom-in and zoom-out buttons. */
589
+ zoom?: boolean;
590
+ };
591
+ type PanoMapDockedPresentation = {
592
+ mode: "docked";
593
+ corner?: PanoMapCorner;
594
+ width?: number;
595
+ height?: number;
596
+ offsetX?: number;
597
+ offsetY?: number;
598
+ };
599
+ type PanoMapDialogPresentation = {
600
+ mode: "dialog";
601
+ corner?: PanoMapCorner;
602
+ offsetX?: number;
603
+ offsetY?: number;
604
+ width?: number;
605
+ height?: number;
606
+ };
607
+ type PanoMapPresentation = PanoMapDockedPresentation | PanoMapDialogPresentation;
608
+ type PanoMapDefinition = {
609
+ image: string;
610
+ /**
611
+ * Pointer interaction with the floor image.
612
+ * - `fixed`: fit-only; no wheel/pan zoom (zoom buttons are also suppressed).
613
+ * - `zoom`: wheel/pan zoom and optional zoom buttons.
614
+ */
615
+ interaction?: "fixed" | "zoom";
616
+ presentation?: PanoMapPresentation;
617
+ controls?: PanoMapControlsDefinition;
618
+ /** Spot icon edge length in CSS pixels. */
619
+ iconSize?: number;
620
+ /**
621
+ * Maximum zoom multiplier relative to the contain-fit scale.
622
+ * Only applies when `interaction` is `"zoom"`. Defaults to 8.
623
+ */
624
+ maxScale?: number;
625
+ spots: PanoMapSpotDefinition[];
626
+ sector?: PanoMapSectorDefinition;
627
+ };
628
+ /** Parse an optional project floor plan. Scene IDs must already be collected. */
629
+ declare function parsePanoMap(value: unknown, sceneIds: Set<string>, options?: {
630
+ draft?: boolean;
631
+ }): PanoMapDefinition | undefined;
632
+ declare function resolvePanoMapAssets(map: PanoMapDefinition, baseUrl: string, resolveAssetUrl?: AssetUrlResolver): PanoMapDefinition;
633
+
534
634
  type PanoProjectSceneDefinition = {
535
635
  id: string;
536
636
  title?: string;
@@ -553,6 +653,7 @@ type PanoProjectDefinition = {
553
653
  initialSceneId?: string;
554
654
  scenes: PanoProjectSceneDefinition[];
555
655
  popups?: ProjectPopupDefinition[];
656
+ map?: PanoMapDefinition;
556
657
  };
557
658
  type LoadedProjectScene = {
558
659
  scene: TileScene | SphereTileScene;
@@ -565,6 +666,7 @@ type LoadedPanoProject = {
565
666
  initialSceneId: string;
566
667
  scenes: LoadedProjectScene[];
567
668
  popups?: ProjectPopupDefinition[];
669
+ map?: PanoMapDefinition;
568
670
  };
569
671
  type LoadPanoProjectOptions = JsonRequestOptions & {
570
672
  /** Directory used for project-relative references. Required for object input. */
@@ -578,4 +680,4 @@ declare function parsePanoProject(value: unknown, options?: {
578
680
  }): PanoProjectDefinition;
579
681
  declare function loadPanoProject(source: string | PanoProjectDefinition, options?: LoadPanoProjectOptions): Promise<LoadedPanoProject>;
580
682
 
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 };
683
+ 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 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,212 @@ 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 parseControls(value, field) {
918
+ if (!isRecord(value)) throw new Error(`${field}: expected an object`);
919
+ const collapse = value.collapse === void 0 ? void 0 : value.collapse === true ? true : value.collapse === false ? false : (() => {
920
+ throw new Error(`${field}.collapse: expected a boolean`);
921
+ })();
922
+ const zoom = value.zoom === void 0 ? void 0 : value.zoom === true ? true : value.zoom === false ? false : (() => {
923
+ throw new Error(`${field}.zoom: expected a boolean`);
924
+ })();
925
+ return {
926
+ ...collapse === void 0 ? {} : { collapse },
927
+ ...zoom === void 0 ? {} : { zoom }
928
+ };
929
+ }
930
+ function parseSpot(value, field, sceneIds) {
931
+ if (!isRecord(value)) throw new Error(`${field}: expected an object`);
932
+ const id = requiredString(value.id, `${field}.id`);
933
+ const sceneId = requiredString(value.sceneId, `${field}.sceneId`);
934
+ if (!sceneIds.has(sceneId))
935
+ throw new Error(`${field}.sceneId: scene does not exist`);
936
+ const x = requiredNumber(value, "x", { min: 0, max: 1 });
937
+ const y = requiredNumber(value, "y", { min: 0, max: 1 });
938
+ const heading = requiredNumber(value, "heading");
939
+ const icon = optionalString(value, "icon");
940
+ if (icon) resolveResource("https://map.invalid/", icon);
941
+ return {
942
+ id,
943
+ sceneId,
944
+ x,
945
+ y,
946
+ heading,
947
+ ...icon === void 0 ? {} : { icon }
948
+ };
949
+ }
950
+ function parsePanoMap(value, sceneIds, options = {}) {
951
+ if (value === void 0) return void 0;
952
+ try {
953
+ if (!isRecord(value)) throw new Error("map: expected an object");
954
+ if (typeof value.image !== "string" || !options.draft && !value.image.trim())
955
+ throw new Error("map.image: expected a nonempty string");
956
+ const image = value.image;
957
+ if (image.trim()) resolveResource("https://map.invalid/", image);
958
+ const interaction = value.interaction === void 0 ? void 0 : value.interaction === "fixed" || value.interaction === "zoom" ? value.interaction : (() => {
959
+ throw new Error('map.interaction: expected "fixed" or "zoom"');
960
+ })();
961
+ const presentation = value.presentation === void 0 ? void 0 : parsePresentation(value.presentation, "map.presentation");
962
+ const controls = value.controls === void 0 ? void 0 : parseControls(value.controls, "map.controls");
963
+ const iconSize = optionalNumber(value, "iconSize", { exclusiveMin: 0 });
964
+ const maxScale = optionalNumber(value, "maxScale", { min: 1 });
965
+ if (!Array.isArray(value.spots)) throw new Error("map.spots: expected an array");
966
+ const spotIds = /* @__PURE__ */ new Set();
967
+ const spots = value.spots.map((spot, index) => {
968
+ const parsed = parseSpot(spot, `map.spots[${index}]`, sceneIds);
969
+ if (spotIds.has(parsed.id))
970
+ throw new Error(`map.spots[${index}].id: duplicate spot ID`);
971
+ spotIds.add(parsed.id);
972
+ return parsed;
973
+ });
974
+ const sector = value.sector === void 0 ? void 0 : parseSector(value.sector, "map.sector");
975
+ return {
976
+ image,
977
+ ...interaction === void 0 ? {} : { interaction },
978
+ ...presentation === void 0 ? {} : { presentation },
979
+ ...controls === void 0 ? {} : { controls },
980
+ ...iconSize === void 0 ? {} : { iconSize },
981
+ ...maxScale === void 0 ? {} : { maxScale },
982
+ spots,
983
+ ...sector === void 0 ? {} : { sector }
984
+ };
985
+ } catch (cause) {
986
+ if (cause instanceof PanoDataError) throw cause;
987
+ const message = cause instanceof Error ? cause.message : "Invalid map";
988
+ throw new PanoDataError("INVALID_PROJECT", message, {
989
+ field: message.split(":")[0],
990
+ cause
991
+ });
992
+ }
993
+ }
994
+ function resolvePanoMapAssets(map, baseUrl, resolveAssetUrl) {
995
+ const resolve = (path, spotId) => mapResource(
996
+ path,
997
+ {
998
+ defaultUrl: resolveResource(baseUrl, path),
999
+ kind: "map",
1000
+ sceneId: spotId ?? "",
1001
+ hotspotId: spotId
1002
+ },
1003
+ resolveAssetUrl
1004
+ );
1005
+ return {
1006
+ ...map,
1007
+ image: resolve(map.image),
1008
+ spots: map.spots.map(
1009
+ (spot) => spot.icon ? { ...spot, icon: resolve(spot.icon, spot.id) } : { ...spot }
1010
+ )
1011
+ };
1012
+ }
1013
+
1014
+ // src/data/project.ts
1015
+ function optionalString2(record, key) {
1016
+ return record[key] === void 0 ? void 0 : requiredString(record[key], key);
1017
+ }
816
1018
  function parsePanoProject(value, options = {}) {
817
1019
  if (!isRecord(value))
818
1020
  throw new PanoDataError("INVALID_PROJECT", "Expected a project object");
@@ -843,7 +1045,7 @@ function parsePanoProject(value, options = {}) {
843
1045
  throw new Error(
844
1046
  "source: provide exactly one of viewerUrl or viewerConfig"
845
1047
  );
846
- const baseUrl = optionalString(src, "baseUrl");
1048
+ const baseUrl = optionalString2(src, "baseUrl");
847
1049
  const source = src.viewerUrl !== void 0 ? {
848
1050
  viewerUrl: requiredString(src.viewerUrl, "source.viewerUrl"),
849
1051
  baseUrl
@@ -883,7 +1085,7 @@ function parsePanoProject(value, options = {}) {
883
1085
  );
884
1086
  return {
885
1087
  id,
886
- title: optionalString(entry, "title"),
1088
+ title: optionalString2(entry, "title"),
887
1089
  source,
888
1090
  initialView,
889
1091
  ...viewLimits === void 0 ? {} : { viewLimits },
@@ -904,7 +1106,7 @@ function parsePanoProject(value, options = {}) {
904
1106
  }
905
1107
  }
906
1108
  );
907
- const initialSceneId = optionalString(value, "initialSceneId") ?? scenes[0]?.id;
1109
+ const initialSceneId = optionalString2(value, "initialSceneId") ?? scenes[0]?.id;
908
1110
  if (initialSceneId !== void 0 && !ids.has(initialSceneId))
909
1111
  throw new PanoDataError(
910
1112
  "INVALID_REFERENCE",
@@ -932,12 +1134,14 @@ function parsePanoProject(value, options = {}) {
932
1134
  }
933
1135
  }
934
1136
  }
1137
+ const map = parsePanoMap(value.map, ids, options);
935
1138
  return {
936
1139
  schemaVersion: 1,
937
- title: optionalString(value, "title"),
1140
+ title: optionalString2(value, "title"),
938
1141
  initialSceneId,
939
1142
  scenes,
940
- ...popups === void 0 ? {} : { popups }
1143
+ ...popups === void 0 ? {} : { popups },
1144
+ ...map === void 0 ? {} : { map }
941
1145
  };
942
1146
  } catch (cause) {
943
1147
  if (cause instanceof PanoDataError) throw cause;
@@ -1036,7 +1240,14 @@ async function loadPanoProject(source, options = {}) {
1036
1240
  title: definition.title,
1037
1241
  initialSceneId: definition.initialSceneId,
1038
1242
  scenes,
1039
- ...definition.popups === void 0 ? {} : { popups: definition.popups.map((popup) => resolveProjectPopupAssets(popup, root, options.resolvePopupAssetUrl)) }
1243
+ ...definition.popups === void 0 ? {} : { popups: definition.popups.map((popup) => resolveProjectPopupAssets(popup, root, options.resolvePopupAssetUrl)) },
1244
+ ...definition.map === void 0 ? {} : {
1245
+ map: resolvePanoMapAssets(
1246
+ definition.map,
1247
+ root,
1248
+ options.resolveAssetUrl
1249
+ )
1250
+ }
1040
1251
  };
1041
1252
  } finally {
1042
1253
  signal?.removeEventListener("abort", cancel);
@@ -1047,8 +1258,10 @@ export {
1047
1258
  hotspotAction,
1048
1259
  loadPanoProject,
1049
1260
  loadSceneFromJson,
1261
+ parsePanoMap,
1050
1262
  parsePanoProject,
1051
1263
  projectHotspotTriggers,
1264
+ resolvePanoMapAssets,
1052
1265
  resolveHotspotAssets as resolveProjectHotspotAssets,
1053
1266
  resolveProjectPopupAssets,
1054
1267
  sceneFromViewerConfig,