@ericchen1990/pano-view 3.9.0 → 4.0.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.zh-CN.md CHANGED
@@ -226,17 +226,18 @@ export function Tour() {
226
226
 
227
227
  ```tsx
228
228
  import { useState } from "react";
229
- import { PanoViewer, ProjectScenes, type LoadedPanoProject, type ProjectPopupDefinition } from "@ericchen1990/pano-view";
229
+ import { PanoOverlay, PanoViewer, ProjectScenes, type LoadedPanoProject, type ProjectPopupDefinition } from "@ericchen1990/pano-view";
230
230
  import { ProjectPopupDialog } from "@ericchen1990/pano-view/ui";
231
231
  import "@ericchen1990/pano-view/ui/styles.css";
232
232
 
233
233
  export function Tour({ project }: { project: LoadedPanoProject }) {
234
234
  const [popup, setPopup] = useState<ProjectPopupDefinition | null>(null);
235
- return <PanoViewer renderOverlay={() => (
236
- <ProjectPopupDialog popup={popup} onOpenChange={(open) => { if (!open) setPopup(null); }} />
237
- )}>
235
+ return <PanoViewer>
238
236
  <ProjectScenes project={project} onPopupRequest={({ popup }) => setPopup(popup)}
239
237
  onActiveSceneIdChange={() => setPopup(null)} />
238
+ <PanoOverlay>
239
+ <ProjectPopupDialog popup={popup} onOpenChange={(open) => { if (!open) setPopup(null); }} />
240
+ </PanoOverlay>
240
241
  </PanoViewer>;
241
242
  }
242
243
  ```
@@ -254,12 +255,14 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
254
255
  "maxScale": 4,
255
256
  "iconSize": 28,
256
257
  "presentation": {
257
- "mode": "docked",
258
- "corner": "bottom-left",
258
+ "mode": "panel",
259
+ "anchor": {
260
+ "align": "bottom-left",
261
+ "axis": "horizontal",
262
+ "inset": 16
263
+ },
259
264
  "width": 240,
260
- "height": 180,
261
- "offsetX": 16,
262
- "offsetY": 16
265
+ "height": 180
263
266
  },
264
267
  "controls": { "collapse": true, "zoom": true },
265
268
  "sector": {
@@ -285,70 +288,70 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
285
288
 
286
289
  `image` 是相对项目目录的路径或绝对 URL。`spots` 必填。点位 `id` 唯一,每个 `sceneId` 必须指向项目中的场景。`x`、`y` 是归一化图像坐标,范围 [0, 1],原点在左上角。`heading` 是查看器 yaw 为 0 时、从图像向上方向起算的顺时针角度;扇形实际旋转为 `heading + yaw`。多个点位可以指向同一场景,扇形画在第一个 `sceneId` 与当前场景相同的点位上。`icon` 可选;省略时使用内置标记。
287
290
 
288
- `interaction` 为 `"fixed"`(默认)或 `"zoom"`。固定模式把图像适配进面板,忽略滚轮、拖拽平移和缩放按钮。缩放模式允许滚轮缩放,并在比例大于适配尺寸后拖拽平移。地图上的滚轮不会带动页面滚动。`maxScale` 是相对适配尺寸的最大倍数,至少为 1,默认 8。`dragMotion` 控制放大后的平移惯性与边界回弹(默认均开启)。设为 `false` 可同时关闭,或传 `{ inertia?: boolean; bounce?: boolean }` 分别开关。点位与朝向编辑仍是直接拖动。`controls.zoom` 仅在 `"zoom"` 时显示缩放按钮。`controls.collapse` 在停靠面板上显示展开和收起。`iconSize` 是标记边长,单位为 CSS 像素,默认 28。图像缩放时,图标和扇形保持屏幕尺寸。
291
+ `interaction` 为 `"fixed"`(默认)或 `"zoom"`。固定模式把图像适配进面板,忽略滚轮、拖拽平移和缩放按钮。缩放模式允许滚轮缩放,并在比例大于适配尺寸后拖拽平移。地图上的滚轮不会带动页面滚动。`maxScale` 是相对适配尺寸的最大倍数,至少为 1,默认 8。`dragMotion` 控制放大后的平移惯性与边界回弹(默认均开启)。设为 `false` 可同时关闭,或传 `{ inertia?: boolean; bounce?: boolean }` 分别开关。点位与朝向编辑仍是直接拖动。`controls.zoom` 仅在 `"zoom"` 时显示缩放按钮。`controls.collapse` 显示边缘拉手,把面板滑出屏幕。`iconSize` 是标记边长,单位为 CSS 像素,默认 28。图像缩放时,图标和扇形保持屏幕尺寸。
289
292
 
290
293
  `sector` 可选。`angle` 是开口角度,大于 0 且小于 360。`radius` 单位为 CSS 像素,不随图像缩放。`followFov` 用查看器视场角和地图面板宽高比换算水平视场,代替 `angle`。没有 `gradient` 时,填充为纯色 `color`(默认 `#ffffff`),不透明度为 `opacity`(默认 0.55)。`gradient.type` 为 `"radial"` 或 `"linear"`,至少两个色标。每个色标包含 [0, 1] 的 `offset`、`color`,以及可选 `opacity`。色标不透明度只描述渐隐形状。`sector.opacity` 在纯色和渐变两种模式下都缩放整块填充。`strokeColor`、`strokeWidth`、`strokeOpacity` 在宽度大于 0 时绘制描边,描边宽度保持 CSS 像素。
291
294
 
292
- `presentation.mode` 为 `"docked"`(默认)或 `"dialog"`。两者都接受 `corner`(`top-left`、`top-right`、`bottom-left`、`bottom-right`,默认 `bottom-left`)、`offsetX` 与 `offsetY`(默认 16),以及 `width` 和 `height`。停靠面板默认 220×160。对话框模式停靠一个打开按钮(未写宽高时为 48×48),并在查看器内部打开同一张地图。选中点位后关闭对话框。地图界面锚定在整个查看器 overlay 上,父页面滚动不会移动或改变它的尺寸。
295
+ `presentation.mode` 为 `"panel"`(默认)或 `"dialog"`。两者都接受 `anchor`,以及 `width` 和 `height`。`anchor` 是场景菜单与地图共用的锚定:`align` 为 `top`、`bottom`、`left`、`right` 或四个角之一(默认 `bottom-left`);`axis` 为 `"horizontal"` 或 `"vertical"`,只在四角生效(默认 `"horizontal"`);`inset` 可以是数字、CSS 长度字符串,或 `{ top, right, bottom, left }`(默认 `16`)。计算时会加上安全区。面板默认 220×160。对话框模式放置一个打开按钮(未写宽高时为 48×48),并在查看器内部打开同一张地图。选中点位后关闭对话框。地图界面锚定在整个查看器 overlay 上,父页面滚动不会移动或改变它的尺寸。四边在给定主轴尺寸时沿该边居中。四角贴住相邻两边。收起时面板保持尺寸,沿外侧滑出:四边朝该边,四角在水平轴向时朝上或下,垂直轴向时朝左或右。拉手留在该边内侧。
293
296
 
294
- 文档保存默认值(`image`、`spots`、`sector`、`presentation`、`interaction`、`maxScale`、`dragMotion`、`controls`、`iconSize`)。挂载 `PanoMapOverlay` 时可用组件 props 覆盖本次展示的尺寸、位置和缩放,不必克隆整份 `map`。优先级为:内置默认值 → 地图文档 → `presentationOverride`(形状与 `map.presentation` 相同)→ 单独 props。点位几何与扇形样式仍写在文档里。`selectedSpotId` / `onSelectedSpotIdChange` 会向下传给内部 `PanoMap`。停靠模式的收起状态由 overlay 内部管理;对话框模式与 `editorSurface` 会强制关掉收起控件,不能从外面控制 `collapsed`。
297
+ 文档保存默认值(`image`、`spots`、`sector`、`presentation`、`interaction`、`maxScale`、`dragMotion`、`controls`、`iconSize`)。挂载 `PanoMapOverlay` 时可用组件 props 覆盖本次展示的尺寸、位置和缩放,不必克隆整份 `map`。优先级为:内置默认值 → 地图文档 → `presentationOverride`(形状与 `map.presentation` 相同)→ 单独 props。点位几何与扇形样式仍写在文档里。`selectedSpotId` / `onSelectedSpotIdChange` 会向下传给内部 `PanoMap`。面板的收起状态由 overlay 内部管理;对话框模式与 `editorSurface` 会强制关掉边缘拉手。直接使用 `PanoMap` 时,`collapsed` 仍只是在宿主框内藏起图面。
295
298
 
296
299
  #### 挂载 `PanoMapOverlay`
297
300
 
298
- 在 `PanoViewer.renderOverlay` 中挂载,并把交给 `ProjectScenes` 的同一个 `activeSceneId` 传进去。内置 `pano-map-*` 类名来自 `@ericchen1990/pano-view/styles.css`。overlay 从查看器读取 yaw 和视场角,不会自己切换场景:在 `onSelectScene` 里设置当前场景。相机状态来自查看器之外时传入 `view`;查看器外又不传 `view` 时,图面停在 yaw `0`、pitch `0`、fov `75`。`PanoMap` 只负责图面,由宿主自己安排外框。
301
+ 在 `PanoOverlay` 中挂载,并把交给 `ProjectScenes` 的同一个 `activeSceneId` 传进去。内置 `pano-map-*` 类名来自 `@ericchen1990/pano-view/styles.css`。overlay 从查看器读取 yaw 和视场角,不会自己切换场景:在 `onSelectScene` 里设置当前场景。相机状态来自查看器之外时传入 `view`;查看器外又不传 `view` 时,图面停在 yaw `0`、pitch `0`、fov `75`。`PanoMap` 只负责图面,由宿主自己安排外框。
299
302
 
300
- `PanoMap` 还可受控选中与收起:`selectedSpotId` / `onSelectedSpotIdChange`(省略时内部选中,默认 `null`);`collapsed` / `onCollapsedChange`(非受控初始为展开);`renderChrome` 在图面之上渲染宿主控件。`showZoomControls` / `showCollapseControl` 与 overlay 相同,也可直接传给 `PanoMap`。`className` / `style` 挂在表面根节点上。
303
+ `PanoMap` 还可受控选中与收起:`selectedSpotId` / `onSelectedSpotIdChange`(省略时内部选中,默认 `null`);`collapsed` / `onCollapsedChange`(非受控初始为展开,并在宿主框内藏起图面);`renderChrome` 在图面之上渲染宿主控件。在 `PanoMapOverlay` 上,`showCollapseControl` 显示共用的边缘拉手。`className` / `style` 挂在表面根节点上。
301
304
 
302
305
  ```tsx
303
306
  import { useState } from "react";
304
307
  import {
305
- PanoViewer, ProjectScenes, PanoMapOverlay, type LoadedPanoProject,
308
+ PanoOverlay, PanoViewer, ProjectScenes, PanoMapOverlay, type LoadedPanoProject,
306
309
  } from "@ericchen1990/pano-view";
307
310
  import "@ericchen1990/pano-view/styles.css";
308
311
 
309
312
  export function Tour({ project }: { project: LoadedPanoProject }) {
310
313
  const [activeSceneId, setActiveSceneId] = useState(project.initialSceneId);
311
314
  const map = project.map;
312
- return <PanoViewer style={{ height: 600 }} renderOverlay={map ? () => (
313
- <PanoMapOverlay
314
- map={map}
315
- activeSceneId={activeSceneId}
316
- onSelectScene={setActiveSceneId}
317
- />
318
- ) : undefined}>
315
+ return <PanoViewer style={{ height: 600 }}>
319
316
  <ProjectScenes
320
317
  project={project}
321
318
  activeSceneId={activeSceneId}
322
319
  onActiveSceneIdChange={setActiveSceneId}
323
320
  />
321
+ {map ? (
322
+ <PanoOverlay>
323
+ <PanoMapOverlay
324
+ map={map}
325
+ activeSceneId={activeSceneId}
326
+ onSelectScene={setActiveSceneId}
327
+ />
328
+ </PanoOverlay>
329
+ ) : null}
324
330
  </PanoViewer>;
325
331
  }
326
332
  ```
327
333
 
328
- #### 尺寸、角落与边距
334
+ #### 尺寸、对齐与边距
329
335
 
330
- - `presentation` — `"docked"` | `"dialog"`(默认取自文档,否则为 `"docked"`)。
331
- - `corner` — `"top-left"` | `"top-right"` | `"bottom-left"` | `"bottom-right"`(默认 `"bottom-left"`)。
332
- - `width` / `height` — 停靠面板尺寸,或对话框打开按钮尺寸,单位为 CSS 像素。停靠默认 220×160;打开按钮默认 48×48。
333
- - `offsetX` / `offsetY` — 相对所选角落的边距,单位为 CSS 像素(默认 16)。
336
+ - `presentation` — `"panel"` | `"dialog"`(默认取自文档,否则为 `"panel"`)。
337
+ - `anchor` — 共用的 `PanoOverlayAnchor`。`align` 是八个位置之一(默认 `"bottom-left"`)。`axis` 用于四角。`inset` 默认 `16`。
338
+ - `width` / `height` — 面板尺寸,或对话框打开按钮尺寸,单位为 CSS 像素。面板默认 220×160;打开按钮默认 48×48。
334
339
 
335
340
  ```tsx
336
341
  <PanoMapOverlay
337
342
  map={map}
338
343
  activeSceneId={activeSceneId}
339
344
  onSelectScene={setActiveSceneId}
340
- presentation="docked"
341
- corner="bottom-right"
345
+ presentation="panel"
346
+ anchor={{ align: "bottom-right", axis: "vertical", inset: 20 }}
342
347
  width={280}
343
348
  height={200}
344
- offsetX={20}
345
- offsetY={20}
346
349
  />
347
350
  ```
348
351
 
349
352
  #### 对话框模式
350
353
 
351
- 对话框模式停靠一个打开按钮,并在查看器内部打开地图。打开按钮尺寸使用 `width` / `height`(以及 `corner` 与边距)。对话框本体使用另一组尺寸:
354
+ 对话框模式放置一个打开按钮,并在查看器内部打开地图。打开按钮尺寸使用 `width` / `height`,位置使用同一个 `anchor`。对话框本体使用另一组尺寸:
352
355
 
353
356
  - `dialogWidth` / `dialogHeight` — 期望的对话框宽高,单位为 CSS 像素(默认 720×520)。两者都会按查看器可用区域夹紧,四周各留 16 像素边距。
354
357
  - `openLabel` / `closeLabel` / `title` — 打开按钮无障碍名称、关闭按钮文案与对话框标题(默认分别为 `"Map"`、`"Close"`、`"Map"`)。
@@ -361,7 +364,7 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
361
364
  activeSceneId={activeSceneId}
362
365
  onSelectScene={setActiveSceneId}
363
366
  presentation="dialog"
364
- corner="bottom-left"
367
+ anchor={{ align: "bottom-left", inset: 16 }}
365
368
  width={52}
366
369
  height={52}
367
370
  dialogWidth={640}
@@ -397,6 +400,36 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
397
400
  />
398
401
  ```
399
402
 
403
+ 面板边缘拉手可用 `toggle` 换图标或改样式。对话框模式没有这条拉手。省略 `toggle` 时仍是默认箭头。`icon` 传元素(`<Icon />`),不要传组件类型。图标要随收起状态变化时传函数,参数为 `{ collapsed }`。`className` 追加到按钮上。`style` 覆盖背景、边框、颜色和尺寸;位置和 `transform` 由贴边布局负责,不要在 `style` 里改。默认箭头使用 `currentColor`,只改 `style.color` 就会变色。
404
+
405
+ ```tsx
406
+ <PanoMapOverlay
407
+ map={map}
408
+ activeSceneId={activeSceneId}
409
+ onSelectScene={setActiveSceneId}
410
+ showCollapseControl
411
+ toggle={{ style: { color: "#7dd3fc" } }}
412
+ />
413
+ ```
414
+
415
+ ```tsx
416
+ <PanoMapOverlay
417
+ map={map}
418
+ activeSceneId={activeSceneId}
419
+ onSelectScene={setActiveSceneId}
420
+ showCollapseControl
421
+ toggle={{
422
+ className: "map-tab",
423
+ style: {
424
+ background: "#0c4a6e",
425
+ color: "#7dd3fc",
426
+ border: "1px solid #7dd3fc",
427
+ },
428
+ icon: ({ collapsed }) => (collapsed ? "+" : "–"),
429
+ }}
430
+ />
431
+ ```
432
+
400
433
  #### 编辑模式
401
434
 
402
435
  `mode="edit"` 用于编辑:设置 `placementSceneId` 后点击地图会新增点位,其 `heading` 取当前 yaw 的相反数,因此在该 yaw 下扇形指向图像上方;拖动点位改变位置,拖动扇形设置 `heading`。`onEditChange` 返回 `{ spots?, selectedSpotId? }`。访客播放保持默认的 `mode="view"`。`editorSurface` 会放大编辑面板;传入 `width` / `height` 可指定尺寸,否则按查看器比例计算。
@@ -652,14 +685,15 @@ export function Tour() {
652
685
  ### 事件与界面元素
653
686
 
654
687
  - [`PanoEvents`](#panoevents) — 查看器级生命周期与交互回调
688
+ - [`PanoOverlay`](#dom-overlay-与弹窗) — 查看器平面上的 DOM 包裹,全屏时仍可见
655
689
  - [`PanoContextMenu`](#panocontextmenu) — 右键重置 / 全屏菜单
656
- - [`PanoSceneMenu`](#panoscenemenu) — 供 `renderOverlay` 使用的可滚动缩略图场景菜单
690
+ - [`PanoSceneMenu`](#panoscenemenu) — 供 `PanoOverlay` 使用的可滚动缩略图场景菜单
657
691
  - [`PanoVideoControls`](#panovideo) — `PanoVideo` 的播放界面(默认挂载)
658
692
 
659
693
  ### 平面图
660
694
 
661
695
  - [`PanoMap`](#平面图地图) — 平面图、场景点位,以及随水平角转动的扇形
662
- - [`PanoMapOverlay`](#平面图地图) — 供 `renderOverlay` 使用的停靠面板或对话框按钮
696
+ - [`PanoMapOverlay`](#平面图地图) — 供 `PanoOverlay` 使用的停靠面板或对话框按钮
663
697
 
664
698
  ### 热点
665
699
 
@@ -684,7 +718,25 @@ export function Tour() {
684
718
 
685
719
  ## DOM Overlay 与弹窗
686
720
 
687
- 普通 DOM 界面使用 `PanoViewer.renderOverlay`,位于 Three.js 渲染树之外。`usePanoOverlay()` 必须在 `renderOverlay` 子树内调用,返回 `portalContainer`、`size`(viewer 宽高)、`visibleRect`(可见矩形)与 `isFullscreen`。`usePanoModal(open)` 在 `open === true` 时占用该 viewer 的模态锁(暂停输入与自动旋转、背景 inert、关闭右键菜单);不负责焦点陷阱。`PanoOverlayPortalProvider` 接收必填的 `container` 与 `children`,只替换后代读到的 `portalContainer`;`size` / `visibleRect` 仍是整个 viewer。字段含义、引用计数与第三方 Dialog 接线见独立的 **[弹窗使用与自定义样式指南](./docs/dialogs.zh-CN.md)**。可选 `/ui` 入口提供 `PanoDialog` 与组成组件。指南随 npm 包分发;在本仓库运行 `pnpm dev`,打开 `/overlays` 可体验示例。
721
+ 普通 DOM 界面优先使用 `PanoOverlay`,位于 Three.js 渲染树之外,挂在全屏根节点内的查看器平面上,因此全屏时仍可见。把它作为 `PanoViewer` 的直接子节点(或包在其下的 Fragment 里),与场景内容并列。`renderOverlay` 仍可用,适合回调式挂载:
722
+
723
+ ```tsx
724
+ <PanoViewer style={{ height: 560 }}>
725
+ <Sphere src="/pano.jpg" />
726
+ <PanoOverlay>
727
+ <PanoSceneMenu items={items} activeId={id} onSelect={setId} />
728
+ <button
729
+ type="button"
730
+ style={{ position: "absolute", right: 16, bottom: 16, pointerEvents: "auto" }}
731
+ onClick={() => void viewer.toggleFullscreen()}
732
+ >
733
+ Fullscreen
734
+ </button>
735
+ </PanoOverlay>
736
+ </PanoViewer>
737
+ ```
738
+
739
+ 该层已是 `position: absolute; inset: 0; pointer-events: none`。子节点自行定位,可点击节点设置 `pointerEvents: "auto"`。同时使用时,`renderOverlay` 先渲染,其后按出现顺序渲染各个 `PanoOverlay`。`usePanoOverlay()` 必须在 `PanoOverlay`(或 `renderOverlay`)子树内调用,返回 `portalContainer`、`size`(viewer 宽高)、`visibleRect`(可见矩形)与 `isFullscreen`。`usePanoModal(open)` 在 `open === true` 时占用该 viewer 的模态锁(暂停输入与自动旋转、背景 inert、关闭右键菜单);不负责焦点陷阱。`PanoOverlayPortalProvider` 接收必填的 `container` 与 `children`,只替换后代读到的 `portalContainer`;`size` / `visibleRect` 仍是整个 viewer。字段含义、引用计数与第三方 Dialog 接线见独立的 **[弹窗使用与自定义样式指南](./docs/dialogs.zh-CN.md)**。可选 `/ui` 入口提供 `PanoDialog` 与组成组件。指南随 npm 包分发;在本仓库运行 `pnpm dev`,打开 `/overlays` 可体验示例。
688
740
 
689
741
  ### 默认弹窗
690
742
 
@@ -769,7 +821,7 @@ await viewer.zoomTo(48, { duration: 400, easing: "linear" });
769
821
 
770
822
  **场景**(需 `<Scenes />`)— `setScene(id)`、`nextScene`、`previousScene`、`getActiveSceneId`、`getSceneIds`、`isSceneTransitioning`。`Scenes` 支持 `defaultActiveSceneId`(非受控)或 `activeSceneId`(受控)。受控模式下命令式调用会触发 `onActiveSceneIdChange`。
771
823
 
772
- **WebVR**(需 `<WebVR />`)— `enterVR`、`exitVR`、`toggleVR`、`isVRAvailable`、`isVREnabled`、`getVRMode`、`requestVRPermission`。
824
+ **WebVR**(需 `<WebVR />`)— `enterVR`、`exitVR`、`toggleVR`、`isVRAvailable`、`isVREnabled`、`getVRMode`、`getVRState`、`subscribeVR`、`requestVRPermission`。用 `subscribeVR` 配合 `useSyncExternalStore` 做响应式进入 / 退出按钮;内置 chrome 不再渲染「进入 VR」。
773
825
 
774
826
  **视频**(需 `<PanoVideo />`)— `getVideo` 返回 `PanoVideoController`,`subscribeVideo` 订阅视频控制器与播放状态变更,另有快捷方法 `playVideo`、`pauseVideo`、`toggleVideo`、`seekVideo`、`setVideoVolume`、`setVideoMuted`、`toggleVideoMuted`。
775
827
 
@@ -785,7 +837,7 @@ await viewer.zoomTo(48, { duration: 400, easing: "linear" });
785
837
  - `cursors` — `true` / 省略使用 grab / grabbing / pointer / move;`false` 不改画布光标;对象可覆盖四个状态。
786
838
  - `contextMenu` — `true` / 省略挂载「重置视图 + 全屏」;`false` 不挂默认菜单;对象可改外观或 `items` / `prepend` / `append`。详见 [PanoContextMenu](#panocontextmenu)。
787
839
  - `mediaActivation` — 有播放意图的子组件时默认开启;`false` 关闭自动激活。
788
- - `renderOverlay` — 在画布外、全屏内渲染 DOM UI;回调收到 `portalContainer`、`size`、`visibleRect`、`isFullscreen`。
840
+ - `renderOverlay` — 可选的回调式 DOM UI,在画布外、全屏内渲染;回调收到 `portalContainer`、`size`、`visibleRect`、`isFullscreen`。优先用子组件 `<PanoOverlay>`。
789
841
  - `onViewChange` — 参数为 `{ yaw, pitch, fov }`(度)。
790
842
  - `onPanoramaClick` / `onPanoramaDoubleClick` / `onPanoramaPointerMove` — 参数为全景指针事件:`position`(yaw / pitch)、`nativeEvent`、`button`、`buttons`、`clientX`、`clientY`,以及可选的 `pointerId`。
791
843
  - 其余标准 `div` 属性(如 `className`、`style`、`id`)可原样传入;类型已去掉冲突的 `onChange` 与 `contextMenu`。
@@ -883,26 +935,30 @@ function SceneExample() {
883
935
 
884
936
  ### WebVR
885
937
 
886
- `enterVR` 与 `requestVRPermission` 须在用户手势中调用。
938
+ `enterVR` 与 `requestVRPermission` 须在用户手势中调用。通过 `getVRState` /
939
+ `subscribeVR` 由宿主 UI 驱动进入;会话 chrome 只显示退出 VR 与 MobileVR 设置
940
+ (空闲数秒后淡出)。
887
941
 
888
942
  ```tsx
943
+ import { useSyncExternalStore } from "react";
889
944
  import { PanoViewer, Sphere, WebVR, usePanoViewer } from "@ericchen1990/pano-view";
890
945
 
891
946
  function VRExample() {
892
947
  const viewer = usePanoViewer();
948
+ const vr = useSyncExternalStore(
949
+ viewer.subscribeVR,
950
+ viewer.getVRState,
951
+ viewer.getVRState,
952
+ );
893
953
 
894
954
  return (
895
955
  <>
896
956
  <button
897
- onClick={async () => {
898
- await viewer.requestVRPermission();
899
- await viewer.enterVR();
900
- }}
957
+ disabled={!vr.available}
958
+ onClick={() => void (vr.enabled ? viewer.exitVR() : viewer.enterVR())}
901
959
  >
902
- 进入 VR
960
+ {vr.enabled ? "退出 VR" : "进入 VR"}
903
961
  </button>
904
- <button onClick={() => void viewer.exitVR()}>退出 VR</button>
905
- <button onClick={() => console.log(viewer.getVRMode())}>当前模式?</button>
906
962
  <PanoViewer ref={viewer.ref} style={{ height: 560 }}>
907
963
  <WebVR />
908
964
  <Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
@@ -1348,7 +1404,7 @@ const scenes: Scene[] = [
1348
1404
 
1349
1405
  ## PanoSceneMenu
1350
1406
 
1351
- `PanoSceneMenu` 是可滚动的缩略图场景菜单。通过 `PanoViewer.renderOverlay` 挂载,这样在普通布局和全屏里都会留在 viewer 内。默认布局需导入 `@ericchen1990/pano-view/styles.css`。
1407
+ `PanoSceneMenu` 是可滚动的缩略图场景菜单。通过 `PanoOverlay` 挂载,这样在普通布局和全屏里都会留在 viewer 内。默认布局需导入 `@ericchen1990/pano-view/styles.css`。
1352
1408
 
1353
1409
  菜单由宿主驱动:传入 `items`、`activeId`、`onSelect`。它不读取 `Scenes` 状态,也不使用场景的 `previewUrl`。立方体场景的 `previewUrl` 通常是 1×6 预览条,不适合直接当菜单缩略图;请改用独立缩略图(转换器输出可优先用 `previews/200.webp` 这类小图)。
1354
1410
 
@@ -1359,6 +1415,7 @@ const scenes: Scene[] = [
1359
1415
  ```tsx
1360
1416
  import { useState } from "react";
1361
1417
  import {
1418
+ PanoOverlay,
1362
1419
  PanoSceneMenu,
1363
1420
  PanoViewer,
1364
1421
  Scenes,
@@ -1383,21 +1440,19 @@ export function Tour() {
1383
1440
  const [activeSceneId, setActiveSceneId] = useState("lobby");
1384
1441
 
1385
1442
  return (
1386
- <PanoViewer
1387
- style={{ height: 560 }}
1388
- renderOverlay={() => (
1389
- <PanoSceneMenu
1390
- activeId={activeSceneId}
1391
- items={menuItems}
1392
- onSelect={setActiveSceneId}
1393
- />
1394
- )}
1395
- >
1443
+ <PanoViewer style={{ height: 560 }}>
1396
1444
  <Scenes
1397
1445
  scenes={scenes}
1398
1446
  activeSceneId={activeSceneId}
1399
1447
  onActiveSceneIdChange={setActiveSceneId}
1400
1448
  />
1449
+ <PanoOverlay>
1450
+ <PanoSceneMenu
1451
+ activeId={activeSceneId}
1452
+ items={menuItems}
1453
+ onSelect={setActiveSceneId}
1454
+ />
1455
+ </PanoOverlay>
1401
1456
  </PanoViewer>
1402
1457
  );
1403
1458
  }
@@ -1405,19 +1460,19 @@ export function Tour() {
1405
1460
 
1406
1461
  每项需要 `id` 与 `thumbnailUrl`。可选 `title` 显示在缩略图底部标签和控制提示上;省略时用 `id`。当前项带叠在缩略图上的选择边框和 `data-active="true"`。场景名默认叠在缩略图底部的暗色半透明条上,可用 `appearance.labelAlign` 改为居中或右对齐。
1407
1462
 
1408
- ### 位置、边距与尺寸
1463
+ ### 对齐、边距与尺寸
1464
+
1465
+ `anchor` 是场景菜单和平面图共用的 `PanoOverlayAnchor`。
1409
1466
 
1410
- - `placement` — `"top"` | `"bottom"` | `"left"` | `"right"`(默认 `"bottom"`)。上下为横向列表,左右为纵向列表。
1411
- - `align` — `"start"` | `"center"` | `"end"`(默认 `"center"`)。菜单主轴尺寸小于可用空间时,沿停靠边对齐;列表内容短于视口时同样对齐。
1412
- - `inset` — 与 viewer 边缘的间隙(默认 `12`)。数字按像素;字符串可带单位(如 `"1rem"`);对象可分别设置 `top` / `right` / `bottom` / `left`。
1413
- - `width` / `height` — 菜单宽高。数字按像素;字符串会原样写入 CSS,因此可用 `%`、`rem`、`calc()`、`min()`、`max()`、`clamp()` 等。
1467
+ - `anchor.align` — `"top"` | `"bottom"` | `"left"` | `"right"` | `"top-left"` | `"top-right"` | `"bottom-left"` | `"bottom-right"`(默认 `"bottom"`)。上下是横条,左右是竖条。四角再看 `anchor.axis`。
1468
+ - `anchor.axis` — `"horizontal"` | `"vertical"`。四边忽略它。四角默认 `"horizontal"`:水平条带朝上或下收起,垂直条带朝左或右收起。
1469
+ - `anchor.inset` — 与 viewer 边缘的间隙(默认 `12`)。数字按像素;字符串可带单位(如 `"1rem"`);对象可分别设置 `top` / `right` / `bottom` / `left`。计算时会加上安全区。
1470
+ - `width` / `height` — 菜单宽高。数字按像素;字符串会原样写入 CSS,因此可用 `%`、`rem`、`calc()`、`min()`、`max()`、`clamp()` 等。四边给了主轴尺寸就沿该边居中,没给就在两侧 inset 之间撑满。四角贴住相邻两边,没给尺寸时收缩到内容。较短的列表靠向角落,在边上则居中。
1414
1471
  - `dragMotion` — 滚动惯性与边界回弹。省略则两者都开(自定义指针滚动,桌面也可拖)。设为 `false` 使用原生 overflow 滚动。可传 `{ inertia?: boolean; bounce?: boolean }` 分别开关。短按仍会选中;拖过数像素后会抑制 click。
1415
1472
 
1416
1473
  ```tsx
1417
1474
  <PanoSceneMenu
1418
- placement="bottom"
1419
- align="center"
1420
- inset={16}
1475
+ anchor={{ align: "bottom", inset: 16 }}
1421
1476
  width="min(720px, 100%)"
1422
1477
  items={menuItems}
1423
1478
  activeId={activeSceneId}
@@ -1425,9 +1480,11 @@ export function Tour() {
1425
1480
  />
1426
1481
 
1427
1482
  <PanoSceneMenu
1428
- placement="right"
1429
- align="start"
1430
- inset={{ top: 24, right: 12, bottom: 24, left: 12 }}
1483
+ anchor={{
1484
+ align: "top-left",
1485
+ axis: "vertical",
1486
+ inset: { top: 24, right: 12, bottom: 24, left: 12 },
1487
+ }}
1431
1488
  height="70%"
1432
1489
  width={160}
1433
1490
  items={menuItems}
@@ -1442,7 +1499,7 @@ export function Tour() {
1442
1499
 
1443
1500
  ### 展开收起与箭头
1444
1501
 
1445
- 默认始终展开。设 `collapsible` 后会出现开关,位于缩略图条朝向画面内侧的正中(底部菜单在条上方,顶部在条下方,左右在朝向画面一侧)。收起时列表沿停靠边滑出屏幕,只把展开按钮留在安全区边缘:
1502
+ 默认始终展开。设 `collapsible` 后会出现开关,位于收起边的内侧正中。收起时列表按自身尺寸加上该边 inset 滑出屏幕,展开按钮留在查看器边缘:
1446
1503
 
1447
1504
  ```tsx
1448
1505
  <PanoSceneMenu
@@ -1471,7 +1528,7 @@ export function Tour() {
1471
1528
  showArrows
1472
1529
  loop
1473
1530
  collapsible
1474
- placement="bottom"
1531
+ anchor={{ align: "bottom" }}
1475
1532
  width="min(720px, 100%)"
1476
1533
  items={menuItems}
1477
1534
  activeId={activeSceneId}
@@ -1518,6 +1575,36 @@ export function Tour() {
1518
1575
 
1519
1576
  根节点还带 `data-placement`、`data-align`、`data-orientation`(`horizontal` | `vertical`)、`data-expanded`、`data-collapsible`;当前项带 `data-active="true"`。
1520
1577
 
1578
+ `collapsible` 时,边缘拉手可用 `toggle` 换图标或改样式。省略 `toggle` 时仍是默认箭头。`icon` 传元素(`<Icon />`),不要传组件类型。图标要随收起状态变化时传函数,参数为 `{ collapsed }`(菜单展开时 `collapsed` 为 `false`)。`className` 追加到按钮上。`style` 覆盖背景、边框、颜色和尺寸;位置和 `transform` 由贴边布局负责,不要在 `style` 里改。默认箭头使用 `currentColor`,只改 `style.color` 就会变色。
1579
+
1580
+ ```tsx
1581
+ <PanoSceneMenu
1582
+ collapsible
1583
+ toggle={{ style: { color: "#7dd3fc" } }}
1584
+ items={menuItems}
1585
+ activeId={activeSceneId}
1586
+ onSelect={setActiveSceneId}
1587
+ />
1588
+ ```
1589
+
1590
+ ```tsx
1591
+ <PanoSceneMenu
1592
+ collapsible
1593
+ toggle={{
1594
+ className: "scene-tab",
1595
+ style: {
1596
+ background: "#0c4a6e",
1597
+ color: "#7dd3fc",
1598
+ border: "1px solid #7dd3fc",
1599
+ },
1600
+ icon: ({ collapsed }) => (collapsed ? "+" : "–"),
1601
+ }}
1602
+ items={menuItems}
1603
+ activeId={activeSceneId}
1604
+ onSelect={setActiveSceneId}
1605
+ />
1606
+ ```
1607
+
1521
1608
  ## PanoFilter
1522
1609
 
1523
1610
  在 `PanoViewer` 内渲染 `PanoFilter` 可对全景源(`Sphere`、`Tile`、`SphereTile`、`PanoVideo`)调色或风格化。内置预设仅作用于全景材质;`colorMatrix` 默认仅作用于全景,设 `colorMatrixScope="all"` 可同时作用于热点纹理。tooltip、视频控件与 iframe 等 DOM 叠加层不受影响。WebXR / MobileVR 使用相同的全景着色器,因此继承相同的滤镜外观。
@@ -1706,7 +1793,7 @@ export function GyroExample() {
1706
1793
 
1707
1794
  ## WebVR
1708
1795
 
1709
- `WebVR` 为最近的 `PanoViewer` 增加沉浸式观看。支持的头戴设备优先使用 `immersive-vr` WebXR 会话。无 WebXR 的手机使用设备方向的立体分屏;桌面可提供鼠标驱动的模拟 VR 预览。内置界面包括进入 VR、退出 VR 与 MobileVR 设置控件。
1796
+ `WebVR` 为最近的 `PanoViewer` 增加沉浸式观看。支持的头戴设备优先使用 `immersive-vr` WebXR 会话。无 WebXR 的手机使用设备方向的立体分屏;桌面可提供鼠标驱动的模拟 VR 预览。进入 VR 由宿主通过查看器句柄触发;内置 chrome 仅在会话中显示退出 VR 与 MobileVR 设置(空闲后淡出)。
1710
1797
 
1711
1798
  ```tsx
1712
1799
  import { useRef } from "react";
@@ -1735,25 +1822,33 @@ export function WebVRExample() {
1735
1822
  }
1736
1823
  ```
1737
1824
 
1738
- 句柄暴露 `enterVR()`、`exitVR()`、`toggleVR()`、`isAvailable()`、`isEnabled()`、`getMode()` 与 `requestPermission()`。模式为 `"webxr"`、`"mobilevr"`、`"fake"`。`profile` 接受 `"none"`、`"cardboard-v1"`、`"cardboard-v2"`、`"gear-vr"`、`"daydream"`,或自定义 `{ fov, ipdMm, k1, k2 }` 镜头配置。MobileVR 设置值会版本化并保存在当前源的 `localStorage` 中。VR 中中心准星显示十字;注视可交互热点时绘制顺时针填充环,经 `cursorDwellMs`(默认 1500)后触发热点点击。设 `cursorDwellMs={0}` 可保留准星但不自动点击。模拟桌面 VR(`fake` 模式)默认请求指针锁定(`mousePointerLock`),隐藏光标并用鼠标环视;若浏览器释放锁定需再次点击视图。回退会话通过 Screen Wake Lock API 或在不支持时的循环隐藏视频保持屏幕常亮(`wakelock`)。
1825
+ 句柄暴露 `enterVR()`、`exitVR()`、`toggleVR()`、`isAvailable()`、`isEnabled()`、`getMode()`、`getState()`、`subscribe()`、`requestPermission()` 与 `resetSettings()`。模式为 `"webxr"`、`"mobilevr"`、`"fake"`。`profile` 接受 `"none"`、`"cardboard-v1"`、`"cardboard-v2"`、`"gear-vr"`、`"daydream"`,或自定义 `{ fov, ipdMm, k1, k2, screenToLensMm?, trayDistanceMm? }` 镜头配置。在已知物理屏幕尺寸时,MobileVR 会用瞳距与可选的托盘到镜片高度(`trayDistanceMm`;Cardboard V1 为 `35`)放置每只眼睛的光轴;`0` 或省略则保持垂直方向居中。当 `screensize` 为 `"auto"` 时,MobileVR 会加载 Cardboard Device Parameter Database(`databaseUrl`,默认 Google 公开 DPDB)估算对角线英寸;传 `""` 或 `null` 可跳过请求。无法识别时触发 `onUnknownDevice`,并继续使用镜头预设 FOV(尺寸未知前不做托盘偏移)。MobileVR 设置值会版本化并保存在当前源的 `localStorage` 中;`resetSettings()` 会清除该存储并恢复 props 或内置默认(Setup 面板也有 Reset)。VR 中中心准星显示十字;注视可交互热点时绘制顺时针填充环,经 `cursorDwellMs`(默认 1500)后触发热点点击。设 `cursorDwellMs={0}` 可保留准星但不自动点击。模拟桌面 VR(`fake` 模式)默认请求指针锁定(`mousePointerLock`),隐藏光标并用鼠标环视;若浏览器释放锁定需再次点击视图。回退会话通过 Screen Wake Lock API 或在不支持时的循环隐藏视频保持屏幕常亮(`wakelock`)。
1739
1826
 
1740
1827
  WebXR 需要 HTTPS 与用户手势。移动端传感器访问也需要 HTTPS,iOS 可能需显式权限提示。嵌入查看器需要适当的 `xr-spatial-tracking`、`gyroscope`、`accelerometer` 权限。iframe 热点、tooltip、视频控件等 DOM 叠加层在沉浸式头戴会话中不可见;基于 mesh 的热点仍可通过默认 WebXR 控制器射线交互。
1741
1828
 
1742
1829
  ### 属性
1743
1830
 
1744
- - `chrome` — 默认 `true` 挂载进入 / 退出 / 设置界面。也可传外观对象:`background`(默认 `rgba(16, 24, 32, 0.78)`)、`color`(`#ffffff`)、`accent`(`#7dd3fc`)、`borderRadius`(`6`)、`fontSize`(`14`)、可选 `setupTitle`。
1831
+ - `chrome` — 默认 `true` 挂载会话内退出 / 设置界面(不含进入 VR 按钮)。也可传外观对象:`background`(默认 `rgba(16, 24, 32, 0.78)`)、`color`(`#ffffff`)、`accent`(`#7dd3fc`)、`borderRadius`(`6`)、`fontSize`(`14`)、可选 `setupTitle`。会话按钮约 3 秒无操作后淡出,与查看器的指针 / 滚轮 / 键盘交互后再次显示。
1745
1832
  - `mobileVr` — 默认 `true`。
1746
1833
  - `desktopSupport` — 默认 `false`(与 `Gyro` 的同名 prop 独立)。
1747
1834
  - `fakeSupport` — 默认 `true`,允许鼠标驱动的桌面模拟 VR。
1748
1835
  - `screensize` — `"auto"`(出厂 / 省略时的存储默认)或英寸数;数字会被夹到 **3–20** 并保留一位小数。
1749
- - `profile` — 省略时用已存设置,出厂为 `"cardboard-v1"`。也可传 `"none"`、`"cardboard-v2"`、`"gear-vr"`、`"daydream"`,或自定义 `{ fov, ipdMm, k1, k2, screenToLensMm?, label?, id? }`。
1836
+ - `databaseUrl` — `"auto"` 屏幕尺寸时使用的 Cardboard DPDB URL。默认 `https://storage.googleapis.com/cardboard-dpdb/dpdb.json`。传 `""` 或 `null` 可关闭在线查询。
1837
+ - `profile` — 省略时用已存设置,出厂为 `"cardboard-v1"`。也可传 `"none"`、`"cardboard-v2"`、`"gear-vr"`、`"daydream"`,或自定义 `{ fov, ipdMm, k1, k2, screenToLensMm?, trayDistanceMm?, label?, id? }`。`trayDistanceMm` 为头显托盘底边到镜片中心的毫米距离;`0` / 省略时垂直光轴保持屏幕中心。
1838
+ - `absolute` — 默认 `false`。MobileVR 使用罗盘绝对朝向(对 WebXR 无效)。
1839
+ - `north` — 默认 `0`。启用 `absolute` 时,全景中代表北方的 yaw。
1840
+ - `friction` — 默认 `0`(范围 `0–0.99`)。MobileVR 陀螺仪平滑;正常观看保持 `0`。
1841
+ - `touchSupport` — 默认 `false`。为 `true` 时允许水平拖动修正 MobileVR 朝向(`horizontaloffset`)。默认关闭,因为镜筒托盘可能误触。
1842
+ - `oversampling` — 默认 `1`,夹到 **0.5–2**。仅放大 MobileVR 畸变离屏目标(视口不变)。WebXR 则在会话开始前设置 framebuffer 缩放(会话中途不可改)。
1843
+ - `foveation` — 可选 WebXR 固定注视点渲染,范围 **0–1**。省略则用设备默认;`0` 为全分辨率,`1` 为边缘最大降采样。
1844
+ - `frameRate` — 可选 WebXR 目标刷新率:`"low" | "mid" | "high"` 或具体 Hz(取最接近的支持值)。省略则保持 store 默认(`"high"`)。
1750
1845
  - `cursor` — 默认 `true`;`false` 关闭中心准星。
1751
1846
  - `cursorDwellMs` — 默认 `1500`;`0` 不自动点击。
1752
1847
  - `wakelock` / `mousePointerLock` — 默认均为 `true`。
1753
1848
  - `onAvailable` / `onUnavailable` — VR 能力变化。
1754
1849
  - `onEnterVR` / `onExitVR` — 参数为模式 `"webxr" | "mobilevr" | "fake"`。
1755
1850
  - `onDenied` — 权限或会话被拒绝,参数为可选 `error`。
1756
- - `onUnknownDevice` — 无法识别设备屏幕 / 镜头参数时。
1851
+ - `onUnknownDevice` — `"auto"` 屏幕尺寸无法从 DPDB 解析时(URL 关闭、无匹配或请求失败)。
1757
1852
 
1758
1853
  ## 鼠标、触控与键盘控件
1759
1854
 
package/dist/data.cjs CHANGED
@@ -846,13 +846,56 @@ function resolveHotspotAssets(hotspot, baseUrl, sceneId, resolver) {
846
846
  return result;
847
847
  }
848
848
 
849
- // src/data/map.ts
850
- var CORNERS = /* @__PURE__ */ new Set([
849
+ // src/overlay/align.ts
850
+ var PANO_OVERLAY_ALIGNS = [
851
+ "top",
852
+ "bottom",
853
+ "left",
854
+ "right",
851
855
  "top-left",
852
856
  "top-right",
853
857
  "bottom-left",
854
858
  "bottom-right"
855
- ]);
859
+ ];
860
+ var ALIGN_LIST = PANO_OVERLAY_ALIGNS.join(", ");
861
+ function isRecord2(value) {
862
+ return typeof value === "object" && value !== null && !Array.isArray(value);
863
+ }
864
+ function parseInsetLength(value, field) {
865
+ if (typeof value === "number" && Number.isFinite(value)) return value;
866
+ if (typeof value === "string" && value.trim()) return value;
867
+ throw new Error(`${field}: expected a finite number or a nonempty string`);
868
+ }
869
+ function parseInset(value, field) {
870
+ if (typeof value === "number" || typeof value === "string") {
871
+ return parseInsetLength(value, field);
872
+ }
873
+ if (!isRecord2(value)) throw new Error(`${field}: expected a number, string, or object`);
874
+ const sides = ["top", "right", "bottom", "left"];
875
+ const inset = {};
876
+ for (const side of sides) {
877
+ if (value[side] === void 0) continue;
878
+ inset[side] = parseInsetLength(value[side], `${field}.${side}`);
879
+ }
880
+ return inset;
881
+ }
882
+ function parseOverlayAnchor(value, field) {
883
+ if (!isRecord2(value)) throw new Error(`${field}: expected an object`);
884
+ const align = value.align === void 0 ? void 0 : typeof value.align === "string" && PANO_OVERLAY_ALIGNS.includes(value.align) ? value.align : (() => {
885
+ throw new Error(`${field}.align: expected ${ALIGN_LIST}`);
886
+ })();
887
+ const axis = value.axis === void 0 ? void 0 : value.axis === "horizontal" || value.axis === "vertical" ? value.axis : (() => {
888
+ throw new Error(`${field}.axis: expected "horizontal" or "vertical"`);
889
+ })();
890
+ const inset = value.inset === void 0 ? void 0 : parseInset(value.inset, `${field}.inset`);
891
+ return {
892
+ ...align === void 0 ? {} : { align },
893
+ ...axis === void 0 ? {} : { axis },
894
+ ...inset === void 0 ? {} : { inset }
895
+ };
896
+ }
897
+
898
+ // src/data/map.ts
856
899
  function optionalNumber(record, key, options = {}) {
857
900
  const value = record[key];
858
901
  if (value === void 0) return void 0;
@@ -930,25 +973,29 @@ function parseSector(value, field) {
930
973
  function parsePresentation(value, field) {
931
974
  if (!isRecord(value)) throw new Error(`${field}: expected an object`);
932
975
  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
- })();
976
+ if (mode === "docked") {
977
+ throw new Error(`${field}.mode: "docked" was removed; expected "panel" or "dialog"`);
978
+ }
979
+ if (mode !== "panel" && mode !== "dialog") {
980
+ throw new Error(`${field}.mode: expected "panel" or "dialog"`);
981
+ }
982
+ if ("corner" in value) {
983
+ throw new Error(`${field}.corner: use anchor.align`);
984
+ }
985
+ if ("offsetX" in value) {
986
+ throw new Error(`${field}.offsetX: use anchor.inset`);
987
+ }
988
+ if ("offsetY" in value) {
989
+ throw new Error(`${field}.offsetY: use anchor.inset`);
990
+ }
991
+ const anchor = value.anchor === void 0 ? void 0 : parseOverlayAnchor(value.anchor, `${field}.anchor`);
941
992
  const width = optionalNumber(value, "width", { exclusiveMin: 0 });
942
993
  const height = optionalNumber(value, "height", { exclusiveMin: 0 });
943
- const offsetX = optionalNumber(value, "offsetX", { min: 0 });
944
- const offsetY = optionalNumber(value, "offsetY", { min: 0 });
945
994
  return {
946
995
  mode,
947
- ...corner === void 0 ? {} : { corner },
996
+ ...anchor === void 0 ? {} : { anchor },
948
997
  ...width === void 0 ? {} : { width },
949
- ...height === void 0 ? {} : { height },
950
- ...offsetX === void 0 ? {} : { offsetX },
951
- ...offsetY === void 0 ? {} : { offsetY }
998
+ ...height === void 0 ? {} : { height }
952
999
  };
953
1000
  }
954
1001
  function parseOptionalBoolean(value, field) {