@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/dist/data.mjs ADDED
@@ -0,0 +1,1057 @@
1
+ // src/data/errors.ts
2
+ var PanoDataError = class extends Error {
3
+ constructor(code, message, details = {}) {
4
+ super(message);
5
+ this.name = "PanoDataError";
6
+ this.code = code;
7
+ Object.assign(this, details);
8
+ }
9
+ };
10
+ function aborted(signal) {
11
+ if (signal?.aborted)
12
+ throw new PanoDataError("ABORTED", "Loading was cancelled");
13
+ }
14
+
15
+ // ../../shared/viewer-config.ts
16
+ function isRecord(value) {
17
+ return value !== null && typeof value === "object" && !Array.isArray(value);
18
+ }
19
+ function requiredString(value, field) {
20
+ if (typeof value !== "string" || !value.trim())
21
+ throw new Error(`${field}: expected a nonempty string`);
22
+ return value;
23
+ }
24
+ function positive(value, field) {
25
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0)
26
+ throw new Error(`${field}: expected a positive safe integer`);
27
+ return value;
28
+ }
29
+ function validateViewerConfig(value) {
30
+ if (!isRecord(value)) throw new Error("viewerConfig: expected an object");
31
+ if (value.type !== "tile" && value.type !== "sphereTile")
32
+ throw new Error("type: expected tile or sphereTile");
33
+ if (!isRecord(value.multires))
34
+ throw new Error("multires: expected an object");
35
+ const tileSize = positive(value.multires.tileSize, "multires.tileSize");
36
+ if (tileSize > 8192) throw new Error("multires.tileSize: maximum is 8192");
37
+ const input = value.multires.levels;
38
+ if (!Array.isArray(input) || input.length === 0 || input.length > 64)
39
+ throw new Error("multires.levels: expected 1 to 64 levels");
40
+ const urlTemplate = requiredString(value.urlTemplate, "urlTemplate");
41
+ const previewUrl = requiredString(value.previewUrl, "previewUrl");
42
+ if (/%/.test(urlTemplate.replace(/%s|%l|%0*[hxucvywr]/g, "")))
43
+ throw new Error("urlTemplate: unsupported placeholder");
44
+ if (value.type === "tile") {
45
+ const levels2 = input.map(
46
+ (level, i) => positive(level, `multires.levels[${i}]`)
47
+ );
48
+ if (levels2.some((level, i) => i > 0 && level <= levels2[i - 1]))
49
+ throw new Error("multires.levels: face sizes must increase");
50
+ const order = value.previewFaceOrder;
51
+ if (!Array.isArray(order) || order.length !== 6 || !["f", "r", "b", "l", "u", "d"].every((face) => order.includes(face)))
52
+ throw new Error("previewFaceOrder: expected six unique cube faces");
53
+ return {
54
+ type: "tile",
55
+ multires: { tileSize, levels: levels2 },
56
+ urlTemplate,
57
+ previewUrl,
58
+ previewFaceOrder: [...order]
59
+ };
60
+ }
61
+ if (urlTemplate.includes("%s"))
62
+ throw new Error("urlTemplate: sphere has no cube face");
63
+ if (value.indexBase !== 0 && value.indexBase !== 1)
64
+ throw new Error("indexBase: expected 0 or 1");
65
+ const levels = input.map((level, i) => {
66
+ if (!isRecord(level))
67
+ throw new Error(`multires.levels[${i}]: expected dimensions`);
68
+ const width = positive(level.width, `multires.levels[${i}].width`);
69
+ const height = positive(level.height, `multires.levels[${i}].height`);
70
+ if (Math.abs(width - 2 * height) > 2)
71
+ throw new Error(`multires.levels[${i}]: expected a full 2:1 panorama`);
72
+ return { width, height };
73
+ });
74
+ if (levels.some(
75
+ (level, i) => i > 0 && (level.width <= levels[i - 1].width || level.height <= levels[i - 1].height)
76
+ ))
77
+ throw new Error("multires.levels: dimensions must increase");
78
+ return {
79
+ type: "sphereTile",
80
+ multires: { tileSize, levels },
81
+ urlTemplate,
82
+ previewUrl,
83
+ indexBase: value.indexBase
84
+ };
85
+ }
86
+
87
+ // src/tile/multires.ts
88
+ function resolveTemplateUrl(template, address) {
89
+ return template.replace(/%s/g, address.face).replace(/%l/g, String(address.level)).replace(/%(0*)([hxucvywr])/g, (_match, zeroes, placeholder) => {
90
+ const value = "hxuc".includes(placeholder) ? address.col : address.row;
91
+ return String(value).padStart(zeroes.length + 1, "0");
92
+ });
93
+ }
94
+
95
+ // src/data/viewer.ts
96
+ function documentUrl(input) {
97
+ try {
98
+ const url = new URL(
99
+ input,
100
+ typeof document === "undefined" ? void 0 : document.baseURI
101
+ );
102
+ if (url.protocol !== "http:" && url.protocol !== "https:")
103
+ throw new Error("Use HTTP(S)");
104
+ return url.href;
105
+ } catch (cause) {
106
+ throw new PanoDataError(
107
+ "INVALID_OPTIONS",
108
+ "Expected an HTTP(S) URL; Node.js requires an absolute URL",
109
+ { cause }
110
+ );
111
+ }
112
+ }
113
+ function directoryUrl(input) {
114
+ const url = new URL(input);
115
+ url.search = "";
116
+ url.hash = "";
117
+ if (!url.pathname.endsWith("/")) url.pathname += "/";
118
+ return url.href;
119
+ }
120
+ function resolveResource(baseUrl, path) {
121
+ try {
122
+ const url = new URL(path, directoryUrl(baseUrl));
123
+ if (!["http:", "https:", "blob:", "data:"].includes(url.protocol))
124
+ throw new Error("Unsupported resource URL protocol");
125
+ return url.href;
126
+ } catch (cause) {
127
+ throw new PanoDataError(
128
+ "INVALID_CONFIG",
129
+ `Invalid resource path: ${path}`,
130
+ { cause }
131
+ );
132
+ }
133
+ }
134
+ function mapResource(path, context, resolver) {
135
+ let result;
136
+ try {
137
+ result = resolver ? resolver(path, context) : context.defaultUrl;
138
+ } catch (cause) {
139
+ throw new PanoDataError("INVALID_CONFIG", "resolveAssetUrl failed", {
140
+ sceneId: context.sceneId,
141
+ cause
142
+ });
143
+ }
144
+ if (typeof result !== "string" || !result.trim())
145
+ throw new PanoDataError(
146
+ "INVALID_CONFIG",
147
+ "resolveAssetUrl must return a nonempty final URL",
148
+ { sceneId: context.sceneId }
149
+ );
150
+ if (!result.startsWith("/") && !/^(https?:|blob:|data:)/i.test(result))
151
+ throw new PanoDataError(
152
+ "INVALID_CONFIG",
153
+ "resolveAssetUrl must return an absolute URL or a site-root path",
154
+ { sceneId: context.sceneId }
155
+ );
156
+ return result;
157
+ }
158
+ function parseConfig(value) {
159
+ if (isRecord(value) && value.type !== "tile" && value.type !== "sphereTile")
160
+ throw new PanoDataError(
161
+ "UNSUPPORTED_PROJECTION",
162
+ "Expected a tile or sphereTile configuration",
163
+ { field: "type" }
164
+ );
165
+ try {
166
+ return validateViewerConfig(value);
167
+ } catch (cause) {
168
+ const message = cause instanceof Error ? cause.message : "Invalid viewer configuration";
169
+ throw new PanoDataError("INVALID_CONFIG", message, {
170
+ cause,
171
+ field: message.split(":")[0]
172
+ });
173
+ }
174
+ }
175
+ function sceneFromViewerConfig(value, options) {
176
+ if (!options || typeof options.id !== "string" || !options.id.trim() || typeof options.baseUrl !== "string" || !options.baseUrl.trim())
177
+ throw new PanoDataError("INVALID_OPTIONS", "id and baseUrl are required");
178
+ const config = parseConfig(value);
179
+ const baseUrl = directoryUrl(documentUrl(options.baseUrl));
180
+ const resolve = (path, kind) => mapResource(
181
+ path,
182
+ {
183
+ kind,
184
+ sceneId: options.id,
185
+ defaultUrl: resolveResource(baseUrl, path)
186
+ },
187
+ options.resolveAssetUrl
188
+ );
189
+ const previewUrl = resolve(config.previewUrl, "preview");
190
+ if (config.type === "tile") {
191
+ return {
192
+ ...config,
193
+ id: options.id,
194
+ baseUrl,
195
+ previewUrl,
196
+ ...options.resolveAssetUrl ? {
197
+ resolveTileUrl: (address) => resolve(resolveTemplateUrl(config.urlTemplate, address), "tile")
198
+ } : {}
199
+ };
200
+ }
201
+ return {
202
+ ...config,
203
+ id: options.id,
204
+ baseUrl,
205
+ previewUrl,
206
+ ...options.resolveAssetUrl ? {
207
+ resolveTileUrl: (address) => {
208
+ const path = resolveTemplateUrl(config.urlTemplate, {
209
+ face: "f",
210
+ level: address.level + config.indexBase,
211
+ row: address.row + config.indexBase,
212
+ col: address.col + config.indexBase
213
+ });
214
+ return resolve(path, "tile");
215
+ }
216
+ } : {}
217
+ };
218
+ }
219
+ async function readJson(url, options) {
220
+ const signal = options.requestInit?.signal;
221
+ aborted(signal);
222
+ let response;
223
+ try {
224
+ response = await fetch(url, { ...options.requestInit, method: "GET" });
225
+ } catch (cause) {
226
+ aborted(signal);
227
+ throw new PanoDataError(
228
+ "FETCH_FAILED",
229
+ "Could not fetch JSON; check the network and server CORS settings",
230
+ { cause }
231
+ );
232
+ }
233
+ aborted(signal);
234
+ if (!response.ok)
235
+ throw new PanoDataError(
236
+ "HTTP_ERROR",
237
+ `JSON request returned HTTP ${response.status}`,
238
+ { status: response.status }
239
+ );
240
+ try {
241
+ const value = await response.json();
242
+ aborted(signal);
243
+ return { value, url: response.url || url };
244
+ } catch (cause) {
245
+ aborted(signal);
246
+ throw new PanoDataError(
247
+ "INVALID_JSON",
248
+ "Expected JSON; check for an HTML fallback or login page",
249
+ { cause }
250
+ );
251
+ }
252
+ }
253
+ async function loadSceneFromJson(url, options) {
254
+ if (!options || typeof options.id !== "string" || !options.id.trim())
255
+ throw new PanoDataError("INVALID_OPTIONS", "id is required");
256
+ const response = await readJson(documentUrl(url), options);
257
+ if (!isRecord(response.value))
258
+ throw new PanoDataError(
259
+ "INVALID_CONFIG",
260
+ "viewer.json must contain an object"
261
+ );
262
+ if (response.value.schemaVersion !== 1)
263
+ throw new PanoDataError(
264
+ "UNSUPPORTED_SCHEMA_VERSION",
265
+ "Unsupported viewer.json schemaVersion",
266
+ { field: "schemaVersion" }
267
+ );
268
+ return sceneFromViewerConfig(response.value, {
269
+ ...options,
270
+ baseUrl: options.baseUrl ?? new URL(".", response.url).href
271
+ });
272
+ }
273
+
274
+ // src/view-limits.ts
275
+ function parseViewLimits(value) {
276
+ if (value === void 0) return void 0;
277
+ if (!value || typeof value !== "object" || Array.isArray(value))
278
+ throw new Error("viewLimits: expected an object");
279
+ const result = {};
280
+ for (const axis of ["yaw", "pitch", "fov"]) {
281
+ const input = value[axis];
282
+ if (input === void 0) continue;
283
+ if (!input || typeof input !== "object" || Array.isArray(input))
284
+ throw new Error(`viewLimits.${axis}: expected min and max`);
285
+ const { min, max } = input;
286
+ const lower = axis === "yaw" ? -180 : axis === "pitch" ? -90 : 1;
287
+ const upper = axis === "yaw" ? 180 : axis === "pitch" ? 90 : 179;
288
+ if (typeof min !== "number" || typeof max !== "number" || !Number.isFinite(min) || !Number.isFinite(max) || min < lower || min > upper || max < lower || max > upper)
289
+ throw new Error(`viewLimits.${axis}: expected finite min and max from ${lower} to ${upper}`);
290
+ if (axis !== "yaw" && min > max)
291
+ throw new Error(`viewLimits.${axis}: min must not exceed max`);
292
+ result[axis] = { min, max };
293
+ }
294
+ return result;
295
+ }
296
+
297
+ // src/data/popups.ts
298
+ function parsePopups(value, draft = false) {
299
+ if (value === void 0) return void 0;
300
+ try {
301
+ if (!Array.isArray(value)) throw new Error("popups: expected an array");
302
+ const ids = /* @__PURE__ */ new Set();
303
+ const string = (input, field, allowEmpty = false) => {
304
+ if (typeof input !== "string" || !allowEmpty && !input.trim())
305
+ throw new Error(`${field}: expected a ${allowEmpty ? "string" : "nonempty string"}`);
306
+ return input;
307
+ };
308
+ return value.map((entry, index) => {
309
+ const field = `popups[${index}]`;
310
+ if (!isRecord(entry)) throw new Error(`${field}: expected an object`);
311
+ const id = requiredString(entry.id, `${field}.id`);
312
+ if (ids.has(id)) throw new Error(`${field}.id: duplicate popup ID`);
313
+ ids.add(id);
314
+ const title = string(entry.title, `${field}.title`, draft);
315
+ if (!Array.isArray(entry.blocks)) throw new Error(`${field}.blocks: expected an array`);
316
+ const blockIds = /* @__PURE__ */ new Set();
317
+ const blocks = entry.blocks.map((block, i) => {
318
+ const path = `${field}.blocks[${i}]`;
319
+ if (!isRecord(block)) throw new Error(`${path}: expected an object`);
320
+ const id2 = requiredString(block.id, `${path}.id`);
321
+ if (blockIds.has(id2)) throw new Error(`${path}.id: duplicate block ID`);
322
+ blockIds.add(id2);
323
+ if (block.type === "text") return { id: id2, type: "text", text: string(block.text, `${path}.text`, true) };
324
+ const src = string(block.src, `${path}.src`, draft);
325
+ if (src) resolveResource("https://popup.invalid/", src);
326
+ if (block.type === "image") return {
327
+ id: id2,
328
+ type: "image",
329
+ src,
330
+ ...block.alt === void 0 ? {} : { alt: string(block.alt, `${path}.alt`, true) }
331
+ };
332
+ if (block.type === "audio") return { id: id2, type: "audio", src };
333
+ if (block.type === "video") {
334
+ const poster = block.poster === void 0 ? void 0 : string(block.poster, `${path}.poster`, true);
335
+ if (poster) resolveResource("https://popup.invalid/", poster);
336
+ return { id: id2, type: "video", src, ...poster ? { poster } : {} };
337
+ }
338
+ throw new Error(`${path}.type: unsupported content block`);
339
+ });
340
+ return { id, title, blocks };
341
+ });
342
+ } catch (cause) {
343
+ const message = cause instanceof Error ? cause.message : "Invalid popup content";
344
+ throw new PanoDataError("INVALID_PROJECT", message, { field: message.split(":")[0], cause });
345
+ }
346
+ }
347
+ function resolveProjectPopupAssets(popup, baseUrl, resolver) {
348
+ return { ...popup, blocks: popup.blocks.map((block) => {
349
+ if (block.type === "text") return { ...block };
350
+ const resolve = (path, field) => {
351
+ if (!path) throw new PanoDataError("INVALID_PROJECT", `${popup.id}/${block.id}.${field}: choose a resource`);
352
+ const context = { defaultUrl: resolveResource(baseUrl, path), popupId: popup.id, blockId: block.id, field };
353
+ const result = resolver ? resolver(path, context) : context.defaultUrl;
354
+ if (typeof result !== "string" || !result.startsWith("/") && !/^(https?:|blob:|data:)/i.test(result))
355
+ throw new PanoDataError("INVALID_CONFIG", "resolvePopupAssetUrl must return an absolute URL or a site-root path");
356
+ return result;
357
+ };
358
+ return { ...block, src: resolve(block.src, "src"), ...block.type === "video" && block.poster ? { poster: resolve(block.poster, "poster") } : {} };
359
+ }) };
360
+ }
361
+
362
+ // ../../shared/cursor-assets.ts
363
+ function mapCursorAssets(cursor, resolve) {
364
+ return cursor.replace(
365
+ /url\(\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|((?:\\.|[^)\\])*?))\s*\)/gi,
366
+ (_match, double, single, bare) => {
367
+ const path = (double ?? single ?? bare ?? "").replace(
368
+ /\\([\da-f]{1,6}\s?|.)/gi,
369
+ (_escape, value) => /^[\da-f]/i.test(value) ? String.fromCodePoint(
370
+ Math.min(parseInt(value.trim(), 16), 1114111)
371
+ ) : value
372
+ );
373
+ return `url(${JSON.stringify(resolve(path))})`;
374
+ }
375
+ );
376
+ }
377
+
378
+ // src/data/hotspots.ts
379
+ var projectHotspotTriggers = ["click", "pointerenter", "pointerleave"];
380
+ function hotspotAction(hotspot, trigger) {
381
+ return hotspot.events?.[trigger] ?? (trigger === "click" ? hotspot.action : void 0);
382
+ }
383
+ function setHotspotAction(hotspot, trigger, action) {
384
+ const events = { ...hotspot.events };
385
+ const next = { ...hotspot, events };
386
+ if (trigger === "click") delete next.action;
387
+ if (action) events[trigger] = action;
388
+ else delete events[trigger];
389
+ if (!Object.keys(events).length) delete next.events;
390
+ return next;
391
+ }
392
+ function jsonValue(value, seen = /* @__PURE__ */ new Set()) {
393
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
394
+ if (typeof value === "number" && Number.isFinite(value)) return value;
395
+ if (typeof value !== "object" || !value || seen.has(value)) throw new Error("params: expected JSON data");
396
+ if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new Error("params: expected JSON data");
397
+ seen.add(value);
398
+ const result = Array.isArray(value) ? value.map((item) => jsonValue(item, seen)) : Object.fromEntries(Object.entries(value).map(([key, item]) => [key, jsonValue(item, seen)]));
399
+ seen.delete(value);
400
+ return result;
401
+ }
402
+ function parseAction(action) {
403
+ if (!isRecord(action)) throw new Error("action: expected an object");
404
+ if (action.type === "scene") return { type: "scene", sceneId: requiredString(action.sceneId, "action.sceneId") };
405
+ if (action.type === "popup") return { type: "popup", popupId: requiredString(action.popupId, "action.popupId") };
406
+ if (action.type === "custom") {
407
+ if (action.params !== void 0 && (!isRecord(action.params) || Array.isArray(action.params))) throw new Error("action.params: expected an object");
408
+ return { type: "custom", actionId: requiredString(action.actionId, "action.actionId"), ...action.params === void 0 ? {} : { params: jsonValue(action.params) } };
409
+ }
410
+ if (action.type === "view") {
411
+ const position = parsePosition(action.view, "action.view");
412
+ const fov = finite(action.view.fov, "action.view.fov");
413
+ if (fov <= 0 || fov >= 180) throw new Error("action.view.fov: expected 0 < fov < 180");
414
+ const duration = action.duration === void 0 ? void 0 : finite(action.duration, "action.duration");
415
+ if (duration !== void 0 && duration < 0) throw new Error("action.duration: expected a nonnegative number");
416
+ return { type: "view", view: { ...position, fov }, ...duration === void 0 ? {} : { duration } };
417
+ }
418
+ throw new Error("action.type: unsupported action");
419
+ }
420
+ var common = [
421
+ "label",
422
+ "visible",
423
+ "interactive",
424
+ "pointerEvents",
425
+ "cursor",
426
+ "renderOrder",
427
+ "tooltip",
428
+ "tooltipTrigger",
429
+ "tooltipPlacement",
430
+ "tooltipOffset",
431
+ "tooltipAppearance"
432
+ ];
433
+ var point = [
434
+ "position",
435
+ "width",
436
+ "height",
437
+ "scale",
438
+ "mode",
439
+ "distance",
440
+ "scaleMode",
441
+ "referenceFov",
442
+ "rotation",
443
+ "opacity"
444
+ ];
445
+ var fields = {
446
+ image: ["src"],
447
+ graphic: ["graphic"],
448
+ sequence: ["src", "frameCount", "frameDirection", "playing", "fps", "loop"],
449
+ video: [
450
+ "src",
451
+ "poster",
452
+ "playing",
453
+ "loop",
454
+ "muted",
455
+ "volume",
456
+ "playsInline",
457
+ "preload",
458
+ "crossOrigin"
459
+ ],
460
+ audio: [
461
+ "src",
462
+ "format",
463
+ "playing",
464
+ "loop",
465
+ "muted",
466
+ "volume",
467
+ "pauseWhenHidden",
468
+ "range",
469
+ "icon",
470
+ "playingIcon",
471
+ "marker",
472
+ "crossOrigin"
473
+ ],
474
+ text: [
475
+ "text",
476
+ "fontFamily",
477
+ "fontSize",
478
+ "fontWeight",
479
+ "fontStyle",
480
+ "color",
481
+ "background",
482
+ "backgroundOpacity",
483
+ "padding",
484
+ "align",
485
+ "verticalAlign",
486
+ "whiteSpace",
487
+ "lineHeight",
488
+ "borderRadius",
489
+ "stroke",
490
+ "strokeWidth"
491
+ ],
492
+ iframe: [
493
+ "src",
494
+ "title",
495
+ "sandbox",
496
+ "allow",
497
+ "referrerPolicy",
498
+ "loading",
499
+ "pointerPolicy",
500
+ "background"
501
+ ],
502
+ polygon: [
503
+ "vertices",
504
+ "fill",
505
+ "fillOpacity",
506
+ "stroke",
507
+ "strokeWidth",
508
+ "strokeOpacity",
509
+ "strokeDashSize",
510
+ "strokeGapSize"
511
+ ],
512
+ polyline: [
513
+ "vertices",
514
+ "stroke",
515
+ "strokeWidth",
516
+ "strokeOpacity",
517
+ "strokeDashSize",
518
+ "strokeGapSize"
519
+ ]
520
+ };
521
+ var booleanFields = /* @__PURE__ */ new Set([
522
+ "visible",
523
+ "interactive",
524
+ "playing",
525
+ "loop",
526
+ "muted",
527
+ "playsInline",
528
+ "pauseWhenHidden",
529
+ "marker"
530
+ ]);
531
+ var numberFields = /* @__PURE__ */ new Set([
532
+ "renderOrder",
533
+ "tooltipOffset",
534
+ "width",
535
+ "height",
536
+ "scale",
537
+ "distance",
538
+ "referenceFov",
539
+ "rotation",
540
+ "opacity",
541
+ "frameCount",
542
+ "fps",
543
+ "volume",
544
+ "range",
545
+ "fontSize",
546
+ "backgroundOpacity",
547
+ "padding",
548
+ "lineHeight",
549
+ "borderRadius",
550
+ "strokeWidth",
551
+ "fillOpacity",
552
+ "strokeOpacity",
553
+ "strokeDashSize",
554
+ "strokeGapSize"
555
+ ]);
556
+ var positiveFields = /* @__PURE__ */ new Set([
557
+ "width",
558
+ "height",
559
+ "scale",
560
+ "distance",
561
+ "referenceFov",
562
+ "frameCount",
563
+ "fps",
564
+ "fontSize",
565
+ "lineHeight"
566
+ ]);
567
+ var enums = {
568
+ pointerEvents: ["auto", "none"],
569
+ tooltipTrigger: ["always", "hover", "click"],
570
+ tooltipPlacement: ["top", "bottom", "left", "right"],
571
+ mode: ["surface", "billboard"],
572
+ scaleMode: ["fov", "fixed"],
573
+ frameDirection: ["horizontal", "vertical"],
574
+ preload: ["none", "metadata", "auto"],
575
+ crossOrigin: ["", "anonymous", "use-credentials"],
576
+ fontStyle: ["normal", "italic"],
577
+ align: ["left", "center", "right"],
578
+ verticalAlign: ["top", "middle", "bottom"],
579
+ whiteSpace: ["normal", "nowrap"],
580
+ loading: ["eager", "lazy"],
581
+ pointerPolicy: ["hotspot", "content"],
582
+ referrerPolicy: [
583
+ "",
584
+ "no-referrer",
585
+ "no-referrer-when-downgrade",
586
+ "origin",
587
+ "origin-when-cross-origin",
588
+ "same-origin",
589
+ "strict-origin",
590
+ "strict-origin-when-cross-origin",
591
+ "unsafe-url"
592
+ ]
593
+ };
594
+ function finite(value, field) {
595
+ if (typeof value !== "number" || !Number.isFinite(value))
596
+ throw new Error(`${field}: expected a finite number`);
597
+ return value;
598
+ }
599
+ function parsePosition(value, field) {
600
+ if (!isRecord(value)) throw new Error(`${field}: expected yaw and pitch`);
601
+ const yaw = finite(value.yaw, `${field}.yaw`);
602
+ const pitch = finite(value.pitch, `${field}.pitch`);
603
+ if (Math.abs(pitch) > 90)
604
+ throw new Error(`${field}.pitch: expected -90 to 90`);
605
+ return { yaw, pitch };
606
+ }
607
+ function parseGraphic(value) {
608
+ if (!isRecord(value)) throw new Error("graphic: expected an object");
609
+ const kind = requiredString(value.kind, "graphic.kind");
610
+ if (![
611
+ "circle",
612
+ "triangle",
613
+ "diamond",
614
+ "star",
615
+ "arrow",
616
+ "rectangle",
617
+ "ring",
618
+ "svg",
619
+ "path"
620
+ ].includes(kind))
621
+ throw new Error("graphic.kind: unsupported graphic");
622
+ const result = { kind };
623
+ for (const key of ["fill", "stroke"]) {
624
+ if (value[key] !== void 0)
625
+ result[key] = requiredString(value[key], `graphic.${key}`);
626
+ }
627
+ for (const key of ["strokeWidth", "cornerRadius", "innerRadius"]) {
628
+ if (value[key] !== void 0)
629
+ result[key] = finite(value[key], `graphic.${key}`);
630
+ }
631
+ if (kind === "svg") result.src = requiredString(value.src, "graphic.src");
632
+ if (kind === "path") {
633
+ result.path = requiredString(value.path, "graphic.path");
634
+ if (!Array.isArray(value.viewBox) || value.viewBox.length !== 4)
635
+ throw new Error("graphic.viewBox: expected four numbers");
636
+ const viewBox = value.viewBox.map(
637
+ (n, i) => finite(n, `graphic.viewBox[${i}]`)
638
+ );
639
+ if (viewBox[2] <= 0 || viewBox[3] <= 0)
640
+ throw new Error("graphic.viewBox: width and height must be positive");
641
+ result.viewBox = viewBox;
642
+ }
643
+ return result;
644
+ }
645
+ function parseTooltip(value) {
646
+ if (typeof value === "string") return value;
647
+ if (!isRecord(value)) throw new Error("tooltip: expected text or an object");
648
+ const result = {};
649
+ for (const key of ["text", "image", "imageAlt"]) {
650
+ if (value[key] !== void 0) {
651
+ if (typeof value[key] !== "string")
652
+ throw new Error(`tooltip.${key}: expected a string`);
653
+ result[key] = value[key];
654
+ }
655
+ }
656
+ return result;
657
+ }
658
+ function parseAppearance(value) {
659
+ if (!isRecord(value))
660
+ throw new Error("tooltipAppearance: expected an object");
661
+ const result = {};
662
+ for (const key of [
663
+ "background",
664
+ "color",
665
+ "border",
666
+ "borderRadius",
667
+ "shadow",
668
+ "padding",
669
+ "fontSize",
670
+ "fontFamily",
671
+ "lineHeight",
672
+ "letterSpacing"
673
+ ]) {
674
+ const item = value[key];
675
+ if (item === void 0) continue;
676
+ if (["background", "color", "border", "shadow", "fontFamily"].includes(key) && typeof item !== "string")
677
+ throw new Error(`tooltipAppearance.${key}: expected a string`);
678
+ if (typeof item !== "string" && (typeof item !== "number" || !Number.isFinite(item)))
679
+ throw new Error(
680
+ `tooltipAppearance.${key}: expected a string or finite number`
681
+ );
682
+ result[key] = item;
683
+ }
684
+ return result;
685
+ }
686
+ function parseHotspot(value, sceneId, field) {
687
+ try {
688
+ if (!isRecord(value)) throw new Error("expected a hotspot object");
689
+ const type = requiredString(value.type, "type");
690
+ if (!Object.prototype.hasOwnProperty.call(fields, type))
691
+ throw new Error("type: unsupported hotspot type");
692
+ const kind = type;
693
+ const result = {
694
+ type,
695
+ id: requiredString(value.id, "id")
696
+ };
697
+ const isPath = kind === "polygon" || kind === "polyline";
698
+ const allowed = [...common, ...isPath ? [] : point, ...fields[kind]];
699
+ for (const key of allowed) {
700
+ const item = value[key];
701
+ if (item === void 0) continue;
702
+ if (key === "position") result[key] = parsePosition(item, key);
703
+ else if (key === "vertices") {
704
+ if (!Array.isArray(item) || item.length < (kind === "polygon" ? 3 : 2))
705
+ throw new Error("vertices: not enough points");
706
+ result[key] = item.map((p, i) => parsePosition(p, `vertices[${i}]`));
707
+ } else if (key === "graphic") result[key] = parseGraphic(item);
708
+ else if (key === "tooltip") result[key] = parseTooltip(item);
709
+ else if (key === "tooltipAppearance") result[key] = parseAppearance(item);
710
+ else if (booleanFields.has(key)) {
711
+ if (typeof item !== "boolean")
712
+ throw new Error(`${key}: expected a boolean`);
713
+ result[key] = item;
714
+ } else if (numberFields.has(key)) {
715
+ const n = finite(item, key);
716
+ if (positiveFields.has(key) && n <= 0)
717
+ throw new Error(`${key}: expected a positive number`);
718
+ if (key === "frameCount" && !Number.isSafeInteger(n))
719
+ throw new Error("frameCount: expected an integer");
720
+ if ([
721
+ "opacity",
722
+ "fillOpacity",
723
+ "strokeOpacity",
724
+ "volume",
725
+ "backgroundOpacity"
726
+ ].includes(key) && (n < 0 || n > 1))
727
+ throw new Error(`${key}: expected 0 to 1`);
728
+ result[key] = n;
729
+ } else if (key === "fontWeight" && typeof item === "number")
730
+ result[key] = finite(item, key);
731
+ else if ((key === "src" || key === "format") && kind === "audio" && Array.isArray(item)) {
732
+ if (!item.length)
733
+ throw new Error(`${key}: expected audio ${key === "src" ? "sources" : "formats"}`);
734
+ result[key] = item.map((entry, i) => requiredString(entry, `${key}[${i}]`));
735
+ } else {
736
+ if (typeof item !== "string")
737
+ throw new Error(`${key}: expected a string`);
738
+ if (enums[key] && !enums[key].includes(item))
739
+ throw new Error(`${key}: unsupported value`);
740
+ result[key] = item;
741
+ }
742
+ }
743
+ if (!isPath && !result.position) throw new Error("position: required");
744
+ if (isPath && !result.vertices) throw new Error("vertices: required");
745
+ if (["image", "sequence", "video", "audio", "iframe"].includes(kind) && !(typeof result.src === "string" ? result.src.trim() : Array.isArray(result.src)))
746
+ throw new Error("src: required");
747
+ if (kind === "graphic" && !result.graphic)
748
+ throw new Error("graphic: required");
749
+ if (kind === "text" && typeof result.text !== "string")
750
+ throw new Error("text: required");
751
+ if (kind === "sequence" && !result.frameCount)
752
+ throw new Error("frameCount: required");
753
+ if (value.action !== void 0) result.action = parseAction(value.action);
754
+ if (value.events !== void 0) {
755
+ if (!isRecord(value.events)) throw new Error("events: expected an object");
756
+ const events = {};
757
+ for (const key of Object.keys(value.events)) {
758
+ if (!projectHotspotTriggers.includes(key)) throw new Error(`events.${key}: unsupported trigger`);
759
+ try {
760
+ events[key] = parseAction(value.events[key]);
761
+ } catch (error) {
762
+ throw new Error(`events.${key}.${error instanceof Error ? error.message : "invalid action"}`);
763
+ }
764
+ }
765
+ result.events = events;
766
+ }
767
+ return result;
768
+ } catch (cause) {
769
+ const detail = cause instanceof Error ? cause.message : "Invalid hotspot";
770
+ throw new PanoDataError("INVALID_PROJECT", `${field}.${detail}`, {
771
+ sceneId,
772
+ field: `${field}.${detail.split(":")[0]}`,
773
+ cause
774
+ });
775
+ }
776
+ }
777
+ function resolveHotspotAssets(hotspot, baseUrl, sceneId, resolver) {
778
+ const resolve = (path) => path ? mapResource(
779
+ path,
780
+ {
781
+ kind: "hotspot",
782
+ sceneId,
783
+ hotspotId: hotspot.id,
784
+ defaultUrl: resolveResource(baseUrl, path)
785
+ },
786
+ resolver
787
+ ) : path;
788
+ const result = { ...hotspot };
789
+ if (result.cursor) result.cursor = mapCursorAssets(result.cursor, resolve);
790
+ if (result.tooltipAppearance?.background) result.tooltipAppearance = {
791
+ ...result.tooltipAppearance,
792
+ background: mapCursorAssets(result.tooltipAppearance.background, resolve)
793
+ };
794
+ if ("src" in result)
795
+ result.src = Array.isArray(result.src) ? result.src.map(resolve) : resolve(result.src);
796
+ if ("poster" in result && result.poster)
797
+ result.poster = resolve(result.poster);
798
+ if (result.type === "audio") {
799
+ if (result.icon) result.icon = resolve(result.icon);
800
+ if (result.playingIcon) result.playingIcon = resolve(result.playingIcon);
801
+ }
802
+ if (result.type === "graphic" && result.graphic.kind === "svg")
803
+ result.graphic = { ...result.graphic, src: resolve(result.graphic.src) };
804
+ if (typeof result.tooltip === "object")
805
+ result.tooltip = {
806
+ ...result.tooltip,
807
+ ...result.tooltip.image ? { image: resolve(result.tooltip.image) } : {}
808
+ };
809
+ return result;
810
+ }
811
+
812
+ // src/data/project.ts
813
+ function optionalString(record, key) {
814
+ return record[key] === void 0 ? void 0 : requiredString(record[key], key);
815
+ }
816
+ function parsePanoProject(value, options = {}) {
817
+ if (!isRecord(value))
818
+ throw new PanoDataError("INVALID_PROJECT", "Expected a project object");
819
+ if (value.schemaVersion !== 1)
820
+ throw new PanoDataError(
821
+ "UNSUPPORTED_SCHEMA_VERSION",
822
+ "Unsupported project schemaVersion",
823
+ { field: "schemaVersion" }
824
+ );
825
+ try {
826
+ const popups = parsePopups(value.popups, options.draft);
827
+ if (!Array.isArray(value.scenes) || !options.draft && !value.scenes.length)
828
+ throw new Error("scenes: expected a nonempty array");
829
+ const ids = /* @__PURE__ */ new Set();
830
+ const scenes = value.scenes.map(
831
+ (entry, index) => {
832
+ let id;
833
+ const field = `scenes[${index}]`;
834
+ try {
835
+ if (!isRecord(entry)) throw new Error("expected a scene object");
836
+ id = requiredString(entry.id, "id");
837
+ if (ids.has(id)) throw new Error("id: duplicate scene ID");
838
+ ids.add(id);
839
+ if (!isRecord(entry.source))
840
+ throw new Error("source: expected an object");
841
+ const src = entry.source;
842
+ if (src.viewerUrl !== void 0 === (src.viewerConfig !== void 0))
843
+ throw new Error(
844
+ "source: provide exactly one of viewerUrl or viewerConfig"
845
+ );
846
+ const baseUrl = optionalString(src, "baseUrl");
847
+ const source = src.viewerUrl !== void 0 ? {
848
+ viewerUrl: requiredString(src.viewerUrl, "source.viewerUrl"),
849
+ baseUrl
850
+ } : { viewerConfig: parseConfig(src.viewerConfig), baseUrl };
851
+ const viewLimits = parseViewLimits(entry.viewLimits);
852
+ let initialView;
853
+ if (entry.initialView !== void 0) {
854
+ if (!isRecord(entry.initialView))
855
+ throw new Error("initialView: expected an object");
856
+ const input = entry.initialView;
857
+ const position = parsePosition(
858
+ { yaw: input.yaw ?? 0, pitch: input.pitch ?? 0 },
859
+ "initialView"
860
+ );
861
+ const fov = input.fov ?? 75;
862
+ if (typeof fov !== "number" || !Number.isFinite(fov) || fov <= 0 || fov >= 180)
863
+ throw new Error(
864
+ "initialView.fov: expected a number between 0 and 180"
865
+ );
866
+ initialView = { ...position, fov };
867
+ }
868
+ if (entry.hotspots !== void 0 && !Array.isArray(entry.hotspots))
869
+ throw new Error("hotspots: expected an array");
870
+ const hotspotIds = /* @__PURE__ */ new Set();
871
+ const hotspots = (entry.hotspots ?? []).map(
872
+ (hotspot, i) => {
873
+ const result = parseHotspot(
874
+ hotspot,
875
+ id,
876
+ `${field}.hotspots[${i}]`
877
+ );
878
+ if (hotspotIds.has(result.id))
879
+ throw new Error(`hotspots[${i}].id: duplicate hotspot ID`);
880
+ hotspotIds.add(result.id);
881
+ return result;
882
+ }
883
+ );
884
+ return {
885
+ id,
886
+ title: optionalString(entry, "title"),
887
+ source,
888
+ initialView,
889
+ ...viewLimits === void 0 ? {} : { viewLimits },
890
+ hotspots
891
+ };
892
+ } catch (cause) {
893
+ if (cause instanceof PanoDataError && cause.sceneId) throw cause;
894
+ const detail = cause instanceof Error ? cause.message : "Invalid scene";
895
+ throw new PanoDataError(
896
+ cause instanceof PanoDataError ? cause.code : "INVALID_PROJECT",
897
+ `${field}.${detail}`,
898
+ {
899
+ sceneId: id,
900
+ field: `${field}.${cause instanceof PanoDataError ? `source.viewerConfig.${cause.field ?? ""}` : detail.split(":")[0]}`,
901
+ cause
902
+ }
903
+ );
904
+ }
905
+ }
906
+ );
907
+ const initialSceneId = optionalString(value, "initialSceneId") ?? scenes[0]?.id;
908
+ if (initialSceneId !== void 0 && !ids.has(initialSceneId))
909
+ throw new PanoDataError(
910
+ "INVALID_REFERENCE",
911
+ "initialSceneId: scene does not exist",
912
+ { field: "initialSceneId" }
913
+ );
914
+ for (const [index, scene] of scenes.entries()) {
915
+ for (const [i, hotspot] of (scene.hotspots ?? []).entries()) {
916
+ for (const trigger of projectHotspotTriggers) {
917
+ const action = hotspotAction(hotspot, trigger);
918
+ if (action?.type === "popup" && !popups?.some((popup) => popup.id === action.popupId))
919
+ throw new PanoDataError("INVALID_REFERENCE", "Hotspot target popup does not exist", {
920
+ sceneId: scene.id,
921
+ field: `scenes[${index}].hotspots[${i}].${hotspot.events?.[trigger] ? `events.${trigger}` : "action"}.popupId`
922
+ });
923
+ if (action?.type === "scene" && !ids.has(action.sceneId))
924
+ throw new PanoDataError(
925
+ "INVALID_REFERENCE",
926
+ "Hotspot target scene does not exist",
927
+ {
928
+ sceneId: scene.id,
929
+ field: `scenes[${index}].hotspots[${i}].${hotspot.events?.[trigger] ? `events.${trigger}` : "action"}.sceneId`
930
+ }
931
+ );
932
+ }
933
+ }
934
+ }
935
+ return {
936
+ schemaVersion: 1,
937
+ title: optionalString(value, "title"),
938
+ initialSceneId,
939
+ scenes,
940
+ ...popups === void 0 ? {} : { popups }
941
+ };
942
+ } catch (cause) {
943
+ if (cause instanceof PanoDataError) throw cause;
944
+ const message = cause instanceof Error ? cause.message : "Invalid project";
945
+ throw new PanoDataError("INVALID_PROJECT", message, {
946
+ cause,
947
+ field: message.split(":")[0]
948
+ });
949
+ }
950
+ }
951
+ async function loadPanoProject(source, options = {}) {
952
+ aborted(options.requestInit?.signal);
953
+ let definition;
954
+ let baseUrl;
955
+ if (typeof source === "string") {
956
+ const response = await readJson(documentUrl(source), options);
957
+ definition = parsePanoProject(response.value);
958
+ baseUrl = options.baseUrl ?? new URL(".", response.url).href;
959
+ } else {
960
+ if (!options.baseUrl)
961
+ throw new PanoDataError(
962
+ "INVALID_OPTIONS",
963
+ "baseUrl is required for project object input"
964
+ );
965
+ definition = parsePanoProject(source);
966
+ baseUrl = options.baseUrl;
967
+ }
968
+ const root = directoryUrl(documentUrl(baseUrl));
969
+ const controller = new AbortController();
970
+ const signal = options.requestInit?.signal;
971
+ const cancel = () => controller.abort();
972
+ signal?.addEventListener("abort", cancel, { once: true });
973
+ const scenes = new Array(definition.scenes.length);
974
+ let next = 0;
975
+ let failure;
976
+ async function worker() {
977
+ while (next < definition.scenes.length && !controller.signal.aborted) {
978
+ const index = next++;
979
+ const item = definition.scenes[index];
980
+ try {
981
+ const sourceBase = item.source.baseUrl === void 0 ? void 0 : resolveResource(root, item.source.baseUrl);
982
+ const sceneOptions = {
983
+ id: item.id,
984
+ resolveAssetUrl: options.resolveAssetUrl
985
+ };
986
+ const scene = item.source.viewerUrl !== void 0 ? await loadSceneFromJson(
987
+ resolveResource(root, item.source.viewerUrl),
988
+ {
989
+ ...sceneOptions,
990
+ baseUrl: sourceBase,
991
+ requestInit: {
992
+ ...options.requestInit,
993
+ signal: controller.signal
994
+ }
995
+ }
996
+ ) : sceneFromViewerConfig(item.source.viewerConfig, {
997
+ ...sceneOptions,
998
+ baseUrl: sourceBase ?? root
999
+ });
1000
+ aborted(controller.signal);
1001
+ if (item.viewLimits !== void 0) scene.viewLimits = item.viewLimits;
1002
+ scenes[index] = {
1003
+ scene,
1004
+ title: item.title,
1005
+ initialView: item.initialView ? { yaw: 0, pitch: 0, fov: 75, ...item.initialView } : void 0,
1006
+ hotspots: item.hotspots.map(
1007
+ (h) => resolveHotspotAssets(h, root, item.id, options.resolveAssetUrl)
1008
+ )
1009
+ };
1010
+ } catch (cause) {
1011
+ if (!failure) {
1012
+ const error = cause instanceof PanoDataError ? cause : new PanoDataError(
1013
+ "INVALID_CONFIG",
1014
+ cause instanceof Error ? cause.message : "Could not load scene",
1015
+ { cause }
1016
+ );
1017
+ const field = `scenes[${index}].${error.field ?? (item.source.viewerUrl !== void 0 ? "source.viewerUrl" : "source.viewerConfig")}`;
1018
+ failure = new PanoDataError(
1019
+ error.code,
1020
+ `Scene ${item.id}: ${error.message}`,
1021
+ { status: error.status, sceneId: item.id, field, cause: error }
1022
+ );
1023
+ }
1024
+ controller.abort();
1025
+ }
1026
+ }
1027
+ }
1028
+ try {
1029
+ aborted(signal);
1030
+ await Promise.all(
1031
+ Array.from({ length: Math.min(4, scenes.length) }, worker)
1032
+ );
1033
+ aborted(signal);
1034
+ if (failure) throw failure;
1035
+ return {
1036
+ title: definition.title,
1037
+ initialSceneId: definition.initialSceneId,
1038
+ scenes,
1039
+ ...definition.popups === void 0 ? {} : { popups: definition.popups.map((popup) => resolveProjectPopupAssets(popup, root, options.resolvePopupAssetUrl)) }
1040
+ };
1041
+ } finally {
1042
+ signal?.removeEventListener("abort", cancel);
1043
+ }
1044
+ }
1045
+ export {
1046
+ PanoDataError,
1047
+ hotspotAction,
1048
+ loadPanoProject,
1049
+ loadSceneFromJson,
1050
+ parsePanoProject,
1051
+ projectHotspotTriggers,
1052
+ resolveHotspotAssets as resolveProjectHotspotAssets,
1053
+ resolveProjectPopupAssets,
1054
+ sceneFromViewerConfig,
1055
+ setHotspotAction
1056
+ };
1057
+ //# sourceMappingURL=data.mjs.map