@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/README.md CHANGED
@@ -677,6 +677,12 @@ function FullscreenExample() {
677
677
  }
678
678
  ```
679
679
 
680
+ iPhone Safari does not implement the HTML5 Fullscreen API. When `enterFullscreen()` or MobileVR runs on an iPhone, the viewer covers the visual viewport and scrolls the page in landscape so Safari’s URL and toolbar can collapse. If the bars stay visible, an on-screen prompt asks the user to swipe. Host pages should set:
681
+
682
+ ```html
683
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
684
+ ```
685
+
680
686
  ### Imperative scene switching
681
687
 
682
688
  Use `usePanoViewer()` to shorten parent-side commands to `viewer.setScene("roof")`. Keep using controlled `activeSceneId` when the host UI needs to render the current scene id reactively. For uncontrolled mode, `defaultActiveSceneId` still lets imperative calls update the internal state directly.
package/README.zh-CN.md CHANGED
@@ -650,6 +650,12 @@ function FullscreenExample() {
650
650
  }
651
651
  ```
652
652
 
653
+ iPhone Safari 不支持 HTML5 Fullscreen API。在 iPhone 上调用 `enterFullscreen()` 或进入 MobileVR 时,查看器会铺满 visual viewport,并在横屏滚动页面,以便收起 Safari 的地址栏和工具栏。若栏仍可见,界面会提示用户滑动。宿主页面应设置:
654
+
655
+ ```html
656
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
657
+ ```
658
+
653
659
  ### 命令式切换场景
654
660
 
655
661
  使用 `usePanoViewer()` 可将父组件侧命令缩短为 `viewer.setScene("roof")`。当宿主 UI 需要响应式显示当前场景 id 时,仍建议继续使用受控 `activeSceneId`。若使用非受控模式,`defaultActiveSceneId` 依然允许命令式调用直接更新内部状态。
package/dist/data.cjs CHANGED
@@ -24,8 +24,10 @@ __export(data_exports, {
24
24
  hotspotAction: () => hotspotAction,
25
25
  loadPanoProject: () => loadPanoProject,
26
26
  loadSceneFromJson: () => loadSceneFromJson,
27
+ parsePanoMap: () => parsePanoMap,
27
28
  parsePanoProject: () => parsePanoProject,
28
29
  projectHotspotTriggers: () => projectHotspotTriggers,
30
+ resolvePanoMapAssets: () => resolvePanoMapAssets,
29
31
  resolveProjectHotspotAssets: () => resolveHotspotAssets,
30
32
  resolveProjectPopupAssets: () => resolveProjectPopupAssets,
31
33
  sceneFromViewerConfig: () => sceneFromViewerConfig,
@@ -844,10 +846,212 @@ function resolveHotspotAssets(hotspot, baseUrl, sceneId, resolver) {
844
846
  return result;
845
847
  }
846
848
 
847
- // src/data/project.ts
849
+ // src/data/map.ts
850
+ var CORNERS = /* @__PURE__ */ new Set([
851
+ "top-left",
852
+ "top-right",
853
+ "bottom-left",
854
+ "bottom-right"
855
+ ]);
856
+ function optionalNumber(record, key, options = {}) {
857
+ const value = record[key];
858
+ if (value === void 0) return void 0;
859
+ if (typeof value !== "number" || !Number.isFinite(value))
860
+ throw new Error(`${key}: expected a finite number`);
861
+ if (options.min !== void 0 && value < options.min)
862
+ throw new Error(`${key}: expected a number >= ${options.min}`);
863
+ if (options.max !== void 0 && value > options.max)
864
+ throw new Error(`${key}: expected a number <= ${options.max}`);
865
+ if (options.exclusiveMin !== void 0 && value <= options.exclusiveMin)
866
+ throw new Error(`${key}: expected a number > ${options.exclusiveMin}`);
867
+ if (options.exclusiveMax !== void 0 && value >= options.exclusiveMax)
868
+ throw new Error(`${key}: expected a number < ${options.exclusiveMax}`);
869
+ return value;
870
+ }
871
+ function requiredNumber(record, key, options = {}) {
872
+ const value = optionalNumber(record, key, options);
873
+ if (value === void 0) throw new Error(`${key}: expected a finite number`);
874
+ return value;
875
+ }
848
876
  function optionalString(record, key) {
849
877
  return record[key] === void 0 ? void 0 : requiredString(record[key], key);
850
878
  }
879
+ function parseGradientStop(value, field) {
880
+ if (!isRecord(value)) throw new Error(`${field}: expected an object`);
881
+ const offset = requiredNumber(value, "offset", { min: 0, max: 1 });
882
+ const color = requiredString(value.color, `${field}.color`);
883
+ const opacity = optionalNumber(value, "opacity", { min: 0, max: 1 });
884
+ return {
885
+ offset,
886
+ color,
887
+ ...opacity === void 0 ? {} : { opacity }
888
+ };
889
+ }
890
+ function parseSector(value, field) {
891
+ if (!isRecord(value)) throw new Error(`${field}: expected an object`);
892
+ const angle = requiredNumber(value, "angle", { exclusiveMin: 0, exclusiveMax: 360 });
893
+ const radius = requiredNumber(value, "radius", { exclusiveMin: 0 });
894
+ const followFov = value.followFov === void 0 ? void 0 : value.followFov === true ? true : value.followFov === false ? false : (() => {
895
+ throw new Error(`${field}.followFov: expected a boolean`);
896
+ })();
897
+ const color = optionalString(value, "color");
898
+ const opacity = optionalNumber(value, "opacity", { min: 0, max: 1 });
899
+ const strokeColor = optionalString(value, "strokeColor");
900
+ const strokeWidth = optionalNumber(value, "strokeWidth", { min: 0 });
901
+ const strokeOpacity = optionalNumber(value, "strokeOpacity", { min: 0, max: 1 });
902
+ let gradient;
903
+ if (value.gradient !== void 0) {
904
+ if (!isRecord(value.gradient))
905
+ throw new Error(`${field}.gradient: expected an object`);
906
+ const type = value.gradient.type;
907
+ if (type !== "radial" && type !== "linear")
908
+ throw new Error(`${field}.gradient.type: expected "radial" or "linear"`);
909
+ if (!Array.isArray(value.gradient.stops) || value.gradient.stops.length < 2)
910
+ throw new Error(`${field}.gradient.stops: expected at least two stops`);
911
+ gradient = {
912
+ type,
913
+ stops: value.gradient.stops.map(
914
+ (stop, index) => parseGradientStop(stop, `${field}.gradient.stops[${index}]`)
915
+ )
916
+ };
917
+ }
918
+ return {
919
+ angle,
920
+ radius,
921
+ ...followFov === void 0 ? {} : { followFov },
922
+ ...color === void 0 ? {} : { color },
923
+ ...opacity === void 0 ? {} : { opacity },
924
+ ...strokeColor === void 0 ? {} : { strokeColor },
925
+ ...strokeWidth === void 0 ? {} : { strokeWidth },
926
+ ...strokeOpacity === void 0 ? {} : { strokeOpacity },
927
+ ...gradient === void 0 ? {} : { gradient }
928
+ };
929
+ }
930
+ function parsePresentation(value, field) {
931
+ if (!isRecord(value)) throw new Error(`${field}: expected an object`);
932
+ const mode = value.mode;
933
+ if (mode !== "docked" && mode !== "dialog")
934
+ throw new Error(`${field}.mode: expected "docked" or "dialog"`);
935
+ const cornerValue = value.corner;
936
+ const corner = cornerValue === void 0 ? void 0 : typeof cornerValue === "string" && CORNERS.has(cornerValue) ? cornerValue : (() => {
937
+ throw new Error(
938
+ `${field}.corner: expected top-left, top-right, bottom-left, or bottom-right`
939
+ );
940
+ })();
941
+ const width = optionalNumber(value, "width", { exclusiveMin: 0 });
942
+ const height = optionalNumber(value, "height", { exclusiveMin: 0 });
943
+ const offsetX = optionalNumber(value, "offsetX", { min: 0 });
944
+ const offsetY = optionalNumber(value, "offsetY", { min: 0 });
945
+ return {
946
+ mode,
947
+ ...corner === void 0 ? {} : { corner },
948
+ ...width === void 0 ? {} : { width },
949
+ ...height === void 0 ? {} : { height },
950
+ ...offsetX === void 0 ? {} : { offsetX },
951
+ ...offsetY === void 0 ? {} : { offsetY }
952
+ };
953
+ }
954
+ function parseControls(value, field) {
955
+ if (!isRecord(value)) throw new Error(`${field}: expected an object`);
956
+ const collapse = value.collapse === void 0 ? void 0 : value.collapse === true ? true : value.collapse === false ? false : (() => {
957
+ throw new Error(`${field}.collapse: expected a boolean`);
958
+ })();
959
+ const zoom = value.zoom === void 0 ? void 0 : value.zoom === true ? true : value.zoom === false ? false : (() => {
960
+ throw new Error(`${field}.zoom: expected a boolean`);
961
+ })();
962
+ return {
963
+ ...collapse === void 0 ? {} : { collapse },
964
+ ...zoom === void 0 ? {} : { zoom }
965
+ };
966
+ }
967
+ function parseSpot(value, field, sceneIds) {
968
+ if (!isRecord(value)) throw new Error(`${field}: expected an object`);
969
+ const id = requiredString(value.id, `${field}.id`);
970
+ const sceneId = requiredString(value.sceneId, `${field}.sceneId`);
971
+ if (!sceneIds.has(sceneId))
972
+ throw new Error(`${field}.sceneId: scene does not exist`);
973
+ const x = requiredNumber(value, "x", { min: 0, max: 1 });
974
+ const y = requiredNumber(value, "y", { min: 0, max: 1 });
975
+ const heading = requiredNumber(value, "heading");
976
+ const icon = optionalString(value, "icon");
977
+ if (icon) resolveResource("https://map.invalid/", icon);
978
+ return {
979
+ id,
980
+ sceneId,
981
+ x,
982
+ y,
983
+ heading,
984
+ ...icon === void 0 ? {} : { icon }
985
+ };
986
+ }
987
+ function parsePanoMap(value, sceneIds, options = {}) {
988
+ if (value === void 0) return void 0;
989
+ try {
990
+ if (!isRecord(value)) throw new Error("map: expected an object");
991
+ if (typeof value.image !== "string" || !options.draft && !value.image.trim())
992
+ throw new Error("map.image: expected a nonempty string");
993
+ const image = value.image;
994
+ if (image.trim()) resolveResource("https://map.invalid/", image);
995
+ const interaction = value.interaction === void 0 ? void 0 : value.interaction === "fixed" || value.interaction === "zoom" ? value.interaction : (() => {
996
+ throw new Error('map.interaction: expected "fixed" or "zoom"');
997
+ })();
998
+ const presentation = value.presentation === void 0 ? void 0 : parsePresentation(value.presentation, "map.presentation");
999
+ const controls = value.controls === void 0 ? void 0 : parseControls(value.controls, "map.controls");
1000
+ const iconSize = optionalNumber(value, "iconSize", { exclusiveMin: 0 });
1001
+ const maxScale = optionalNumber(value, "maxScale", { min: 1 });
1002
+ if (!Array.isArray(value.spots)) throw new Error("map.spots: expected an array");
1003
+ const spotIds = /* @__PURE__ */ new Set();
1004
+ const spots = value.spots.map((spot, index) => {
1005
+ const parsed = parseSpot(spot, `map.spots[${index}]`, sceneIds);
1006
+ if (spotIds.has(parsed.id))
1007
+ throw new Error(`map.spots[${index}].id: duplicate spot ID`);
1008
+ spotIds.add(parsed.id);
1009
+ return parsed;
1010
+ });
1011
+ const sector = value.sector === void 0 ? void 0 : parseSector(value.sector, "map.sector");
1012
+ return {
1013
+ image,
1014
+ ...interaction === void 0 ? {} : { interaction },
1015
+ ...presentation === void 0 ? {} : { presentation },
1016
+ ...controls === void 0 ? {} : { controls },
1017
+ ...iconSize === void 0 ? {} : { iconSize },
1018
+ ...maxScale === void 0 ? {} : { maxScale },
1019
+ spots,
1020
+ ...sector === void 0 ? {} : { sector }
1021
+ };
1022
+ } catch (cause) {
1023
+ if (cause instanceof PanoDataError) throw cause;
1024
+ const message = cause instanceof Error ? cause.message : "Invalid map";
1025
+ throw new PanoDataError("INVALID_PROJECT", message, {
1026
+ field: message.split(":")[0],
1027
+ cause
1028
+ });
1029
+ }
1030
+ }
1031
+ function resolvePanoMapAssets(map, baseUrl, resolveAssetUrl) {
1032
+ const resolve = (path, spotId) => mapResource(
1033
+ path,
1034
+ {
1035
+ defaultUrl: resolveResource(baseUrl, path),
1036
+ kind: "map",
1037
+ sceneId: spotId ?? "",
1038
+ hotspotId: spotId
1039
+ },
1040
+ resolveAssetUrl
1041
+ );
1042
+ return {
1043
+ ...map,
1044
+ image: resolve(map.image),
1045
+ spots: map.spots.map(
1046
+ (spot) => spot.icon ? { ...spot, icon: resolve(spot.icon, spot.id) } : { ...spot }
1047
+ )
1048
+ };
1049
+ }
1050
+
1051
+ // src/data/project.ts
1052
+ function optionalString2(record, key) {
1053
+ return record[key] === void 0 ? void 0 : requiredString(record[key], key);
1054
+ }
851
1055
  function parsePanoProject(value, options = {}) {
852
1056
  if (!isRecord(value))
853
1057
  throw new PanoDataError("INVALID_PROJECT", "Expected a project object");
@@ -878,7 +1082,7 @@ function parsePanoProject(value, options = {}) {
878
1082
  throw new Error(
879
1083
  "source: provide exactly one of viewerUrl or viewerConfig"
880
1084
  );
881
- const baseUrl = optionalString(src, "baseUrl");
1085
+ const baseUrl = optionalString2(src, "baseUrl");
882
1086
  const source = src.viewerUrl !== void 0 ? {
883
1087
  viewerUrl: requiredString(src.viewerUrl, "source.viewerUrl"),
884
1088
  baseUrl
@@ -918,7 +1122,7 @@ function parsePanoProject(value, options = {}) {
918
1122
  );
919
1123
  return {
920
1124
  id,
921
- title: optionalString(entry, "title"),
1125
+ title: optionalString2(entry, "title"),
922
1126
  source,
923
1127
  initialView,
924
1128
  ...viewLimits === void 0 ? {} : { viewLimits },
@@ -939,7 +1143,7 @@ function parsePanoProject(value, options = {}) {
939
1143
  }
940
1144
  }
941
1145
  );
942
- const initialSceneId = optionalString(value, "initialSceneId") ?? scenes[0]?.id;
1146
+ const initialSceneId = optionalString2(value, "initialSceneId") ?? scenes[0]?.id;
943
1147
  if (initialSceneId !== void 0 && !ids.has(initialSceneId))
944
1148
  throw new PanoDataError(
945
1149
  "INVALID_REFERENCE",
@@ -967,12 +1171,14 @@ function parsePanoProject(value, options = {}) {
967
1171
  }
968
1172
  }
969
1173
  }
1174
+ const map = parsePanoMap(value.map, ids, options);
970
1175
  return {
971
1176
  schemaVersion: 1,
972
- title: optionalString(value, "title"),
1177
+ title: optionalString2(value, "title"),
973
1178
  initialSceneId,
974
1179
  scenes,
975
- ...popups === void 0 ? {} : { popups }
1180
+ ...popups === void 0 ? {} : { popups },
1181
+ ...map === void 0 ? {} : { map }
976
1182
  };
977
1183
  } catch (cause) {
978
1184
  if (cause instanceof PanoDataError) throw cause;
@@ -1071,7 +1277,14 @@ async function loadPanoProject(source, options = {}) {
1071
1277
  title: definition.title,
1072
1278
  initialSceneId: definition.initialSceneId,
1073
1279
  scenes,
1074
- ...definition.popups === void 0 ? {} : { popups: definition.popups.map((popup) => resolveProjectPopupAssets(popup, root, options.resolvePopupAssetUrl)) }
1280
+ ...definition.popups === void 0 ? {} : { popups: definition.popups.map((popup) => resolveProjectPopupAssets(popup, root, options.resolvePopupAssetUrl)) },
1281
+ ...definition.map === void 0 ? {} : {
1282
+ map: resolvePanoMapAssets(
1283
+ definition.map,
1284
+ root,
1285
+ options.resolveAssetUrl
1286
+ )
1287
+ }
1075
1288
  };
1076
1289
  } finally {
1077
1290
  signal?.removeEventListener("abort", cancel);