@ericchen1990/pano-view 3.4.2 → 3.6.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 +615 -73
- package/README.zh-CN.md +583 -71
- package/dist/data.cjs +1080 -0
- package/dist/data.cjs.map +1 -0
- package/dist/data.d.mts +581 -0
- package/dist/data.d.ts +581 -0
- package/dist/data.mjs +1057 -0
- package/dist/data.mjs.map +1 -0
- package/dist/index.cjs +9120 -8431
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +904 -625
- package/dist/index.d.ts +904 -625
- package/dist/index.mjs +9273 -8578
- package/dist/index.mjs.map +1 -1
- package/dist/ui/styles.css +70 -0
- package/dist/ui.cjs +195 -0
- package/dist/ui.cjs.map +1 -0
- package/dist/ui.d.mts +63 -0
- package/dist/ui.d.ts +63 -0
- package/dist/ui.mjs +152 -0
- package/dist/ui.mjs.map +1 -0
- package/docs/dialogs.md +606 -0
- package/docs/dialogs.zh-CN.md +606 -0
- package/package.json +20 -5
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @ericchen1990/pano-view
|
|
1
|
+
# @ericchen1990/pano-view · [](./LICENSE) [](https://www.npmjs.com/package/@ericchen1990/pano-view) [](https://github.com/Eric-Chen1990/pano-view/actions/workflows/ci.yml) [](https://github.com/Eric-Chen1990/pano-view/pulls)
|
|
2
2
|
|
|
3
3
|
**English** | [简体中文](./README.zh-CN.md)
|
|
4
4
|
|
|
@@ -6,6 +6,301 @@ Composable React components for equirectangular and krpano-style multiresolution
|
|
|
6
6
|
|
|
7
7
|
Try the components in the [live playground](https://pano-view-playground.vercel.app/?utm_source=github&utm_medium=readme&utm_campaign=package-readme&utm_content=playground).
|
|
8
8
|
|
|
9
|
+
## Contents
|
|
10
|
+
|
|
11
|
+
- [pano-converter](#pano-converter)
|
|
12
|
+
- [Data sources and choosing an approach](#data-sources-and-choosing-an-approach)
|
|
13
|
+
- [Static projects and JSON data](#static-projects-and-json-data)
|
|
14
|
+
- [FAQ and troubleshooting](#faq-and-troubleshooting)
|
|
15
|
+
- [krpano-compatible tile output](#krpano-compatible-tile-output)
|
|
16
|
+
- [Install](#install)
|
|
17
|
+
- [Examples](#examples)
|
|
18
|
+
- [Exported components](#exported-components)
|
|
19
|
+
- [PanoViewer](#panoviewer)
|
|
20
|
+
- [DOM overlays and dialogs](#dom-overlays-and-dialogs)
|
|
21
|
+
- [Sphere](#sphere)
|
|
22
|
+
- [PanoVideo](#panovideo)
|
|
23
|
+
- [Tile](#tile)
|
|
24
|
+
- [Scenes](#scenes)
|
|
25
|
+
- [PanoFilter](#panofilter)
|
|
26
|
+
- [AutoRotate](#autorotate)
|
|
27
|
+
- [BackgroundAudio](#backgroundaudio)
|
|
28
|
+
- [Gyro](#gyro)
|
|
29
|
+
- [WebVR](#webvr)
|
|
30
|
+
- [Mouse, touch, and keyboard controls](#mouse-touch-and-keyboard-controls)
|
|
31
|
+
- [Panorama coordinate events](#panorama-coordinate-events)
|
|
32
|
+
- [PanoEvents](#panoevents)
|
|
33
|
+
- [PanoContextMenu](#panocontextmenu)
|
|
34
|
+
- [Shared hotspot contract and saved definitions](#shared-hotspot-contract-and-saved-definitions)
|
|
35
|
+
- [ImageHotspot](#imagehotspot)
|
|
36
|
+
- [GraphicHotspot](#graphichotspot)
|
|
37
|
+
- [Hotspot mode and zoom behaviour](#hotspot-mode-and-zoom-behaviour)
|
|
38
|
+
- [PolygonHotspot](#polygonhotspot)
|
|
39
|
+
- [PolylineHotspot](#polylinehotspot)
|
|
40
|
+
- [SequenceHotspot](#sequencehotspot)
|
|
41
|
+
- [VideoHotspot](#videohotspot)
|
|
42
|
+
- [AudioHotspot](#audiohotspot)
|
|
43
|
+
- [TextHotspot](#texthotspot)
|
|
44
|
+
- [IframeHotspot](#iframehotspot)
|
|
45
|
+
- [Next.js and SSR](#nextjs-and-ssr)
|
|
46
|
+
- [Spherical multiresolution tiles](#spherical-multiresolution-tiles)
|
|
47
|
+
- [Accessibility matrices and tooltip typography](#accessibility-matrices-and-tooltip-typography)
|
|
48
|
+
|
|
49
|
+
## pano-converter
|
|
50
|
+
|
|
51
|
+
[@ericchen1990/pano-converter](../converter/README.md) is the other package in
|
|
52
|
+
this repository. It generates cube and equirectangular tile pyramids in
|
|
53
|
+
Node.js and through the `pano-convert` CLI, then returns `viewerConfig` for
|
|
54
|
+
[`Tile`](#tile), [`SphereTile`](#spherical-multiresolution-tiles), and
|
|
55
|
+
[`Scenes`](#scenes). It does not depend on React.
|
|
56
|
+
|
|
57
|
+
## Data sources and choosing an approach
|
|
58
|
+
|
|
59
|
+
The viewer accepts configuration objects or loads JSON over HTTP. It does not connect to a database: your host backend owns database connections, queries, authorization and response mapping. A database-backed API can also return JSON; the distinction is static files versus dynamically supplied configuration, not the JSON format itself. Preview images, tiles and media can use object storage/CDN with either approach.
|
|
60
|
+
|
|
61
|
+
| Consideration | Static JSON + CDN | Database + backend API |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| Configuration reads | No database queries; storage, request and traffic costs still apply | Requests that reach the backend need processing and may query the database; caching can reduce that work |
|
|
64
|
+
| Updates | Generate and publish files, then manage entry references and caches | No static-file publication is required; freshness still depends on backend and client caches |
|
|
65
|
+
| Access and personalization | Public files suit public content; private delivery needs hosting/CDN access control | Backend can authorize users/organizations and return different configurations |
|
|
66
|
+
| Availability and maintenance | Depends on static hosting/CDN; maintain the publication and cache-update process | Depends on the API and its database/cache services; maintain queries, authorization and cache invalidation |
|
|
67
|
+
| Best fit | Public, published tours; infrequent updates; high read traffic | Editing previews, private content, frequent updates and personalization |
|
|
68
|
+
|
|
69
|
+
**Prefer JSON + CDN for public, published tours; prefer database + API for editing previews, private content and personalization.** If both are needed, use the database as the editing source and generate a JSON snapshot on publication. That hybrid approach needs an application-owned publication process; the package does not generate or synchronize snapshots for you.
|
|
70
|
+
|
|
71
|
+
Neither path automatically polls or subscribes to configuration changes. To update an already-open page, the host must reload the configuration and pass the new data to the viewer. An uncached API read can return current database values, but does not make an existing viewer update automatically.
|
|
72
|
+
|
|
73
|
+
### Database-backed configuration
|
|
74
|
+
|
|
75
|
+
In your backend, query and authorize the tour, then map only the intended configuration fields into a project. The application-owned `readAuthorizedTour` below performs that database read and access check; it is not a package API. Its scene records contain the compact `viewerConfig` saved from conversion, asset directories, views and hotspot definitions:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { loadPanoProject } from "@ericchen1990/pano-view/data";
|
|
79
|
+
import { readAuthorizedTour } from "./tour-repository";
|
|
80
|
+
|
|
81
|
+
export async function loadTourFromDatabase(tourId: string) {
|
|
82
|
+
const tour = await readAuthorizedTour(tourId);
|
|
83
|
+
return loadPanoProject({
|
|
84
|
+
schemaVersion: 1,
|
|
85
|
+
initialSceneId: tour.initialSceneId,
|
|
86
|
+
scenes: tour.scenes.map((scene) => ({
|
|
87
|
+
id: scene.id,
|
|
88
|
+
title: scene.title,
|
|
89
|
+
source: {
|
|
90
|
+
viewerConfig: scene.viewerConfig,
|
|
91
|
+
baseUrl: scene.assetBaseUrl,
|
|
92
|
+
},
|
|
93
|
+
initialView: scene.initialView,
|
|
94
|
+
hotspots: scene.hotspots,
|
|
95
|
+
})),
|
|
96
|
+
}, { baseUrl: tour.assetBaseUrl });
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Object input requires `baseUrl`: here `tour.assetBaseUrl` is an absolute directory URL such as `https://cdn.example.com/tours/123/v12/`. Project-relative hotspot URLs resolve against that directory; each scene's optional `source.baseUrl` overrides its panorama tile/preview directory. With every `source.viewerConfig` inline, this call makes no project/viewer JSON requests; rendering still loads previews, tiles and media. On the server, import data helpers from `@ericchen1990/pano-view/data`.
|
|
101
|
+
|
|
102
|
+
Pass the resulting `LoadedPanoProject` to a viewer component; in Next.js, keep the viewer inside a client boundary:
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
"use client";
|
|
106
|
+
|
|
107
|
+
import {
|
|
108
|
+
PanoViewer, ProjectScenes, type LoadedPanoProject,
|
|
109
|
+
} from "@ericchen1990/pano-view";
|
|
110
|
+
|
|
111
|
+
export function DatabaseTour({ project }: { project: LoadedPanoProject }) {
|
|
112
|
+
return <PanoViewer style={{ height: 600 }}>
|
|
113
|
+
<ProjectScenes project={project} transition="dissolve" />
|
|
114
|
+
</PanoViewer>;
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Alternatively, return a `PanoProjectDefinition` from an authorized JSON endpoint and use `loadPanoProject("https://api.example.com/tours/123", { baseUrl: "https://cdn.example.com/tours/123/v12/" })`; the URL need not end in `.json`. A single scene can use `sceneFromViewerConfig` with an existing config or `loadSceneFromJson` with an API returning a `schemaVersion: 1` viewer document, then render through `Scenes`. See [Static projects and JSON data](#static-projects-and-json-data) for request options and error handling. Protect private images/media separately: JSON request credentials are not forwarded to texture requests.
|
|
119
|
+
|
|
120
|
+
### Caching and publishing updates
|
|
121
|
+
|
|
122
|
+
`project.json` (scene structure, views and hotspots), each `viewer.json` (panorama rendering configuration), and previews/tiles/media are separate cached resources. Refreshing the project alone will not refresh an unchanged scene URL or an image at the same URL. Database-backed APIs can also be cached by the backend, CDN and client, so cache policy matters for both approaches.
|
|
123
|
+
|
|
124
|
+
- **Prefer versioned directories:** publish a complete release under a new path such as `/tours/123/v12/`, including its `project.json`, scene configs and assets, before switching the page or entry reference. Version changed assets too, and retain old assets while old configurations may still be in use. Immutable versioned files can use long-lived caching; a mutable page or pointer that selects the version needs short caching or revalidation. AWS also [recommends versioned file or directory names](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/UpdatingExistingObjects.html).
|
|
125
|
+
- **When overwriting the same URL:** wait for its configured cache lifetime to expire, require revalidation, or invalidate the affected CDN paths after uploading. CDN invalidation does not clear files already cached in browsers; a newly configured response header cannot retroactively replace an already-fresh cached response. Versioned paths avoid depending on those copies expiring. Query strings such as `?v=12` work only if the CDN includes them in its cache key; JSON query parameters are not inherited by referenced assets.
|
|
126
|
+
- **Choose headers for the resource:** `Cache-Control: no-cache` permits storage but requires validation before reuse; `no-store` tells caches not to store the response. A mutable public entry can use revalidation with validators such as `ETag`, while private/user-specific API responses need an explicit policy such as `private, no-store` and must not be shared between users. Configure hosting/CDN rules to honor the intended policy. See [MDN HTTP caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching) and [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control).
|
|
127
|
+
|
|
128
|
+
These headers and CDN operations belong to your hosting/backend setup. The loader's `requestInit` supports only `headers`, `credentials` and `signal`; it does not expose Fetch's `cache` option, invalidate CDNs or refresh an already-loaded project automatically.
|
|
129
|
+
|
|
130
|
+
## Static projects and JSON data
|
|
131
|
+
|
|
132
|
+
No database is required. The converter writes `manifest.json` for archiving/reconstruction and a small `viewer.json` for rendering. Your application authors `project.json` to connect scenes, views and hotspots. Do not pass the full manifest directly to `Scenes`.
|
|
133
|
+
|
|
134
|
+
Deploy this `project.json`, both panorama output directories and your React app as static files:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"schemaVersion": 1,
|
|
139
|
+
"initialSceneId": "entrance",
|
|
140
|
+
"scenes": [
|
|
141
|
+
{
|
|
142
|
+
"id": "entrance",
|
|
143
|
+
"source": { "viewerUrl": "./panoramas/entrance/viewer.json" },
|
|
144
|
+
"initialView": { "yaw": 0, "pitch": 0, "fov": 75 },
|
|
145
|
+
"hotspots": [{
|
|
146
|
+
"id": "to-hall", "type": "graphic", "label": "Enter hall",
|
|
147
|
+
"position": { "yaw": 30, "pitch": -10 },
|
|
148
|
+
"graphic": { "kind": "arrow" },
|
|
149
|
+
"action": { "type": "scene", "sceneId": "hall" }
|
|
150
|
+
}]
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"id": "hall",
|
|
154
|
+
"source": { "viewerUrl": "./panoramas/hall/viewer.json" },
|
|
155
|
+
"hotspots": [{
|
|
156
|
+
"id": "caption", "type": "text",
|
|
157
|
+
"position": { "yaw": 0, "pitch": 5 }, "text": "Welcome"
|
|
158
|
+
}]
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`source` must contain exactly one of `viewerUrl` or inline `viewerConfig`; optional `baseUrl` overrides panorama assets for that scene. `title`, `initialView` and `hotspots` are optional. The first scene is the default initial scene. Scene IDs must be unique, hotspot IDs unique within a scene, and scene-action targets must exist.
|
|
165
|
+
|
|
166
|
+
`loadPanoProject` loads metadata with at most four concurrent requests and rejects the whole project if one scene fails. It does not preload every texture. Its `LoadedPanoProject` contains `initialSceneId` and `scenes`; each entry has a runtime `scene`, title, initial view and hotspots with resolved asset URLs. Pass it to `ProjectScenes`:
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
import { useEffect, useState } from "react";
|
|
170
|
+
import {
|
|
171
|
+
PanoViewer, ProjectScenes, loadPanoProject, PanoDataError,
|
|
172
|
+
type LoadedPanoProject,
|
|
173
|
+
} from "@ericchen1990/pano-view";
|
|
174
|
+
|
|
175
|
+
export function Tour() {
|
|
176
|
+
const [project, setProject] = useState<LoadedPanoProject>();
|
|
177
|
+
const [error, setError] = useState<string>();
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
const controller = new AbortController();
|
|
180
|
+
loadPanoProject("/tour/project.json", {
|
|
181
|
+
requestInit: { signal: controller.signal },
|
|
182
|
+
}).then((result) => {
|
|
183
|
+
if (!controller.signal.aborted) setProject(result);
|
|
184
|
+
}).catch((cause: unknown) => {
|
|
185
|
+
if (!controller.signal.aborted) {
|
|
186
|
+
setError(cause instanceof PanoDataError
|
|
187
|
+
? `${cause.code}: ${cause.message}` : String(cause));
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
return () => controller.abort();
|
|
191
|
+
}, []);
|
|
192
|
+
if (error) return <p role="alert">{error}</p>;
|
|
193
|
+
if (!project) return <p role="status">Loading tour…</p>;
|
|
194
|
+
return <PanoViewer style={{ height: 600 }}>
|
|
195
|
+
<ProjectScenes project={project} transition="dissolve" />
|
|
196
|
+
</PanoViewer>;
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`ProjectScenes` belongs inside `PanoViewer` and reuses all nine existing hotspot types. A `scene` action navigates on click. Without an action, audio/video/sequence hotspots toggle playback. Media unmounts on leaving a scene and starts from its JSON settings on re-entry; browser autoplay restrictions still apply.
|
|
201
|
+
|
|
202
|
+
Configured `initialView` values apply at `onSceneReady`, defaulting missing fields to yaw 0, pitch 0, fov 75. With no view configured, the current view is retained. Hotspots hide during transitions. Control selection with `activeSceneId` / `onActiveSceneIdChange`. Call `event.preventDefault()` in `onHotspotClick` to cancel the default action. `onHotspotError` receives the scene, hotspot and original error (media errors may be events containing an `error` field). `renderHotspot(hotspot, context)` returns `undefined` for the default renderer or `null` to hide an item. Context includes `sceneId`, `playing`, `onClick`, `togglePlayback`, `reportError` and `renderDefault`.
|
|
203
|
+
|
|
204
|
+
### Hotspot actions and shared popup content
|
|
205
|
+
|
|
206
|
+
`ProjectHotspotAction` is a discriminated union. Keep `{ type: "scene", sceneId }` for navigation. Use `{ type: "view", view: { yaw, pitch, fov }, duration?: number }` to animate the current scene (700 ms by default, 0 for immediate movement), or `{ type: "popup", popupId }` to request shared content. Narrow `action.type` before reading `sceneId`. Explicit actions take priority over media playback and can all be cancelled with `onHotspotClick(event).preventDefault()`.
|
|
207
|
+
|
|
208
|
+
Add an optional project-level `popups: ProjectPopupDefinition[]`. Each entry has a unique `id`, a nonempty `title`, and ordered `blocks` with unique IDs within that popup:
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"id": "story",
|
|
213
|
+
"title": "About this place",
|
|
214
|
+
"blocks": [
|
|
215
|
+
{ "id": "intro", "type": "text", "text": "Plain text, including line breaks." },
|
|
216
|
+
{ "id": "photo", "type": "image", "src": "media/photo.webp", "alt": "The main hall" },
|
|
217
|
+
{ "id": "film", "type": "video", "src": "media/movie.mp4", "poster": "media/photo.webp" },
|
|
218
|
+
{ "id": "voice", "type": "audio", "src": "media/story.mp3" }
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Store this content structure in `project.json`; store media as separate files. Popup paths resolve from the project directory, independently of each panorama's `viewer.json`. `loadPanoProject` returns resolved `popups`; its optional `resolvePopupAssetUrl(path, context)` receives `{ defaultUrl, popupId, blockId, field }`, where field is `src` or `poster`. Like the existing resolver, return an absolute URL or site-root path. Existing `resolveAssetUrl` callbacks continue to receive only their scene/hotspot resources. `resolveProjectPopupAssets` is also exported from `/data` for custom loaders. Request credentials for JSON do not automatically protect media.
|
|
224
|
+
|
|
225
|
+
`ProjectScenes.onPopupRequest({ sceneId, hotspot, popup })` delegates display to your overlay. A missing handler or missing popup reports through `onHotspotError`. The optional UI entry supplies `ProjectPopupDialog` without adding Dialog dependencies to the viewer core:
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
import { useState } from "react";
|
|
229
|
+
import { PanoViewer, ProjectScenes, type LoadedPanoProject, type ProjectPopupDefinition } from "@ericchen1990/pano-view";
|
|
230
|
+
import { ProjectPopupDialog } from "@ericchen1990/pano-view/ui";
|
|
231
|
+
import "@ericchen1990/pano-view/ui/styles.css";
|
|
232
|
+
|
|
233
|
+
export function Tour({ project }: { project: LoadedPanoProject }) {
|
|
234
|
+
const [popup, setPopup] = useState<ProjectPopupDefinition | null>(null);
|
|
235
|
+
return <PanoViewer renderOverlay={() => (
|
|
236
|
+
<ProjectPopupDialog popup={popup} onOpenChange={(open) => { if (!open) setPopup(null); }} />
|
|
237
|
+
)}>
|
|
238
|
+
<ProjectScenes project={project} onPopupRequest={({ popup }) => setPopup(popup)}
|
|
239
|
+
onActiveSceneIdChange={() => setPopup(null)} />
|
|
240
|
+
</PanoViewer>;
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`ProjectPopupDialog` accepts `popup` (or `null` to close), `onOpenChange`, optional `closeLabel` and `onMediaError(block)`. It renders text as text, uses native audio/video controls without autoplay, and releases media when content closes or changes. It inherits `PanoDialog` fullscreen, sizing and focus behavior. Replace this component with your own overlay to customize rendering.
|
|
245
|
+
|
|
246
|
+
The schema remains version 1 and old projects remain valid. Projects using new actions require a viewer that supports them. `parsePanoProject(input, { draft: true })` is for authoring only: it permits empty scene lists and unfinished popup titles/media. Loading, saving and package export validate strictly. Host applications may save the same document in a database or map it to tables; the viewer has no database connection.
|
|
247
|
+
|
|
248
|
+
For individual panoramas and existing configs use the pure data entry. Next.js viewer components need a client boundary; server-side data loading uses `/data`:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
import {
|
|
252
|
+
sceneFromViewerConfig, loadSceneFromJson, loadPanoProject,
|
|
253
|
+
} from "@ericchen1990/pano-view/data";
|
|
254
|
+
|
|
255
|
+
// A JSON API and a different asset/CDN origin.
|
|
256
|
+
const scene = await loadSceneFromJson(
|
|
257
|
+
"https://api.example.com/rooms/123/viewer",
|
|
258
|
+
{ id: "room", baseUrl: "https://cdn.example.com/rooms/123/" },
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// Existing Node.js conversion results need no extra JSON request.
|
|
262
|
+
const inlineScene = sceneFromViewerConfig(result.viewerConfig, {
|
|
263
|
+
id: "room", baseUrl: "https://cdn.example.com/rooms/123/",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Use source.viewerConfig instead of source.viewerUrl to inline scene data.
|
|
267
|
+
const project = await loadPanoProject({
|
|
268
|
+
schemaVersion: 1,
|
|
269
|
+
scenes: [{ id: "room", source: { viewerConfig: result.viewerConfig } }],
|
|
270
|
+
}, { baseUrl: "https://cdn.example.com/rooms/123/" });
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### URLs and upload mapping
|
|
274
|
+
|
|
275
|
+
| Reference | Default base |
|
|
276
|
+
| --- | --- |
|
|
277
|
+
| Project `viewerUrl`, hotspot assets, `source.baseUrl` | Project directory |
|
|
278
|
+
| Tiles and preview in external `viewer.json` | That JSON directory |
|
|
279
|
+
| Tiles and preview in inline config | Project directory |
|
|
280
|
+
| Panorama with `source.baseUrl` | Specified directory |
|
|
281
|
+
|
|
282
|
+
URL inputs use the final response directory after redirects. Object inputs require `baseUrl`; it can also override the project directory. Directory trailing slashes are normalized. Domains and cloud prefixes stay outside converter JSON. Browsers accept relative entry URLs; Node.js requires absolute HTTP(S) entry URLs. Full resource URLs remain intact; leading `/` paths resolve against the resource base origin.
|
|
283
|
+
|
|
284
|
+
If uploads rename files or scatter directories, provide synchronous `resolveAssetUrl(path, context)`. Context has `defaultUrl`, `kind` (`tile`, `preview`, `hotspot`), `sceneId` and optional `hotspotId`. Return a final absolute URL or site-root path; it is not joined again. `uploadedUrls` below is application-owned:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
const project = await loadPanoProject("/tour/project.json", {
|
|
288
|
+
resolveAssetUrl(path, context) {
|
|
289
|
+
// Map keys must distinguish scenes and, for hotspot assets, hotspot IDs.
|
|
290
|
+
const key = `${context.sceneId}/${context.hotspotId ?? ""}/${path}`;
|
|
291
|
+
return uploadedUrls[key] ?? context.defaultUrl;
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Hotspot mapping covers images, SVG image URLs, audio source arrays, video posters, playback icons and tooltip images; text, CSS and SVG path data remain unchanged. Acquire signed URLs ahead of time in your application and return them synchronously.
|
|
297
|
+
|
|
298
|
+
`requestInit` accepts only `headers`, `credentials` and `signal`, for JSON requests only. They are not forwarded to textures, and JSON query parameters are not inherited by assets. `parsePanoProject(unknown)` validates and copies project data independently. `sceneFromViewerConfig` accepts the unwrapped compact config; `loadSceneFromJson` accepts a versioned `viewer.json`.
|
|
299
|
+
|
|
300
|
+
`PanoDataError` exposes stable codes: `INVALID_OPTIONS`, `FETCH_FAILED`, `HTTP_ERROR`, `INVALID_JSON`, `INVALID_CONFIG`, `UNSUPPORTED_SCHEMA_VERSION`, `UNSUPPORTED_PROJECTION`, `INVALID_PROJECT`, `INVALID_REFERENCE`, `ABORTED`. HTTP errors carry `status`; project errors include `sceneId` and `field` where available. Successful loading means metadata is ready, not that textures have loaded; use `onSceneReady` for render readiness.
|
|
301
|
+
|
|
302
|
+
See the `/project` playground route and `public/fixtures/projects/tour/project.json` for a complete static example.
|
|
303
|
+
|
|
9
304
|
## krpano-compatible tile output
|
|
10
305
|
|
|
11
306
|
Use this package when you want a React-native viewer around equirectangular
|
|
@@ -21,17 +316,44 @@ not affiliated with krpano.
|
|
|
21
316
|
|
|
22
317
|
### Package
|
|
23
318
|
|
|
319
|
+
<details name="pano-view-install" open>
|
|
320
|
+
<summary>npm</summary>
|
|
321
|
+
|
|
24
322
|
```bash
|
|
25
323
|
npm install @ericchen1990/pano-view
|
|
26
324
|
```
|
|
27
325
|
|
|
28
|
-
|
|
326
|
+
</details>
|
|
327
|
+
<details name="pano-view-install">
|
|
328
|
+
<summary>pnpm</summary>
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
pnpm add @ericchen1990/pano-view
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
</details>
|
|
335
|
+
<details name="pano-view-install">
|
|
336
|
+
<summary>yarn</summary>
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
yarn add @ericchen1990/pano-view
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
</details>
|
|
343
|
+
<details name="pano-view-install">
|
|
344
|
+
<summary>bun</summary>
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
bun add @ericchen1990/pano-view
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
</details>
|
|
29
351
|
|
|
30
352
|
### Peer dependencies
|
|
31
353
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
354
|
+
The **host application** must provide compatible versions of these packages.
|
|
355
|
+
Automatic peer-dependency installation depends on the package manager and its
|
|
356
|
+
settings. Install any missing peers explicitly using the ranges below.
|
|
35
357
|
|
|
36
358
|
| Package | Supported range |
|
|
37
359
|
| --- | --- |
|
|
@@ -45,16 +367,17 @@ this library. npm 7+ and pnpm install them automatically; Yarn Classic and npm
|
|
|
45
367
|
Keep a **single copy** of React, Three.js, and the React Three Fiber packages
|
|
46
368
|
in the host app. Duplicate `three` trees are a common cause of blank canvases
|
|
47
369
|
and context errors (`R3F: Hooks can only be used within the Canvas component`).
|
|
48
|
-
If that happens, check `npm ls three
|
|
370
|
+
If that happens, check `npm ls three`, `pnpm why three`, `yarn why three`, or
|
|
371
|
+
`bun pm ls` and dedupe.
|
|
49
372
|
|
|
50
373
|
### Stylesheet (HTML chrome)
|
|
51
374
|
|
|
52
375
|
Import the shipped stylesheet once in the host app. The WebGL panorama
|
|
53
376
|
(`PanoViewer`, `Sphere`, `Tile`, 3D hotspots) renders without it. Built-in
|
|
54
|
-
**HTML** overlays
|
|
377
|
+
**HTML** overlays depend on its prefixed `pano-*` classes for layout.
|
|
55
378
|
|
|
56
379
|
Those overlays include video controls, captions, the default context menu,
|
|
57
|
-
hotspot tooltips,
|
|
380
|
+
hotspot tooltips, controls for assistive technology, and WebVR session UI.
|
|
58
381
|
|
|
59
382
|
```ts
|
|
60
383
|
import "@ericchen1990/pano-view/styles.css";
|
|
@@ -70,11 +393,14 @@ Skip this import only if you do not use those overlays, or if you restyle
|
|
|
70
393
|
them entirely through `className`, `style`, and appearance props. Without
|
|
71
394
|
the stylesheet, the overlays still mount, but they render unstyled.
|
|
72
395
|
|
|
73
|
-
##
|
|
396
|
+
## Examples
|
|
74
397
|
|
|
75
398
|
Give PanoViewer an explicit size and render one panorama source inside it.
|
|
76
399
|
Import the stylesheet once when you use built-in HTML overlays such as video
|
|
77
|
-
controls, captions, tooltips, the context menu, or WebVR UI.
|
|
400
|
+
controls, captions, tooltips, the context menu, or WebVR UI. Each example
|
|
401
|
+
below shows a basic setup; the linked section has the full props.
|
|
402
|
+
|
|
403
|
+
### Equirectangular image
|
|
78
404
|
|
|
79
405
|
~~~tsx
|
|
80
406
|
import "@ericchen1990/pano-view/styles.css";
|
|
@@ -89,9 +415,78 @@ export function Panorama() {
|
|
|
89
415
|
}
|
|
90
416
|
~~~
|
|
91
417
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
418
|
+
`previewUrl` must be a low-resolution 2:1 image shown while the full image
|
|
419
|
+
loads. See [Sphere](#sphere).
|
|
420
|
+
|
|
421
|
+
### Cube tiles
|
|
422
|
+
|
|
423
|
+
Copy `url`, `multires`, and the preview path from krpano XML. Read the
|
|
424
|
+
[migration guide](../../docs/krpano-migration.md) before replacing Sphere with
|
|
425
|
+
Tile.
|
|
426
|
+
|
|
427
|
+
~~~tsx
|
|
428
|
+
import { PanoViewer, Tile } from "@ericchen1990/pano-view";
|
|
429
|
+
|
|
430
|
+
export function TilePanorama() {
|
|
431
|
+
return (
|
|
432
|
+
<PanoViewer style={{ height: 560 }}>
|
|
433
|
+
<Tile
|
|
434
|
+
baseUrl="/panoramas/room"
|
|
435
|
+
urlTemplate="tiles/%s/l%l/%v/l%l_%s_%v_%h.jpg"
|
|
436
|
+
multires="512,1000,2000"
|
|
437
|
+
previewUrl="preview.jpg"
|
|
438
|
+
/>
|
|
439
|
+
</PanoViewer>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
~~~
|
|
443
|
+
|
|
444
|
+
Placeholders and preview face order are in [Tile](#tile).
|
|
445
|
+
|
|
446
|
+
### 360 video
|
|
447
|
+
|
|
448
|
+
~~~tsx
|
|
449
|
+
import { PanoViewer, PanoVideo } from "@ericchen1990/pano-view";
|
|
450
|
+
|
|
451
|
+
export function TourVideo() {
|
|
452
|
+
return (
|
|
453
|
+
<PanoViewer style={{ height: 560 }}>
|
|
454
|
+
<PanoVideo autoPlay muted src="/tour.mp4" />
|
|
455
|
+
</PanoViewer>
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
~~~
|
|
459
|
+
|
|
460
|
+
Variants, captions, and custom controls are in [PanoVideo](#panovideo).
|
|
461
|
+
|
|
462
|
+
### Multiple scenes
|
|
463
|
+
|
|
464
|
+
~~~tsx
|
|
465
|
+
import { useState } from "react";
|
|
466
|
+
import { PanoViewer, Scenes, type Scene } from "@ericchen1990/pano-view";
|
|
467
|
+
|
|
468
|
+
const scenes: Scene[] = [
|
|
469
|
+
{ id: "lobby", type: "sphere", src: "/lobby.webp", previewUrl: "/lobby-preview.webp" },
|
|
470
|
+
{ id: "roof", type: "sphere", src: "/roof.webp", previewUrl: "/roof-preview.webp" },
|
|
471
|
+
];
|
|
472
|
+
|
|
473
|
+
export function Tour() {
|
|
474
|
+
const [activeSceneId, setActiveSceneId] = useState("lobby");
|
|
475
|
+
|
|
476
|
+
return (
|
|
477
|
+
<PanoViewer style={{ height: 560 }}>
|
|
478
|
+
<Scenes
|
|
479
|
+
activeSceneId={activeSceneId}
|
|
480
|
+
onActiveSceneIdChange={setActiveSceneId}
|
|
481
|
+
scenes={scenes}
|
|
482
|
+
transition="dissolve"
|
|
483
|
+
/>
|
|
484
|
+
</PanoViewer>
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
~~~
|
|
488
|
+
|
|
489
|
+
Presets and hotspot rendering are in [Scenes](#scenes).
|
|
95
490
|
|
|
96
491
|
## Exported components
|
|
97
492
|
|
|
@@ -99,9 +494,10 @@ guide documents the XML-to-prop mapping and the relative-path rules.
|
|
|
99
494
|
|
|
100
495
|
- [`PanoViewer`](#panoviewer) — canvas shell, camera, default controls, and imperative view API
|
|
101
496
|
- [`Sphere`](#sphere) — single 2:1 equirectangular image
|
|
497
|
+
- [`SphereTile`](#spherical-multiresolution-tiles) — multiresolution equirectangular tiles
|
|
102
498
|
- [`PanoVideo`](#panovideo) — 2:1 equirectangular video on the panorama sphere
|
|
103
499
|
- [`Tile`](#tile) — krpano-style multires cube tiles
|
|
104
|
-
- [`Scenes`](#scenes) —
|
|
500
|
+
- [`Scenes`](#scenes) — multi-scene switching in controlled or uncontrolled mode
|
|
105
501
|
- [`BackgroundAudio`](#backgroundaudio) — one shared track, or a different track per scene
|
|
106
502
|
- [`PanoFilter`](#panofilter) — color and artistic filters on the panorama source
|
|
107
503
|
|
|
@@ -141,11 +537,27 @@ guide documents the XML-to-prop mapping and the relative-path rules.
|
|
|
141
537
|
- Context menu helpers — `createPanoContextMenuPresets`, `composePanoContextMenuItems`, and related APIs ([`PanoContextMenu`](#panocontextmenu))
|
|
142
538
|
- Polygon / polyline validation — `validatePolygonVertices`, `validatePolylineVertices`, `unwrapPolygonVertices`
|
|
143
539
|
|
|
540
|
+
## DOM overlays and dialogs
|
|
541
|
+
|
|
542
|
+
Use `PanoViewer.renderOverlay` for ordinary DOM UI outside the Three.js tree. `usePanoOverlay()` supplies its stable Portal target, measured viewer/visible dimensions and fullscreen state. The optional `/ui` entry provides `PanoDialog` and composable parts; your application can also integrate an existing Dialog.
|
|
543
|
+
|
|
544
|
+
See the separate **[Dialog and styling guide](./docs/dialogs.md)** for complete examples, CSS variable defaults and placement, custom layouts, third-party integration, nested portals, lifecycle, API tables and troubleshooting. The guide is included in the npm package. Run `pnpm dev` and open `/overlays` for the local examples.
|
|
545
|
+
|
|
546
|
+
### Default dialog
|
|
547
|
+
|
|
548
|
+
Start with [Quick start](./docs/dialogs.md#quick-start), then [customize the default appearance](./docs/dialogs.md#customize-the-default-appearance). Default styling is an optional import from `@ericchen1990/pano-view/ui/styles.css`.
|
|
549
|
+
|
|
550
|
+
For multiple hotspots, use the [complete scene/hotspot-to-content example](./docs/dialogs.md#choose-content-by-hotspot): five content definitions, one shared dialog, matching project JSON and scene-local IDs.
|
|
551
|
+
|
|
552
|
+
### Compose parts or integrate an existing Dialog
|
|
553
|
+
|
|
554
|
+
Use [composable parts](./docs/dialogs.md#compose-a-different-layout) for a custom header/body/footer, or [connect an existing Dialog](./docs/dialogs.md#use-an-existing-dialog) through the local Portal container and `usePanoModal(open)`. Fullscreen belongs in viewer-level UI, not in dialog content.
|
|
555
|
+
|
|
144
556
|
## PanoViewer
|
|
145
557
|
|
|
146
558
|
`PanoViewer` owns the canvas, perspective camera, and controls. Give its container an explicit size and place one panorama source inside it.
|
|
147
559
|
|
|
148
|
-
Angles
|
|
560
|
+
Angles in the public API are measured in degrees. Positive yaw looks right and positive pitch looks up.
|
|
149
561
|
|
|
150
562
|
```tsx
|
|
151
563
|
import "@ericchen1990/pano-view/styles.css";
|
|
@@ -214,13 +626,13 @@ which use their spherical center. Hotspot ids must be unique within a viewer.
|
|
|
214
626
|
|
|
215
627
|
**Fullscreen** — `enterFullscreen`, `exitFullscreen`, `toggleFullscreen`, `isFullscreen`.
|
|
216
628
|
|
|
217
|
-
**Scenes** (requires `<Scenes />`) — `setScene(id)`, `nextScene`, `previousScene`, `getActiveSceneId`, `getSceneIds`, `isSceneTransitioning`. `Scenes`
|
|
629
|
+
**Scenes** (requires `<Scenes />`) — `setScene(id)`, `nextScene`, `previousScene`, `getActiveSceneId`, `getSceneIds`, `isSceneTransitioning`. `Scenes` accepts `defaultActiveSceneId` (uncontrolled) or `activeSceneId` (controlled). In controlled mode imperative calls invoke `onActiveSceneIdChange`.
|
|
218
630
|
|
|
219
631
|
**WebVR** (requires `<WebVR />`) — `enterVR`, `exitVR`, `toggleVR`, `isVRAvailable`, `isVREnabled`, `getVRMode`, `requestVRPermission`.
|
|
220
632
|
|
|
221
|
-
**Video** (requires `<PanoVideo />`) — `getVideo` returns the `PanoVideoController`, `subscribeVideo` subscribes to
|
|
633
|
+
**Video** (requires `<PanoVideo />`) — `getVideo` returns the `PanoVideoController`, `subscribeVideo` subscribes to video controller and playback-state changes, plus convenience aliases `playVideo`, `pauseVideo`, `toggleVideo`, `seekVideo`, `setVideoVolume`, `setVideoMuted`, `toggleVideoMuted`.
|
|
222
634
|
|
|
223
|
-
**Background Audio** (requires `<BackgroundAudio />`) — `getBackgroundAudio` returns the `BackgroundAudioController`, `subscribeBackgroundAudio` subscribes to
|
|
635
|
+
**Background Audio** (requires `<BackgroundAudio />`) — `getBackgroundAudio` returns the `BackgroundAudioController`, `subscribeBackgroundAudio` subscribes to background-audio controller and playback-state changes, plus convenience aliases `playBackgroundAudio`, `pauseBackgroundAudio`, `toggleBackgroundAudio`, `setBackgroundAudioVolume`, `setBackgroundAudioMuted`, `toggleBackgroundAudioMuted`. `BackgroundAudio` accepts `playing` (controlled) or `defaultPlaying` (uncontrolled).
|
|
224
636
|
|
|
225
637
|
### Mobile media activation
|
|
226
638
|
|
|
@@ -242,7 +654,7 @@ By default, when a child has an initial playback intent (`PanoVideo autoPlay`, a
|
|
|
242
654
|
|
|
243
655
|
The `media` argument provides `resumeAudio()`, `playVideo({ unmute })`, and `playBackgroundAudio()`. Controlled background audio remains host-owned: update its `playing` state from this callback. `viewer.activateMedia()` lets a custom control reuse the same path.
|
|
244
656
|
|
|
245
|
-
Methods that depend on an unmounted child component (e.g. `enterVR` without `<WebVR />`)
|
|
657
|
+
Methods that depend on an unmounted child component (e.g. `enterVR` without `<WebVR />`) perform no action and return `false`, `undefined`, `null`, or a corresponding Promise, depending on the method.
|
|
246
658
|
|
|
247
659
|
### Fullscreen
|
|
248
660
|
|
|
@@ -467,7 +879,7 @@ function TourPlayer() {
|
|
|
467
879
|
|
|
468
880
|
The two deprecated auto-rotation handle methods (`startAutoRotate`, `stopAutoRotate`) remain for compatibility. Mouse, touch, and keyboard input are enabled by default — you do not need to render control components for ordinary viewing. Render `MouseControls`, `TouchControls`, or `KeyboardControls` only to override properties (the child replaces that channel's default instance). Tune shared behaviour through `controls` (`inertia`, `invert`, `bouncingLimits`, `fovSpeed`, `frictionStop`, `rotateDamping`, `zoomDamping`, and top-level `rotateSpeed` / `zoomSpeed`). Disable a channel with `controls.mouse` / `touch` / `keyboard` set to `false`, or pass an options object (including `enabled`) to override defaults without mounting a child. Auto-rotation is off until you render `AutoRotate`. A default context menu (Reset view / Enter fullscreen) is also mounted — see [`PanoContextMenu`](#panocontextmenu).
|
|
469
881
|
|
|
470
|
-
Drag and zoom update a target view that the camera follows smoothly. `rotateDamping` and `zoomDamping` control that following speed in seconds^-1 (defaults: `14` and `16` respectively);
|
|
882
|
+
Drag and zoom update a target view that the camera follows smoothly. `rotateDamping` and `zoomDamping` control that following speed in seconds^-1 (defaults: `14` and `16` respectively); smaller positive values make the camera approach the target more gradually, while `0` disables smoothing for that axis. Both values must be non-negative finite numbers. Imperative `setView()` and `reset()` remain immediate.
|
|
471
883
|
|
|
472
884
|
The canvas cursor defaults to `grab`, becomes `grabbing` while dragging with the mouse, `pointer` over an interactive hotspot, and `move` while dragging a hotspot. Override those values with `cursors` (`default`, `dragging`, `hotspot`, `hotspotDragging`), or pass `cursors={false}` to leave the canvas cursor unchanged. A hotspot may set `cursor` to replace the hover cursor for that hotspot only.
|
|
473
885
|
|
|
@@ -489,7 +901,20 @@ The canvas cursor defaults to `grab`, becomes `grabbing` while dragging with the
|
|
|
489
901
|
|
|
490
902
|
`Sphere` expects a 2:1 equirectangular image. Use `yawOffset` when the source's forward direction needs horizontal adjustment.
|
|
491
903
|
|
|
904
|
+
`Sphere` loads the complete `src` image as one texture. High-resolution images
|
|
905
|
+
increase download, decoding, and GPU texture memory costs. Exceeding a device's
|
|
906
|
+
texture-size or memory limits can cause loading failures or WebGL context loss;
|
|
907
|
+
memory exhaustion can also crash the page. The current spherical renderer may
|
|
908
|
+
still show visible artifacts near the top and bottom poles.
|
|
909
|
+
|
|
910
|
+
Prefer cube tiles ([`Tile`](#tile)) for high-resolution panoramas, generated
|
|
911
|
+
with [@ericchen1990/pano-converter](../converter/README.md). The viewer loads
|
|
912
|
+
required tile levels by view, avoiding one oversized texture. For existing
|
|
913
|
+
equirectangular tiles, use [`SphereTile`](#spherical-multiresolution-tiles).
|
|
914
|
+
|
|
492
915
|
`previewUrl` is required and shows a low-resolution 2:1 image while `src` loads.
|
|
916
|
+
Provide a small preview to reduce download, decoding, and texture memory costs
|
|
917
|
+
for the initial display.
|
|
493
918
|
Relative paths resolve against the directory of `src`; root-absolute and
|
|
494
919
|
`http(s)` URLs are used as-is. For krpano sphere scenes, copy
|
|
495
920
|
[`<preview url>`](https://krpano.com/docu/xml/#preview) into `previewUrl`. That
|
|
@@ -530,8 +955,8 @@ Pass `controls={false}` to hide it, an appearance object to restyle the default
|
|
|
530
955
|
instance, or render `PanoVideoControls` as a `PanoViewer` child to replace that
|
|
531
956
|
instance.
|
|
532
957
|
|
|
533
|
-
Captions use WebVTT `tracks`. The video element is
|
|
534
|
-
rendered
|
|
958
|
+
Captions use WebVTT `tracks`. The video element is mounted inside the viewer as a
|
|
959
|
+
transparent 1×1-pixel node; cue text is rendered in a screen-space overlay. `captions={false}` hides the overlay and the language
|
|
535
960
|
menu; an object merges with the default appearance (`color`, `background`,
|
|
536
961
|
`fontSize`, `fontFamily`, `textShadow`, `padding`, `borderRadius`, `maxWidth`,
|
|
537
962
|
`bottom`). Those values act as the initial caption appearance; viewers can still
|
|
@@ -580,13 +1005,14 @@ export function VideoExample() {
|
|
|
580
1005
|
}
|
|
581
1006
|
```
|
|
582
1007
|
|
|
583
|
-
`muted` defaults to `true`
|
|
584
|
-
|
|
585
|
-
`webkit-playsinline`, and the X5 `h5-page` attributes,
|
|
586
|
-
the page on iPhone Safari and in WeChat, QQ, and UC.
|
|
1008
|
+
`muted` defaults to `true` for better autoplay compatibility; playback
|
|
1009
|
+
remains subject to browser policy. `playsInline` also defaults to true and writes `playsinline`,
|
|
1010
|
+
`webkit-playsinline`, and the X5 `h5-page` attributes, to request playback
|
|
1011
|
+
within the page on iPhone Safari and in WeChat, QQ, and UC. Actual behavior
|
|
1012
|
+
depends on the browser and its version. Picture-in-picture and
|
|
587
1013
|
remote playback stay disabled so the element can keep feeding the sphere
|
|
588
1014
|
texture. The element is mounted inside the viewer at 1×1px. Unmuted playback
|
|
589
|
-
|
|
1015
|
+
may require a user gesture; `onPlaybackStateChange` receives `"blocked"` when `play()` is
|
|
590
1016
|
rejected. Remote videos and VTT files that are not same-origin should set
|
|
591
1017
|
`crossOrigin` (usually `"anonymous"`). For mobile delivery, provide an H.264 +
|
|
592
1018
|
AAC MP4 fallback, poster, CORS, and Range support.
|
|
@@ -673,12 +1099,12 @@ The first `multires` value is the tile size. Remaining values are ascending cube
|
|
|
673
1099
|
/>
|
|
674
1100
|
```
|
|
675
1101
|
|
|
676
|
-
`previewFaceOrder` must list
|
|
1102
|
+
`previewFaceOrder` must list all six face codes in the preview atlas's actual top-to-bottom order. The default is `l`, `f`, `r`, `b`, `u`, `d`. The preview image is a vertical 1×6 cubestrip. krpano `thumb.jpg` is a small thumbnail and is not this atlas.
|
|
677
1103
|
|
|
678
1104
|
During rapid rotation or zoom, loaded tiles remain visible while newly visible tiles use their parent level or the preview as a local fallback.
|
|
679
1105
|
|
|
680
1106
|
For layouts that are not krpano XML, override the default with a placeholder
|
|
681
|
-
template or a resolver. Both
|
|
1107
|
+
template or a resolver. Both accept paths relative to `baseUrl` or absolute URLs:
|
|
682
1108
|
|
|
683
1109
|
```tsx
|
|
684
1110
|
<Tile
|
|
@@ -704,14 +1130,14 @@ a single non-stereo cube panorama.
|
|
|
704
1130
|
|
|
705
1131
|
## Scenes
|
|
706
1132
|
|
|
707
|
-
`Scenes` switches
|
|
708
|
-
|
|
709
|
-
|
|
1133
|
+
`Scenes` switches between single-image equirectangular, cube-tile, and
|
|
1134
|
+
equirectangular-tile scenes in controlled or uncontrolled mode. Transitions blend
|
|
1135
|
+
a GPU framebuffer snapshot with the incoming scene. The incoming scene stays hidden while it loads its preview; tiled scenes also
|
|
1136
|
+
load the lowest-resolution tiles around the view. Once the scene can be drawn, the current framebuffer becomes a temporary GPU
|
|
710
1137
|
texture and the outgoing source textures are released. The incoming scene then switches to
|
|
711
1138
|
full detail immediately: tiles that are already sharp replace the preview during the blend,
|
|
712
|
-
and tiles that are still loading keep the coarser parent tile or the preview. The
|
|
713
|
-
|
|
714
|
-
the current view.
|
|
1139
|
+
and tiles that are still loading keep the coarser parent tile or the preview. The incoming scene starts loading detail tiles after the outgoing scene is
|
|
1140
|
+
released. Tile selection includes the current view and a small surrounding margin.
|
|
715
1141
|
|
|
716
1142
|
```tsx
|
|
717
1143
|
import {
|
|
@@ -743,7 +1169,7 @@ const scenes: Scene[] = [
|
|
|
743
1169
|
</PanoViewer>;
|
|
744
1170
|
```
|
|
745
1171
|
|
|
746
|
-
Available
|
|
1172
|
+
Available krpano-style presets are `none`, `crossfade`, `zoom`, `blackout`,
|
|
747
1173
|
`whiteFlash`, `slideRightToLeft`, `slideTopToBottom`, `slideDiagonal`,
|
|
748
1174
|
`circleOpen`, `verticalOpen`, `horizontalOpen`, and `ellipticZoomOpen`.
|
|
749
1175
|
Grid presets are `pixelate`, `gridWipe`, `gridWipeUp`, `gridWipeRight`,
|
|
@@ -768,7 +1194,7 @@ values are treated as `0`. `transition="none"` uses the same condition and then
|
|
|
768
1194
|
over directly. A new target, an interrupt, unmount, or a lost WebGL context cancels a
|
|
769
1195
|
pending wait so a stale readiness report cannot affect the next scene.
|
|
770
1196
|
|
|
771
|
-
`onReady`
|
|
1197
|
+
`onReady` means a source can be drawn for the first time. `onSceneReady` fires
|
|
772
1198
|
for the first paintable scene and when an incoming scene takes over after the outgoing
|
|
773
1199
|
snapshot. It does not mean the current view's full detail has finished loading.
|
|
774
1200
|
|
|
@@ -780,14 +1206,16 @@ apply to the whole `Scenes` viewer rather than to each tile scene.
|
|
|
780
1206
|
## PanoFilter
|
|
781
1207
|
|
|
782
1208
|
Render `PanoFilter` inside `PanoViewer` to color-grade or stylize the panorama
|
|
783
|
-
source (`Sphere`, `Tile`, and `PanoVideo`).
|
|
784
|
-
only
|
|
785
|
-
|
|
786
|
-
|
|
1209
|
+
source (`Sphere`, `Tile`, `SphereTile`, and `PanoVideo`). Built-in presets affect
|
|
1210
|
+
only panorama materials. `colorMatrix` defaults to the panorama; set
|
|
1211
|
+
`colorMatrixScope="all"` to include hotspot textures. DOM overlays such as
|
|
1212
|
+
tooltips, video controls, and iframes are unaffected. WebXR / MobileVR use the
|
|
1213
|
+
same panorama shader and inherit its filter appearance.
|
|
787
1214
|
|
|
788
1215
|
`intensity` mixes the filtered result with the original image (`0`–`1`,
|
|
789
|
-
default `1`). Omit the component
|
|
790
|
-
|
|
1216
|
+
default `1`). Omit the component or set `enabled={false}` to disable all filtering.
|
|
1217
|
+
`preset="none"` disables only the built-in preset; a configured `colorMatrix`
|
|
1218
|
+
still applies.
|
|
791
1219
|
|
|
792
1220
|
```tsx
|
|
793
1221
|
<PanoViewer style={{ height: 560 }}>
|
|
@@ -838,10 +1266,11 @@ follow the camera. Render it as a child of `PanoViewer`, **alongside** `Scenes`,
|
|
|
838
1266
|
`Sphere`, or `Tile`. Do not put it in `Scenes`' `renderHotspots` — hotspots
|
|
839
1267
|
unmount during scene transitions and would stop the audio.
|
|
840
1268
|
|
|
841
|
-
`playing` is
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1269
|
+
When `playing` is provided, the host controls playback. Otherwise the component
|
|
1270
|
+
uses internal state initialized by `defaultPlaying` (default `false`). `loop`
|
|
1271
|
+
defaults to `true`. If the browser blocks autoplay, `onPlaybackStateChange`
|
|
1272
|
+
receives `"blocked"`. The component retries `play()` after the next click or
|
|
1273
|
+
keypress while playback is still requested.
|
|
845
1274
|
|
|
846
1275
|
Pick one of the two patterns.
|
|
847
1276
|
|
|
@@ -1198,10 +1627,12 @@ Supported callbacks (krpano names in parentheses where applicable):
|
|
|
1198
1627
|
`PanoViewer`'s existing `onViewChange` / `onPanoramaClick` / `onPanoramaDoubleClick` /
|
|
1199
1628
|
`onPanoramaPointerMove` props remain supported and share the same event bus.
|
|
1200
1629
|
|
|
1201
|
-
Resource
|
|
1202
|
-
`
|
|
1203
|
-
`
|
|
1204
|
-
|
|
1630
|
+
Resource callbacks belong to each source: `Sphere` provides `onLoad`,
|
|
1631
|
+
`onError`, and `onPreviewError`; `Tile` / `SphereTile` provide `onLoadProgress`,
|
|
1632
|
+
`onTileError`, and `onPreviewError`. Sources report initial render readiness
|
|
1633
|
+
through `onReady`. Use `Scenes` callbacks `onSceneReady`, `onTransitionEnd`,
|
|
1634
|
+
and `onTransitionError` for scene state. This package does not provide a
|
|
1635
|
+
one-to-one mapping of krpano XML-loading, VR, or frame-render events.
|
|
1205
1636
|
|
|
1206
1637
|
## PanoContextMenu
|
|
1207
1638
|
|
|
@@ -1378,6 +1809,11 @@ color, border, corner radius, shadow, padding, and font size.
|
|
|
1378
1809
|
fontSize: 13,
|
|
1379
1810
|
}}
|
|
1380
1811
|
/>
|
|
1812
|
+
```
|
|
1813
|
+
|
|
1814
|
+
For a hover-triggered tooltip:
|
|
1815
|
+
|
|
1816
|
+
```tsx
|
|
1381
1817
|
<GraphicHotspot
|
|
1382
1818
|
id="marker"
|
|
1383
1819
|
ariaLabel="Signal"
|
|
@@ -1390,9 +1826,8 @@ color, border, corner radius, shadow, padding, and font size.
|
|
|
1390
1826
|
```
|
|
1391
1827
|
|
|
1392
1828
|
For persistence or a host-owned editor, use the exported discriminated
|
|
1393
|
-
`HotspotDefinition` union.
|
|
1394
|
-
|
|
1395
|
-
categories:
|
|
1829
|
+
`HotspotDefinition` union. Render each variant according to `type`; an exhaustive
|
|
1830
|
+
`switch` must cover every hotspot category in the union:
|
|
1396
1831
|
|
|
1397
1832
|
```ts
|
|
1398
1833
|
import type { HotspotDefinition } from "@ericchen1990/pano-view";
|
|
@@ -1494,8 +1929,8 @@ property to orient them in another direction.
|
|
|
1494
1929
|
```
|
|
1495
1930
|
|
|
1496
1931
|
`cornerRadius` is relative to the rendered graphic's shorter side: `0` keeps
|
|
1497
|
-
square corners and `0.5` produces the maximum rounded corners.
|
|
1498
|
-
|
|
1932
|
+
square corners and `0.5` produces the maximum rounded corners. The value is a
|
|
1933
|
+
relative proportion, not a texture-pixel measurement.
|
|
1499
1934
|
|
|
1500
1935
|
`GraphicHotspot` uses the same `onLoad(texture)` and `onError(error)` callbacks
|
|
1501
1936
|
as `ImageHotspot`. Built-in paths are rasterized locally; only an SVG URL
|
|
@@ -1504,12 +1939,12 @@ markup.
|
|
|
1504
1939
|
|
|
1505
1940
|
## Hotspot mode and zoom behaviour
|
|
1506
1941
|
|
|
1507
|
-
Point hotspots use
|
|
1508
|
-
placement properties:
|
|
1942
|
+
Point hotspots use `mode` to configure orientation and placement together:
|
|
1509
1943
|
|
|
1510
1944
|
- `mode="surface"` attaches the plane to the local panorama surface.
|
|
1511
1945
|
- `mode="billboard"` keeps the plane facing the camera and floats it in front
|
|
1512
|
-
of the shell.
|
|
1946
|
+
of the shell. `distance` is the distance from the camera (world units, default `10`);
|
|
1947
|
+
decrease it to move the hotspot nearer.
|
|
1513
1948
|
The built-in panorama shell is at roughly `1000` units; distance is
|
|
1514
1949
|
automatically capped so the hotspot's corners cannot cross it.
|
|
1515
1950
|
- `scaleMode="fov"` (default) lets a hotspot grow when the user zooms in.
|
|
@@ -1576,8 +2011,7 @@ pitch clamped. Individual vertex editing is provided by the authoring
|
|
|
1576
2011
|
workflow, not the runtime hotspot component.
|
|
1577
2012
|
|
|
1578
2013
|
Set `fillOpacity={0}` for a closed outline-only polygon. The fill and outline
|
|
1579
|
-
share the same spherical edge sampling
|
|
1580
|
-
between them.
|
|
2014
|
+
share the same spherical edge sampling so the outline follows the fill boundary.
|
|
1581
2015
|
|
|
1582
2016
|
## PolylineHotspot
|
|
1583
2017
|
|
|
@@ -1688,9 +2122,11 @@ attenuation (stereo pan only). Use a smaller `range` such as `90` for a
|
|
|
1688
2122
|
spot source.
|
|
1689
2123
|
|
|
1690
2124
|
Playback uses Web Audio via Howler.js (bundled; hosts do not install
|
|
1691
|
-
`howler`). The file must decode before
|
|
1692
|
-
format. `src` may be a string or an array of fallback URLs.
|
|
1693
|
-
streaming
|
|
2125
|
+
`howler`). The file must decode before playback starts; MP3 is a suitable
|
|
2126
|
+
format. `src` may be a string or an array of fallback URLs. The component uses Web Audio rather than HTML5
|
|
2127
|
+
streaming to retain stereo panning.
|
|
2128
|
+
|
|
2129
|
+
For extensionless sources such as `blob:` URLs, pass `format="mp3"` (or an array of container hints in source order). The optional `format` field is also supported by audio definitions in `project.json`.
|
|
1694
2130
|
|
|
1695
2131
|
`playing` is controlled like `VideoHotspot`. `loop` defaults to `false`,
|
|
1696
2132
|
`muted` to `false`, `volume` to `1`, and `pauseWhenHidden` to `true` (pause
|
|
@@ -1803,9 +2239,10 @@ not load.
|
|
|
1803
2239
|
|
|
1804
2240
|
## Next.js and SSR
|
|
1805
2241
|
|
|
1806
|
-
The
|
|
1807
|
-
Component boundary.
|
|
1808
|
-
|
|
2242
|
+
The component entry is marked as a client module; render it within a Client
|
|
2243
|
+
Component boundary. Use `@ericchen1990/pano-view/data` for server-side
|
|
2244
|
+
configuration loading. The browser loads panorama assets. The viewer itself
|
|
2245
|
+
needs no API key; asset servers still need appropriate access and CORS settings.
|
|
1809
2246
|
|
|
1810
2247
|
Import the stylesheet once from a Client Component or a global CSS file:
|
|
1811
2248
|
|
|
@@ -1817,7 +2254,20 @@ See [Stylesheet (HTML chrome)](#stylesheet-html-chrome).
|
|
|
1817
2254
|
|
|
1818
2255
|
## Spherical multiresolution tiles
|
|
1819
2256
|
|
|
1820
|
-
`SphereTile` renders
|
|
2257
|
+
`SphereTile` renders multiresolution tile pyramids covering a complete 2:1
|
|
2258
|
+
equirectangular panorama. It selects tiles according to the view, zoom, and
|
|
2259
|
+
texture budget, mapping each tile as a separate texture onto its corresponding
|
|
2260
|
+
spherical region. Coarser tiles or the preview cover regions whose detail has
|
|
2261
|
+
not loaded yet.
|
|
2262
|
+
|
|
2263
|
+
This avoids loading the entire panorama as one high-resolution texture and can
|
|
2264
|
+
reduce peak image-decoding and texture-upload costs. Actual memory use still
|
|
2265
|
+
depends on tile dimensions, visible levels, and the cache budget. The current
|
|
2266
|
+
spherical renderer may still show visible artifacts near the top and bottom
|
|
2267
|
+
poles. Prefer cube tiles ([`Tile`](#tile)) for newly generated panorama assets;
|
|
2268
|
+
use `SphereTile` for existing equirectangular tile pyramids.
|
|
2269
|
+
|
|
2270
|
+
Basic usage:
|
|
1821
2271
|
|
|
1822
2272
|
```tsx
|
|
1823
2273
|
import { PanoViewer, SphereTile } from "@ericchen1990/pano-view";
|
|
@@ -1885,9 +2335,9 @@ const scenes: Scene[] = [
|
|
|
1885
2335
|
`onSceneReady(sceneId)` fires when the first scene becomes paintable and when an
|
|
1886
2336
|
incoming scene takes over after the outgoing snapshot is captured. The transition
|
|
1887
2337
|
may still be running; use this callback to apply the incoming camera view without
|
|
1888
|
-
changing the outgoing snapshot.
|
|
1889
|
-
the blend. `transitionWaitForDetailMs` can hold the outgoing snapshot until the current
|
|
1890
|
-
view is
|
|
2338
|
+
changing the outgoing snapshot. For tiled scenes, detail loading is enabled after that handoff and continues
|
|
2339
|
+
during the blend. `transitionWaitForDetailMs` can hold the outgoing snapshot within the configured timeout until the current
|
|
2340
|
+
view's target detail is ready; it does not change when `onSceneReady` fires. `onTransitionEnd` marks
|
|
1891
2341
|
completion of the blend.
|
|
1892
2342
|
|
|
1893
2343
|
`SphereTileScene` accepts the source options of `SphereTile`, including
|
|
@@ -1902,13 +2352,105 @@ its own cache, callbacks, and loading options.
|
|
|
1902
2352
|
`PanoFilter` also accepts `colorMatrix`, a readonly row-major 4×5 RGBA matrix
|
|
1903
2353
|
(20 finite numbers, matching SVG `feColorMatrix`). The matrix is applied in
|
|
1904
2354
|
display/sRGB space after the panorama preset. `intensity` mixes each effect
|
|
1905
|
-
with
|
|
2355
|
+
with its input; `enabled={false}` disables both. Set
|
|
1906
2356
|
`colorMatrixScope="all"` to include hotspot textures; the default `"panorama"`
|
|
1907
2357
|
keeps existing behavior. Artistic presets always affect only the panorama.
|
|
1908
2358
|
HTML tooltips, iframes and other DOM chrome remain host-styled.
|
|
1909
2359
|
|
|
1910
|
-
Hotspot `tooltipAppearance`
|
|
2360
|
+
Hotspot `tooltipAppearance` accepts `fontFamily`, `lineHeight`, and
|
|
1911
2361
|
`letterSpacing`, in addition to `fontSize` and existing paint options.
|
|
1912
2362
|
`lineHeight` numbers are unitless; numeric `letterSpacing` values are CSS pixels.
|
|
1913
2363
|
|
|
1914
2364
|
Regression checks: `pnpm --filter @ericchen1990/pano-view test` (Node 22+).
|
|
2365
|
+
|
|
2366
|
+
## FAQ and troubleshooting
|
|
2367
|
+
|
|
2368
|
+
### Why prefer cube tiles?
|
|
2369
|
+
|
|
2370
|
+
[`Tile`](#tile) loads cube tiles according to the view and zoom, avoiding one oversized panorama texture and the current spherical renderer's pole artifacts. Use [`SphereTile`](#spherical-multiresolution-tiles) for existing equirectangular tiles with view-dependent loading; tiling alone does not remove spherical pole artifacts. `Sphere` suits single images with manageable dimensions. A preview improves initial display time, but its dimensions should still be kept small. See [Sphere](#sphere).
|
|
2371
|
+
|
|
2372
|
+
### How do I deploy multiple scenes without a database?
|
|
2373
|
+
|
|
2374
|
+
Keep the converter directory structure, author `project.json` referencing each `viewer.json`, and render it with `loadPanoProject` and `ProjectScenes`. Upload your React build and assets to static hosting. Under a prefix such as `/demo/`, use the correct project entry URL and prefer relative references inside the project.
|
|
2375
|
+
|
|
2376
|
+
### Why do JSON or textures fail across origins?
|
|
2377
|
+
|
|
2378
|
+
Inspect Network and Console to identify the failing config, preview or tile. The server/CDN must allow your app origin. Public anonymous resources may use `Access-Control-Allow-Origin: *`; credentialed requests need an explicit allowed origin and `Access-Control-Allow-Credentials: true`. Use `Vary: Origin` when varying by origin. JavaScript may only receive `Failed to fetch`, so the loader cannot diagnose CORS with certainty.
|
|
2379
|
+
|
|
2380
|
+
`no-cors` gives an unreadable opaque response; it cannot make JSON readable or bypass WebGL texture rules. Configure the server. See [MDN CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) and [WebGL textures](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Using_textures_in_WebGL).
|
|
2381
|
+
|
|
2382
|
+
Example S3 console CORS configuration for reads (replace the website origin):
|
|
2383
|
+
|
|
2384
|
+
```json
|
|
2385
|
+
[{
|
|
2386
|
+
"AllowedOrigins": ["https://app.example.com"],
|
|
2387
|
+
"AllowedMethods": ["GET", "HEAD"],
|
|
2388
|
+
"AllowedHeaders": ["Authorization"]
|
|
2389
|
+
}]
|
|
2390
|
+
```
|
|
2391
|
+
|
|
2392
|
+
CORS does not grant object read permission. When using a CDN, check its actual response headers too. See the [official S3 CORS guide](https://docs.aws.amazon.com/AmazonS3/latest/userguide/cors.html).
|
|
2393
|
+
|
|
2394
|
+
### Why do tiles return 404 or use the wrong URL?
|
|
2395
|
+
|
|
2396
|
+
Compare the complete Network URL with the uploaded object key. Check `baseUrl`, extra directory levels, case and filenames. A new domain/prefix only needs a new root; renamed files need mapping. Preserve the converter template, particularly `%v_%h` versus `%h_%v`.
|
|
2397
|
+
|
|
2398
|
+
### Why INVALID_JSON with HTTP 200?
|
|
2399
|
+
|
|
2400
|
+
Inspect the response body for HTML. A static SPA fallback may serve the homepage for a missing JSON file, or authentication may redirect to a login page. Fix the URL/routing; status 200 does not guarantee JSON.
|
|
2401
|
+
|
|
2402
|
+
### Why does JSON work but images return 401/403 or expired signatures?
|
|
2403
|
+
|
|
2404
|
+
Treat config authentication and image access separately. `requestInit` headers/credentials apply to JSON only; its URL token is not appended to tiles. Supply public URLs, signed URLs, or your application's supported authentication mechanism for assets. Acquire signatures before mapping/loading and refresh them when expired; keep temporary credentials outside conversion JSON.
|
|
2405
|
+
|
|
2406
|
+
### Why is the canvas blank after config loading succeeds?
|
|
2407
|
+
|
|
2408
|
+
Ensure the viewer container has nonzero height, inspect texture requests and WebGL console errors, and keep React, Three.js and React Three Fiber singletons. Metadata success is not render readiness: observe `onSceneReady` and `onTransitionError`.
|
|
2409
|
+
|
|
2410
|
+
### Why is the preview correct but detailed tiles are missing or misplaced?
|
|
2411
|
+
|
|
2412
|
+
Upload every tile and preserve actual level sizes, `urlTemplate`, `previewFaceOrder` and `indexBase`. Do not assume all levels exactly double or confuse zero-based logical coordinates with stored indices. A custom resolver must identify the corresponding original tile.
|
|
2413
|
+
|
|
2414
|
+
### Why are hotspots missing or not navigating?
|
|
2415
|
+
|
|
2416
|
+
Check the owning scene, `visible`, `interactive`, `pointerEvents`, angles and asset requests. Hotspots hide during transitions. IDs must be unique in their respective scopes and `action.sceneId` must exist. `preventDefault()` cancels built-in actions. Text alone does not create an information dialog.
|
|
2417
|
+
|
|
2418
|
+
### Why does media not play?
|
|
2419
|
+
|
|
2420
|
+
Start playback from a click or keyboard gesture, check supported formats and permissions, and inspect `onHotspotError`. Browsers can block unmuted autoplay; tab visibility and device rules can also affect playback. The component does not bypass browser policies.
|
|
2421
|
+
|
|
2422
|
+
### What about local files, mixed content, caching and versions?
|
|
2423
|
+
|
|
2424
|
+
Serve files over local HTTP(S), rather than double-clicking `file://` HTML. HTTPS pages should load HTTPS assets. Use versioned asset directories and coordinate JSON/image caching so stale configs do not target replaced files. `UNSUPPORTED_SCHEMA_VERSION` requires a supported config version. Planar conversions have no `viewer.json` and need a planar viewer.
|
|
2425
|
+
|
|
2426
|
+
For conversion failures see [converter error codes and troubleshooting](../converter/README.md#error-codes-and-troubleshooting).
|
|
2427
|
+
|
|
2428
|
+
### Camera-center limits
|
|
2429
|
+
|
|
2430
|
+
`PanoViewer`, every `Scenes` scene, and each `project.json` scene accept optional `viewLimits`:
|
|
2431
|
+
|
|
2432
|
+
```ts
|
|
2433
|
+
viewLimits: {
|
|
2434
|
+
yaw: { min: 170, max: -170 },
|
|
2435
|
+
pitch: { min: -30, max: 45 },
|
|
2436
|
+
fov: { min: 40, max: 90 },
|
|
2437
|
+
}
|
|
2438
|
+
```
|
|
2439
|
+
|
|
2440
|
+
Angles are degrees. Yaw bounds are within −180…180; min > max crosses the seam. −180…180 allows a full circle. Pitch bounds are within −90…90, FOV within 1…179; both require min ≤ max. Equal bounds lock an axis. Scene axes override viewer axes individually; omitted axes inherit viewer settings, with existing `minFov` / `maxFov` (30 / 100 by default) as the FOV fallback. Bounds apply to the camera center, including pointer, touch, keyboard, inertia, auto rotation, gyro and view navigation APIs; they do not keep the entire visible image inside the bounds. Explicit axes have hard boundaries even with `bouncingLimits`. Limited yaw animations stay in the permitted arc, overriding a shorter path through forbidden angles. Immersive WebXR remains controlled by the headset.
|
|
2441
|
+
|
|
2442
|
+
Scene limits take effect when the incoming scene takes over, before its initial view. Live changes cancel navigation and inertia and constrain the current view without rewriting authored initial views or reloading textures. Removing scene limits restores viewer limits.
|
|
2443
|
+
|
|
2444
|
+
### Hotspot interaction events and custom actions
|
|
2445
|
+
|
|
2446
|
+
`events` maps `click`, `pointerenter`, and `pointerleave` to one action each. Every event supports `scene`, `view`, `popup`, and `{ type: "custom", actionId, params? }`. `params` is a JSON object; functions, credentials, API implementations and form definitions belong to the host. Parameter strings are not resolved or packaged as assets.
|
|
2447
|
+
|
|
2448
|
+
```json
|
|
2449
|
+
{"events":{"click":{"type":"scene","sceneId":"hall"},"pointerenter":{"type":"custom","actionId":"product.preview","params":{"visible":true}},"pointerleave":{"type":"custom","actionId":"product.preview","params":{"visible":false}}}}
|
|
2450
|
+
```
|
|
2451
|
+
|
|
2452
|
+
Legacy `action` remains a click fallback; `events.click` wins. Editing or clearing click removes the legacy fallback. Loading a project does not migrate it. New event configurations require an updated viewer (project schemaVersion remains 1). Media without a click action still toggles playback; hover has no default action.
|
|
2453
|
+
|
|
2454
|
+
`ProjectScenes.onHotspotEvent(event)` receives `trigger`, `sceneId`, `hotspot`, `interaction` and `preventDefault()`. On clicks it runs before the existing `onHotspotClick`; either callback can cancel the action. `onCustomAction(event)` additionally receives `action` and may return a Promise. Dispatch by the stable `action.actionId`, validate business parameters and throw for unknown actions. Missing handlers, synchronous exceptions and rejected Promises reach `onHotspotError` with `trigger`. Requests, authentication, cancellation, duplicate prevention and response ordering are host responsibilities; events dispatch independently without automatic retries.
|
|
2455
|
+
|
|
2456
|
+
Hover runs once per logical hotspot boundary, skips touch and keyboard focus, and respects visibility and pointer interaction settings. Scene changes, hiding and unmounting clear hover state without executing leave actions. Existing tooltip behavior is independent. Custom renderers must forward `context.onHoverChange(hovered, interaction)` as well as `context.onClick`; `context.renderDefault()` wires both automatically.
|