@ericchen1990/pano-view 3.10.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.md CHANGED
@@ -259,12 +259,14 @@ An optional project-level `map` places scene spots on one floor image. Projects
259
259
  "maxScale": 4,
260
260
  "iconSize": 28,
261
261
  "presentation": {
262
- "mode": "docked",
263
- "corner": "bottom-left",
262
+ "mode": "panel",
263
+ "anchor": {
264
+ "align": "bottom-left",
265
+ "axis": "horizontal",
266
+ "inset": 16
267
+ },
264
268
  "width": 240,
265
- "height": 180,
266
- "offsetX": 16,
267
- "offsetY": 16
269
+ "height": 180
268
270
  },
269
271
  "controls": { "collapse": true, "zoom": true },
270
272
  "sector": {
@@ -290,19 +292,19 @@ An optional project-level `map` places scene spots on one floor image. Projects
290
292
 
291
293
  `image` is a project-relative path or an absolute URL. `spots` is required. Spot IDs are unique, and each `sceneId` must name a scene in the project. `x` and `y` are normalized image coordinates in [0, 1], with the origin at the top-left. `heading` is clockwise degrees from image-up when viewer yaw is 0; the drawn sector uses `heading + yaw`. Several spots may share a scene. The sector is drawn on the first spot whose `sceneId` matches the active scene. `icon` is optional; a spot without one uses the built-in marker.
292
294
 
293
- `interaction` is `"fixed"` (the default) or `"zoom"`. Fixed mode fits the image in the panel and ignores the wheel, drag-pan, and zoom buttons. Zoom mode allows wheel zoom, and drag-pan once the scale is above the fitted size. Wheel events on the map do not scroll the page. `maxScale` is the maximum multiple of that fitted size, at least 1, default 8. `dragMotion` controls pan inertia and edge bounce while zoomed (both on by default). Set `false` to disable both, or `{ inertia?: boolean; bounce?: boolean }` to toggle either flag. Spot and heading edits stay direct. `controls.zoom` shows the zoom buttons only when interaction is `"zoom"`. `controls.collapse` shows expand and collapse on a docked panel. `iconSize` is the marker edge in CSS pixels, default 28. Icons and the sector keep that screen size while the floor image zooms.
295
+ `interaction` is `"fixed"` (the default) or `"zoom"`. Fixed mode fits the image in the panel and ignores the wheel, drag-pan, and zoom buttons. Zoom mode allows wheel zoom, and drag-pan once the scale is above the fitted size. Wheel events on the map do not scroll the page. `maxScale` is the maximum multiple of that fitted size, at least 1, default 8. `dragMotion` controls pan inertia and edge bounce while zoomed (both on by default). Set `false` to disable both, or `{ inertia?: boolean; bounce?: boolean }` to toggle either flag. Spot and heading edits stay direct. `controls.zoom` shows the zoom buttons only when interaction is `"zoom"`. `controls.collapse` shows an edge tab that slides a panel off-screen. `iconSize` is the marker edge in CSS pixels, default 28. Icons and the sector keep that screen size while the floor image zooms.
294
296
 
295
297
  `sector` is optional. `angle` is the opening in degrees, greater than 0 and less than 360. `radius` is CSS pixels and does not scale with the image. `followFov` replaces `angle` with the horizontal field of view derived from the viewer FOV and the map panel's aspect ratio. Without `gradient`, the fill is solid `color` (default `#ffffff`) at `opacity` (default 0.55). `gradient.type` is `"radial"` or `"linear"`, with at least two stops. Each stop has `offset` in [0, 1], `color`, and an optional `opacity`. Stop opacities describe only the fade shape. `sector.opacity` scales the whole fill in both solid and gradient mode. `strokeColor`, `strokeWidth`, and `strokeOpacity` draw an outline when the width is greater than 0. The outline width stays in CSS pixels.
296
298
 
297
- `presentation.mode` is `"docked"` (the default) or `"dialog"`. Both accept `corner` (`top-left`, `top-right`, `bottom-left`, or `bottom-right`; default `bottom-left`), `offsetX` and `offsetY` (default 16), plus `width` and `height`. A docked panel defaults to 220×160. Dialog mode docks a launcher (48×48 when width and height are omitted) and opens the same map in a viewer-local dialog. Choosing a spot closes that dialog. The chrome is positioned on the full viewer overlay, so scrolling a parent page does not move or resize it.
299
+ `presentation.mode` is `"panel"` (the default) or `"dialog"`. Both accept `anchor`, plus `width` and `height`. `anchor` is the shared overlay anchor: `align` is `top`, `bottom`, `left`, `right`, or one of the four corners (default `bottom-left`); `axis` is `"horizontal"` or `"vertical"` and applies only to corners (default `"horizontal"`); `inset` is a number, a CSS length string, or `{ top, right, bottom, left }` (default `16`). Safe-area insets are added on top. A panel defaults to 220×160. Dialog mode places a launcher (48×48 when width and height are omitted) and opens the same map in a viewer-local dialog. Choosing a spot closes that dialog. The chrome is positioned on the full viewer overlay, so scrolling a parent page does not move or resize it. Edges center a sized box on that edge. Corners pin to both adjacent edges. Collapsing keeps the panel size and slides it off the outer edge: edges slide toward that edge, and corners slide vertically when `axis` is horizontal or horizontally when `axis` is vertical. The tab stays on the inner side of that edge.
298
300
 
299
- The document stores saved defaults (`image`, `spots`, `sector`, `presentation`, `interaction`, `maxScale`, `dragMotion`, `controls`, `iconSize`). Mount-time props on `PanoMapOverlay` override presentation and zoom for that render without cloning the map. Priority is built-in defaults → map document → `presentationOverride` (same shape as `map.presentation`) → individual props. Spot geometry and sector styling stay in the document. `selectedSpotId` / `onSelectedSpotIdChange` are forwarded to the inner `PanoMap`. Docked collapse state is owned by the overlay; dialog mode and `editorSurface` force collapse controls off, so `collapsed` cannot be driven from outside.
301
+ The document stores saved defaults (`image`, `spots`, `sector`, `presentation`, `interaction`, `maxScale`, `dragMotion`, `controls`, `iconSize`). Mount-time props on `PanoMapOverlay` override presentation and zoom for that render without cloning the map. Priority is built-in defaults → map document → `presentationOverride` (same shape as `map.presentation`) → individual props. Spot geometry and sector styling stay in the document. `selectedSpotId` / `onSelectedSpotIdChange` are forwarded to the inner `PanoMap`. Panel collapse state is owned by the overlay; dialog mode and `editorSurface` force the edge tab off. `PanoMap`'s own `collapsed` prop still hides the surface inside a host-owned frame.
300
302
 
301
303
  #### Mounting `PanoMapOverlay`
302
304
 
303
305
  Mount it inside `PanoOverlay` and pass the same `activeSceneId` you pass to `ProjectScenes`. Import `@ericchen1990/pano-view/styles.css` for the built-in `pano-map-*` classes. The overlay reads yaw and FOV from the viewer. It does not change scenes by itself: set the active scene from `onSelectScene`. Pass `view` when the camera state comes from outside the viewer; outside the viewer with no `view`, the surface stays at yaw `0`, pitch `0`, fov `75`. `PanoMap` is the surface alone, for a host-owned frame.
304
306
 
305
- `PanoMap` also accepts controlled selection and collapse: `selectedSpotId` / `onSelectedSpotIdChange` (internal selection defaults to `null`); `collapsed` / `onCollapsedChange` (uncontrolled starts expanded); `renderChrome` to draw host controls above the surface. `showZoomControls` / `showCollapseControl` work on `PanoMap` the same way as on the overlay. `className` / `style` attach to the surface root.
307
+ `PanoMap` also accepts controlled selection and collapse: `selectedSpotId` / `onSelectedSpotIdChange` (internal selection defaults to `null`); `collapsed` / `onCollapsedChange` (uncontrolled starts expanded and hides the surface inside the host frame); `renderChrome` to draw host controls above the surface. On `PanoMapOverlay`, `showCollapseControl` shows the shared edge tab instead. `className` / `style` attach to the surface root.
306
308
 
307
309
  ```tsx
308
310
  import { useState } from "react";
@@ -333,30 +335,27 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
333
335
  }
334
336
  ```
335
337
 
336
- #### Size, corner, and inset
338
+ #### Size, align, and inset
337
339
 
338
- - `presentation` — `"docked"` | `"dialog"` (defaults from the document, otherwise `"docked"`).
339
- - `corner` — `"top-left"` | `"top-right"` | `"bottom-left"` | `"bottom-right"` (default `"bottom-left"`).
340
- - `width` / `height` — docked panel size, or the dialog launcher size, in CSS pixels. Docked defaults to 220×160; the dialog launcher defaults to 48×48.
341
- - `offsetX` / `offsetY` — inset from the chosen corner in CSS pixels (default 16).
340
+ - `presentation` — `"panel"` | `"dialog"` (defaults from the document, otherwise `"panel"`).
341
+ - `anchor` — shared `PanoOverlayAnchor`. `align` is one of eight positions (default `"bottom-left"`). `axis` is used for corners. `inset` defaults to `16`.
342
+ - `width` / `height` — panel size, or the dialog launcher size, in CSS pixels. Panel defaults to 220×160; the dialog launcher defaults to 48×48.
342
343
 
343
344
  ```tsx
344
345
  <PanoMapOverlay
345
346
  map={map}
346
347
  activeSceneId={activeSceneId}
347
348
  onSelectScene={setActiveSceneId}
348
- presentation="docked"
349
- corner="bottom-right"
349
+ presentation="panel"
350
+ anchor={{ align: "bottom-right", axis: "vertical", inset: 20 }}
350
351
  width={280}
351
352
  height={200}
352
- offsetX={20}
353
- offsetY={20}
354
353
  />
355
354
  ```
356
355
 
357
356
  #### Dialog mode
358
357
 
359
- Dialog mode docks a launcher button and opens the map in a viewer-local modal. Launcher size uses `width` / `height` (and `corner` / offsets). The dialog body uses a separate pair:
358
+ Dialog mode places a launcher button and opens the map in a viewer-local modal. Launcher size uses `width` / `height`, and the launcher uses the same `anchor`. The dialog body uses a separate pair:
360
359
 
361
360
  - `dialogWidth` / `dialogHeight` — preferred dialog size in CSS pixels (default 720×520). Both are clamped to the viewer with a 16 px margin on each side.
362
361
  - `openLabel` / `closeLabel` / `title` — launcher accessible name, close button label, and dialog heading (defaults `"Map"`, `"Close"`, `"Map"`).
@@ -369,7 +368,7 @@ Choosing a spot closes the dialog.
369
368
  activeSceneId={activeSceneId}
370
369
  onSelectScene={setActiveSceneId}
371
370
  presentation="dialog"
372
- corner="bottom-left"
371
+ anchor={{ align: "bottom-left", inset: 16 }}
373
372
  width={52}
374
373
  height={52}
375
374
  dialogWidth={640}
@@ -405,6 +404,36 @@ These props override the map document for the current mount:
405
404
  />
406
405
  ```
407
406
 
407
+ `toggle` replaces the panel edge-tab icon or restyles that button. Dialog mode has no edge tab. Omit `toggle` to keep the default chevron. Pass an element (`<Icon />`) for `icon`, not a component type. When the glyph depends on the collapsed state, pass a function of `{ collapsed }`. `className` is appended to the button. `style` overrides background, border, color, and size. Position and `transform` belong to the edge layout; do not set them in `style`. The default chevron uses `currentColor`, so `style.color` recolors it without replacing the icon.
408
+
409
+ ```tsx
410
+ <PanoMapOverlay
411
+ map={map}
412
+ activeSceneId={activeSceneId}
413
+ onSelectScene={setActiveSceneId}
414
+ showCollapseControl
415
+ toggle={{ style: { color: "#7dd3fc" } }}
416
+ />
417
+ ```
418
+
419
+ ```tsx
420
+ <PanoMapOverlay
421
+ map={map}
422
+ activeSceneId={activeSceneId}
423
+ onSelectScene={setActiveSceneId}
424
+ showCollapseControl
425
+ toggle={{
426
+ className: "map-tab",
427
+ style: {
428
+ background: "#0c4a6e",
429
+ color: "#7dd3fc",
430
+ border: "1px solid #7dd3fc",
431
+ },
432
+ icon: ({ collapsed }) => (collapsed ? "+" : "–"),
433
+ }}
434
+ />
435
+ ```
436
+
408
437
  #### Edit mode
409
438
 
410
439
  `mode="edit"` is the authoring surface: with `placementSceneId` set, a click adds a spot whose heading is the negation of the current yaw, so the sector points image-up at that yaw; dragging a spot moves it, and dragging the sector sets `heading`. `onEditChange` reports `{ spots?, selectedSpotId? }`. Visitor playback stays on the default `mode="view"`. `editorSurface` enlarges the panel for authoring; pass `width` / `height` to size that surface, otherwise it scales with the viewer.
@@ -691,7 +720,7 @@ Presets and hotspot rendering are in [Scenes](#scenes).
691
720
  ### Floor plan
692
721
 
693
722
  - [`PanoMap`](#floor-plan-maps) — floor image, scene spots, and the yaw-linked sector
694
- - [`PanoMapOverlay`](#floor-plan-maps) — docked panel or dialog launcher for `PanoOverlay`
723
+ - [`PanoMapOverlay`](#floor-plan-maps) — panel or dialog launcher for `PanoOverlay`
695
724
 
696
725
  ### Hotspots
697
726
 
@@ -1561,18 +1590,21 @@ active item gets a selection ring above the thumbnail and `data-active="true"`.
1561
1590
  Scene names sit on a dark translucent bar at the bottom of each thumbnail;
1562
1591
  use `appearance.labelAlign` for `"center"` or `"end"` alignment.
1563
1592
 
1564
- ### Placement, inset, and size
1593
+ ### Align, inset, and size
1594
+
1595
+ `anchor` is the shared `PanoOverlayAnchor` used by the scene menu and the floor-plan map.
1565
1596
 
1566
- - `placement` — `"top"` | `"bottom"` | `"left"` | `"right"` (default `"bottom"`).
1567
- Top/bottom render a horizontal strip; left/right render a vertical strip.
1568
- - `align` — `"start"` | `"center"` | `"end"` (default `"center"`). When the menu
1569
- is smaller than the available dock space, aligns it along that edge. Short
1570
- lists are also aligned inside the strip the same way.
1571
- - `inset` — gap from the viewer edges (default `12`). A number is pixels; a
1597
+ - `anchor.align` — `"top"` | `"bottom"` | `"left"` | `"right"` | `"top-left"` | `"top-right"` | `"bottom-left"` | `"bottom-right"` (default `"bottom"`).
1598
+ Top and bottom are horizontal strips. Left and right are vertical strips.
1599
+ A corner uses `anchor.axis`.
1600
+ - `anchor.axis` — `"horizontal"` | `"vertical"`. Edges ignore it. Corners default to `"horizontal"`: a horizontal strip slides up or down, and a vertical strip slides left or right.
1601
+ - `anchor.inset` — gap from the viewer edges (default `12`). A number is pixels; a
1572
1602
  string keeps units such as `"1rem"`; an object sets `top` / `right` /
1573
- `bottom` / `left` independently.
1603
+ `bottom` / `left` independently. Safe-area insets are added.
1574
1604
  - `width` / `height` — menu size. A number is pixels; a string is used as CSS as-is,
1575
1605
  including `%`, `rem`, `calc()`, `min()`, `max()`, and `clamp()`.
1606
+ On an edge, a set main-axis size is centered and a missing one stretches between the cross-axis insets. A corner pins to both edges and shrinks to content when a size is omitted.
1607
+ Short lists pack toward the corner, or stay centered on an edge.
1576
1608
  - `dragMotion` — scroll inertia and edge bounce. Omit for both on (custom pointer
1577
1609
  scrolling, including desktop drag). Set `false` for native overflow scroll.
1578
1610
  Pass `{ inertia?: boolean; bounce?: boolean }` to toggle either flag. A short
@@ -1580,9 +1612,7 @@ use `appearance.labelAlign` for `"center"` or `"end"` alignment.
1580
1612
 
1581
1613
  ```tsx
1582
1614
  <PanoSceneMenu
1583
- placement="bottom"
1584
- align="center"
1585
- inset={16}
1615
+ anchor={{ align: "bottom", inset: 16 }}
1586
1616
  width="min(720px, 100%)"
1587
1617
  items={menuItems}
1588
1618
  activeId={activeSceneId}
@@ -1590,9 +1620,11 @@ use `appearance.labelAlign` for `"center"` or `"end"` alignment.
1590
1620
  />
1591
1621
 
1592
1622
  <PanoSceneMenu
1593
- placement="right"
1594
- align="start"
1595
- inset={{ top: 24, right: 12, bottom: 24, left: 12 }}
1623
+ anchor={{
1624
+ align: "top-left",
1625
+ axis: "vertical",
1626
+ inset: { top: 24, right: 12, bottom: 24, left: 12 },
1627
+ }}
1596
1628
  height="70%"
1597
1629
  width={160}
1598
1630
  items={menuItems}
@@ -1601,8 +1633,8 @@ use `appearance.labelAlign` for `"center"` or `"end"` alignment.
1601
1633
  />
1602
1634
  ```
1603
1635
 
1604
- If you omit `width` on a top/bottom menu, the strip stretches between the left
1605
- and right insets. If you omit `height` on a left/right menu, it stretches between
1636
+ If you omit `width` on a top or bottom menu, the strip stretches between the left
1637
+ and right insets. If you omit `height` on a left or right menu, it stretches between
1606
1638
  the top and bottom insets. The menu also applies a max size that subtracts
1607
1639
  `inset` and safe-area insets, so `100%` still stays inside the viewer.
1608
1640
 
@@ -1611,9 +1643,8 @@ Use `style` when you need one-off positioning overrides on top of these props.
1611
1643
  ### Collapsible menu and arrows
1612
1644
 
1613
1645
  By default the menu stays open. Set `collapsible` to show a toggle centered on
1614
- the inner edge of the strip (above a bottom menu, below a top menu, toward the
1615
- viewer for left/right). Collapsing slides the list off-screen along the dock
1616
- edge and leaves only the expand button on the safe-area edge:
1646
+ the inner side of the collapse edge. Collapsing slides the list off-screen by
1647
+ its size plus that edge's inset, and leaves the expand button on the viewer edge:
1617
1648
 
1618
1649
  ```tsx
1619
1650
  <PanoSceneMenu
@@ -1649,7 +1680,7 @@ via `label`. When collapsible, `defaultExpanded` defaults to `true`.
1649
1680
  showArrows
1650
1681
  loop
1651
1682
  collapsible
1652
- placement="bottom"
1683
+ anchor={{ align: "bottom" }}
1653
1684
  width="min(720px, 100%)"
1654
1685
  items={menuItems}
1655
1686
  activeId={activeSceneId}
@@ -1703,6 +1734,43 @@ The root also exposes `data-placement`, `data-align`, `data-orientation`
1703
1734
  (`horizontal` | `vertical`), `data-expanded`, and `data-collapsible`. The active
1704
1735
  item has `data-active="true"`.
1705
1736
 
1737
+ When `collapsible` is set, `toggle` replaces the edge-tab icon or restyles that
1738
+ button. Omit `toggle` to keep the default chevron. Pass an element (`<Icon />`)
1739
+ for `icon`, not a component type. When the glyph depends on the collapsed
1740
+ state, pass a function of `{ collapsed }` (`collapsed` is `false` while the
1741
+ menu is expanded). `className` is appended to the button. `style` overrides
1742
+ background, border, color, and size. Position and `transform` belong to the
1743
+ edge layout; do not set them in `style`. The default chevron uses
1744
+ `currentColor`, so `style.color` recolors it without replacing the icon.
1745
+
1746
+ ```tsx
1747
+ <PanoSceneMenu
1748
+ collapsible
1749
+ toggle={{ style: { color: "#7dd3fc" } }}
1750
+ items={menuItems}
1751
+ activeId={activeSceneId}
1752
+ onSelect={setActiveSceneId}
1753
+ />
1754
+ ```
1755
+
1756
+ ```tsx
1757
+ <PanoSceneMenu
1758
+ collapsible
1759
+ toggle={{
1760
+ className: "scene-tab",
1761
+ style: {
1762
+ background: "#0c4a6e",
1763
+ color: "#7dd3fc",
1764
+ border: "1px solid #7dd3fc",
1765
+ },
1766
+ icon: ({ collapsed }) => (collapsed ? "+" : "–"),
1767
+ }}
1768
+ items={menuItems}
1769
+ activeId={activeSceneId}
1770
+ onSelect={setActiveSceneId}
1771
+ />
1772
+ ```
1773
+
1706
1774
  ## PanoFilter
1707
1775
 
1708
1776
  Render `PanoFilter` inside `PanoViewer` to color-grade or stylize the panorama
package/README.zh-CN.md CHANGED
@@ -255,12 +255,14 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
255
255
  "maxScale": 4,
256
256
  "iconSize": 28,
257
257
  "presentation": {
258
- "mode": "docked",
259
- "corner": "bottom-left",
258
+ "mode": "panel",
259
+ "anchor": {
260
+ "align": "bottom-left",
261
+ "axis": "horizontal",
262
+ "inset": 16
263
+ },
260
264
  "width": 240,
261
- "height": 180,
262
- "offsetX": 16,
263
- "offsetY": 16
265
+ "height": 180
264
266
  },
265
267
  "controls": { "collapse": true, "zoom": true },
266
268
  "sector": {
@@ -286,19 +288,19 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
286
288
 
287
289
  `image` 是相对项目目录的路径或绝对 URL。`spots` 必填。点位 `id` 唯一,每个 `sceneId` 必须指向项目中的场景。`x`、`y` 是归一化图像坐标,范围 [0, 1],原点在左上角。`heading` 是查看器 yaw 为 0 时、从图像向上方向起算的顺时针角度;扇形实际旋转为 `heading + yaw`。多个点位可以指向同一场景,扇形画在第一个 `sceneId` 与当前场景相同的点位上。`icon` 可选;省略时使用内置标记。
288
290
 
289
- `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。图像缩放时,图标和扇形保持屏幕尺寸。
290
292
 
291
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 像素。
292
294
 
293
- `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 上,父页面滚动不会移动或改变它的尺寸。四边在给定主轴尺寸时沿该边居中。四角贴住相邻两边。收起时面板保持尺寸,沿外侧滑出:四边朝该边,四角在水平轴向时朝上或下,垂直轴向时朝左或右。拉手留在该边内侧。
294
296
 
295
- 文档保存默认值(`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` 仍只是在宿主框内藏起图面。
296
298
 
297
299
  #### 挂载 `PanoMapOverlay`
298
300
 
299
301
  在 `PanoOverlay` 中挂载,并把交给 `ProjectScenes` 的同一个 `activeSceneId` 传进去。内置 `pano-map-*` 类名来自 `@ericchen1990/pano-view/styles.css`。overlay 从查看器读取 yaw 和视场角,不会自己切换场景:在 `onSelectScene` 里设置当前场景。相机状态来自查看器之外时传入 `view`;查看器外又不传 `view` 时,图面停在 yaw `0`、pitch `0`、fov `75`。`PanoMap` 只负责图面,由宿主自己安排外框。
300
302
 
301
- `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` 挂在表面根节点上。
302
304
 
303
305
  ```tsx
304
306
  import { useState } from "react";
@@ -329,30 +331,27 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
329
331
  }
330
332
  ```
331
333
 
332
- #### 尺寸、角落与边距
334
+ #### 尺寸、对齐与边距
333
335
 
334
- - `presentation` — `"docked"` | `"dialog"`(默认取自文档,否则为 `"docked"`)。
335
- - `corner` — `"top-left"` | `"top-right"` | `"bottom-left"` | `"bottom-right"`(默认 `"bottom-left"`)。
336
- - `width` / `height` — 停靠面板尺寸,或对话框打开按钮尺寸,单位为 CSS 像素。停靠默认 220×160;打开按钮默认 48×48。
337
- - `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。
338
339
 
339
340
  ```tsx
340
341
  <PanoMapOverlay
341
342
  map={map}
342
343
  activeSceneId={activeSceneId}
343
344
  onSelectScene={setActiveSceneId}
344
- presentation="docked"
345
- corner="bottom-right"
345
+ presentation="panel"
346
+ anchor={{ align: "bottom-right", axis: "vertical", inset: 20 }}
346
347
  width={280}
347
348
  height={200}
348
- offsetX={20}
349
- offsetY={20}
350
349
  />
351
350
  ```
352
351
 
353
352
  #### 对话框模式
354
353
 
355
- 对话框模式停靠一个打开按钮,并在查看器内部打开地图。打开按钮尺寸使用 `width` / `height`(以及 `corner` 与边距)。对话框本体使用另一组尺寸:
354
+ 对话框模式放置一个打开按钮,并在查看器内部打开地图。打开按钮尺寸使用 `width` / `height`,位置使用同一个 `anchor`。对话框本体使用另一组尺寸:
356
355
 
357
356
  - `dialogWidth` / `dialogHeight` — 期望的对话框宽高,单位为 CSS 像素(默认 720×520)。两者都会按查看器可用区域夹紧,四周各留 16 像素边距。
358
357
  - `openLabel` / `closeLabel` / `title` — 打开按钮无障碍名称、关闭按钮文案与对话框标题(默认分别为 `"Map"`、`"Close"`、`"Map"`)。
@@ -365,7 +364,7 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
365
364
  activeSceneId={activeSceneId}
366
365
  onSelectScene={setActiveSceneId}
367
366
  presentation="dialog"
368
- corner="bottom-left"
367
+ anchor={{ align: "bottom-left", inset: 16 }}
369
368
  width={52}
370
369
  height={52}
371
370
  dialogWidth={640}
@@ -401,6 +400,36 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
401
400
  />
402
401
  ```
403
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
+
404
433
  #### 编辑模式
405
434
 
406
435
  `mode="edit"` 用于编辑:设置 `placementSceneId` 后点击地图会新增点位,其 `heading` 取当前 yaw 的相反数,因此在该 yaw 下扇形指向图像上方;拖动点位改变位置,拖动扇形设置 `heading`。`onEditChange` 返回 `{ spots?, selectedSpotId? }`。访客播放保持默认的 `mode="view"`。`editorSurface` 会放大编辑面板;传入 `width` / `height` 可指定尺寸,否则按查看器比例计算。
@@ -1431,19 +1460,19 @@ export function Tour() {
1431
1460
 
1432
1461
  每项需要 `id` 与 `thumbnailUrl`。可选 `title` 显示在缩略图底部标签和控制提示上;省略时用 `id`。当前项带叠在缩略图上的选择边框和 `data-active="true"`。场景名默认叠在缩略图底部的暗色半透明条上,可用 `appearance.labelAlign` 改为居中或右对齐。
1433
1462
 
1434
- ### 位置、边距与尺寸
1463
+ ### 对齐、边距与尺寸
1464
+
1465
+ `anchor` 是场景菜单和平面图共用的 `PanoOverlayAnchor`。
1435
1466
 
1436
- - `placement` — `"top"` | `"bottom"` | `"left"` | `"right"`(默认 `"bottom"`)。上下为横向列表,左右为纵向列表。
1437
- - `align` — `"start"` | `"center"` | `"end"`(默认 `"center"`)。菜单主轴尺寸小于可用空间时,沿停靠边对齐;列表内容短于视口时同样对齐。
1438
- - `inset` — 与 viewer 边缘的间隙(默认 `12`)。数字按像素;字符串可带单位(如 `"1rem"`);对象可分别设置 `top` / `right` / `bottom` / `left`。
1439
- - `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 之间撑满。四角贴住相邻两边,没给尺寸时收缩到内容。较短的列表靠向角落,在边上则居中。
1440
1471
  - `dragMotion` — 滚动惯性与边界回弹。省略则两者都开(自定义指针滚动,桌面也可拖)。设为 `false` 使用原生 overflow 滚动。可传 `{ inertia?: boolean; bounce?: boolean }` 分别开关。短按仍会选中;拖过数像素后会抑制 click。
1441
1472
 
1442
1473
  ```tsx
1443
1474
  <PanoSceneMenu
1444
- placement="bottom"
1445
- align="center"
1446
- inset={16}
1475
+ anchor={{ align: "bottom", inset: 16 }}
1447
1476
  width="min(720px, 100%)"
1448
1477
  items={menuItems}
1449
1478
  activeId={activeSceneId}
@@ -1451,9 +1480,11 @@ export function Tour() {
1451
1480
  />
1452
1481
 
1453
1482
  <PanoSceneMenu
1454
- placement="right"
1455
- align="start"
1456
- 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
+ }}
1457
1488
  height="70%"
1458
1489
  width={160}
1459
1490
  items={menuItems}
@@ -1468,7 +1499,7 @@ export function Tour() {
1468
1499
 
1469
1500
  ### 展开收起与箭头
1470
1501
 
1471
- 默认始终展开。设 `collapsible` 后会出现开关,位于缩略图条朝向画面内侧的正中(底部菜单在条上方,顶部在条下方,左右在朝向画面一侧)。收起时列表沿停靠边滑出屏幕,只把展开按钮留在安全区边缘:
1502
+ 默认始终展开。设 `collapsible` 后会出现开关,位于收起边的内侧正中。收起时列表按自身尺寸加上该边 inset 滑出屏幕,展开按钮留在查看器边缘:
1472
1503
 
1473
1504
  ```tsx
1474
1505
  <PanoSceneMenu
@@ -1497,7 +1528,7 @@ export function Tour() {
1497
1528
  showArrows
1498
1529
  loop
1499
1530
  collapsible
1500
- placement="bottom"
1531
+ anchor={{ align: "bottom" }}
1501
1532
  width="min(720px, 100%)"
1502
1533
  items={menuItems}
1503
1534
  activeId={activeSceneId}
@@ -1544,6 +1575,36 @@ export function Tour() {
1544
1575
 
1545
1576
  根节点还带 `data-placement`、`data-align`、`data-orientation`(`horizontal` | `vertical`)、`data-expanded`、`data-collapsible`;当前项带 `data-active="true"`。
1546
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
+
1547
1608
  ## PanoFilter
1548
1609
 
1549
1610
  在 `PanoViewer` 内渲染 `PanoFilter` 可对全景源(`Sphere`、`Tile`、`SphereTile`、`PanoVideo`)调色或风格化。内置预设仅作用于全景材质;`colorMatrix` 默认仅作用于全景,设 `colorMatrixScope="all"` 可同时作用于热点纹理。tooltip、视频控件与 iframe 等 DOM 叠加层不受影响。WebXR / MobileVR 使用相同的全景着色器,因此继承相同的滤镜外观。
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) {