@ikaros-arch/react-3dhop-iiif 0.1.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/index.js ADDED
@@ -0,0 +1,1979 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DEFAULT_FALLBACK_LANGUAGES: () => DEFAULT_FALLBACK_LANGUAGES,
24
+ DEFAULT_MEASURE_UNIT: () => DEFAULT_MEASURE_UNIT,
25
+ FORMAT_PREFERENCE: () => FORMAT_PREFERENCE,
26
+ IIIFCollectionCarousel: () => IIIFCollectionCarousel,
27
+ IIIFCollectionPicker: () => IIIFCollectionPicker,
28
+ IIIFCollectionProvider: () => IIIFCollectionProvider,
29
+ IIIFLanguageSwitcher: () => IIIFLanguageSwitcher,
30
+ IIIFMetadataPanel: () => IIIFMetadataPanel,
31
+ IIIFModelsPanel: () => IIIFModelsPanel,
32
+ IIIFMultiManifestModelsPanel: () => IIIFMultiManifestModelsPanel,
33
+ IIIFMultiManifestProvider: () => IIIFMultiManifestProvider,
34
+ IIIFMultiManifestViewer: () => IIIFMultiManifestViewer,
35
+ IIIFProvider: () => IIIFProvider,
36
+ IIIFSavedViewsPanel: () => IIIFSavedViewsPanel,
37
+ IIIFSummary: () => IIIFSummary,
38
+ IIIFViewer: () => IIIFViewer,
39
+ LANGUAGE_NAMES: () => LANGUAGE_NAMES,
40
+ UNITS_IN_METRES: () => UNITS_IN_METRES,
41
+ buildModelMatrix: () => buildModelMatrix,
42
+ cameraToView: () => cameraToView,
43
+ collectLanguages: () => collectLanguages,
44
+ composeTransforms: () => composeTransforms,
45
+ getUnitScaleFactor: () => getUnitScaleFactor,
46
+ isKnownUnit: () => isKnownUnit,
47
+ layoutManifests: () => layoutManifests,
48
+ loadCollection: () => loadCollection,
49
+ loadManifest: () => loadManifest,
50
+ parseCollection: () => parseCollection,
51
+ parseManifest: () => parseManifest,
52
+ resolveLanguageMap: () => resolveLanguageMap,
53
+ sceneFromManifest: () => sceneFromManifest,
54
+ sceneFromManifests: () => sceneFromManifests,
55
+ selectBestSource: () => selectBestSource,
56
+ track2view: () => track2view,
57
+ transformToMatrix: () => transformToMatrix,
58
+ useIIIFCollection: () => useIIIFCollection,
59
+ useIIIFManifest: () => useIIIFManifest,
60
+ useIIIFMultiManifest: () => useIIIFMultiManifest,
61
+ view2track: () => view2track,
62
+ viewToCameraAnnotation: () => viewToCameraAnnotation
63
+ });
64
+ module.exports = __toCommonJS(index_exports);
65
+
66
+ // src/IIIFViewer.tsx
67
+ var import_react2 = require("react");
68
+ var import_react_3dhop2 = require("@ikaros-arch/react-3dhop");
69
+
70
+ // src/context.tsx
71
+ var import_react = require("react");
72
+ var import_react_3dhop = require("@ikaros-arch/react-3dhop");
73
+
74
+ // src/iiif/mat4.ts
75
+ function identity() {
76
+ return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
77
+ }
78
+ function translation([x, y, z]) {
79
+ return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1];
80
+ }
81
+ function scaling([x, y, z]) {
82
+ return [x, 0, 0, 0, 0, y, 0, 0, 0, 0, z, 0, 0, 0, 0, 1];
83
+ }
84
+ function degToRad(degrees) {
85
+ return degrees * Math.PI / 180;
86
+ }
87
+ function radToDeg(radians) {
88
+ return radians * 180 / Math.PI;
89
+ }
90
+ function rotationAngleAxis(angle, axis) {
91
+ const length = Math.hypot(axis[0], axis[1], axis[2]);
92
+ if (length === 0) {
93
+ return identity();
94
+ }
95
+ const x = axis[0] / length;
96
+ const y = axis[1] / length;
97
+ const z = axis[2] / length;
98
+ const s = Math.sin(angle);
99
+ const c = Math.cos(angle);
100
+ const t = 1 - c;
101
+ return [
102
+ t * x * x + c,
103
+ t * x * y + z * s,
104
+ t * z * x - y * s,
105
+ 0,
106
+ t * x * y - z * s,
107
+ t * y * y + c,
108
+ t * y * z + x * s,
109
+ 0,
110
+ t * z * x + y * s,
111
+ t * y * z - x * s,
112
+ t * z * z + c,
113
+ 0,
114
+ 0,
115
+ 0,
116
+ 0,
117
+ 1
118
+ ];
119
+ }
120
+ function multiply(a, b) {
121
+ const out = new Array(16);
122
+ for (let column = 0; column < 4; column += 1) {
123
+ for (let row = 0; row < 4; row += 1) {
124
+ let sum = 0;
125
+ for (let k = 0; k < 4; k += 1) {
126
+ sum += a[k * 4 + row] * b[column * 4 + k];
127
+ }
128
+ out[column * 4 + row] = sum;
129
+ }
130
+ }
131
+ return out;
132
+ }
133
+ function transformPoint(m, [x, y, z]) {
134
+ const w = m[3] * x + m[7] * y + m[11] * z + m[15];
135
+ const divisor = w === 0 ? 1 : w;
136
+ return [
137
+ (m[0] * x + m[4] * y + m[8] * z + m[12]) / divisor,
138
+ (m[1] * x + m[5] * y + m[9] * z + m[13]) / divisor,
139
+ (m[2] * x + m[6] * y + m[10] * z + m[14]) / divisor
140
+ ];
141
+ }
142
+
143
+ // src/iiif/camera.ts
144
+ var Y_AXIS = [0, 1, 0];
145
+ var NEGATIVE_X_AXIS = [-1, 0, 0];
146
+ var DEGENERATE = 1e-9;
147
+ function view2track(view, framing) {
148
+ const { sceneCenter, sceneRadiusInv } = framing;
149
+ const panX = (view.target[0] - sceneCenter[0]) * sceneRadiusInv;
150
+ const panY = (view.target[1] - sceneCenter[1]) * sceneRadiusInv;
151
+ const panZ = (view.target[2] - sceneCenter[2]) * sceneRadiusInv;
152
+ const direction = [
153
+ view.target[0] - view.position[0],
154
+ view.target[1] - view.position[1],
155
+ view.target[2] - view.position[2]
156
+ ];
157
+ const length = Math.hypot(direction[0], direction[1], direction[2]);
158
+ if (length === 0) {
159
+ return [0, 0, panX, panY, panZ, 0];
160
+ }
161
+ const unit = [direction[0] / length, direction[1] / length, direction[2] / length];
162
+ const distance = length * sceneRadiusInv;
163
+ const theta = radToDeg(-Math.asin(clamp(unit[1], -1, 1)));
164
+ let phi;
165
+ if (Math.hypot(unit[0], unit[2]) > DEGENERATE) {
166
+ phi = radToDeg(Math.atan2(-unit[0], -unit[2]));
167
+ } else if (view.up && Math.hypot(view.up[0], view.up[2]) > DEGENERATE) {
168
+ const sign = theta >= 0 ? 1 : -1;
169
+ phi = radToDeg(Math.atan2(sign * -view.up[0], sign * -view.up[2]));
170
+ } else {
171
+ phi = 0;
172
+ }
173
+ return [phi, theta, panX, panY, panZ, distance];
174
+ }
175
+ function track2view(trackState, framing, fov = 60) {
176
+ const { sceneCenter, sceneRadiusInv } = framing;
177
+ const [phi, theta, panX, panY, panZ, normalizedDistance] = trackState;
178
+ const target = [
179
+ panX / sceneRadiusInv + sceneCenter[0],
180
+ panY / sceneRadiusInv + sceneCenter[1],
181
+ panZ / sceneRadiusInv + sceneCenter[2]
182
+ ];
183
+ const orientation = multiply(
184
+ rotationAngleAxis(degToRad(phi), Y_AXIS),
185
+ rotationAngleAxis(degToRad(theta), NEGATIVE_X_AXIS)
186
+ );
187
+ const distance = normalizedDistance / sceneRadiusInv;
188
+ const offset = transformPoint(orientation, [0, 0, distance]);
189
+ const up = transformPoint(orientation, [0, 1, 0]);
190
+ return {
191
+ position: [offset[0] + target[0], offset[1] + target[1], offset[2] + target[2]],
192
+ target,
193
+ up,
194
+ fov
195
+ };
196
+ }
197
+ function cameraToView(camera, framing, models = []) {
198
+ let target = [...framing.sceneCenter];
199
+ if (camera.target) {
200
+ target = camera.target;
201
+ } else if (camera.lookAtId) {
202
+ const referenced = models.find((model) => model.id === camera.lookAtId);
203
+ if (referenced) {
204
+ target = referenced.position;
205
+ }
206
+ }
207
+ return {
208
+ position: camera.position,
209
+ target,
210
+ up: [0, 1, 0],
211
+ // An orthographic camera has no field of view; 0 is the sentinel 3DHOP's own tooling uses.
212
+ fov: camera.type === "OrthographicCamera" ? 0 : camera.fov ?? 60
213
+ };
214
+ }
215
+ function round(value, decimals) {
216
+ const factor = 10 ** decimals;
217
+ return Math.round(value * factor) / factor;
218
+ }
219
+ function viewToCameraAnnotation(view, { idBase = "https://example.org/iiif/3d", label, sceneId = "https://example.org/iiif/scene/1" } = {}) {
220
+ const timestamp = Date.now();
221
+ const isOrthographic = view.fov === 0;
222
+ return {
223
+ id: `${idBase}/camera_${timestamp}`,
224
+ type: "Annotation",
225
+ motivation: ["painting"],
226
+ body: [
227
+ {
228
+ id: `${idBase}/cameras/${timestamp}`,
229
+ type: isOrthographic ? "OrthographicCamera" : "PerspectiveCamera",
230
+ label: { en: [label ?? `Saved View ${(/* @__PURE__ */ new Date()).toLocaleString()}`] },
231
+ lookAt: {
232
+ type: "PointSelector",
233
+ x: round(view.target[0], 3),
234
+ y: round(view.target[1], 3),
235
+ z: round(view.target[2], 3)
236
+ },
237
+ ...isOrthographic ? {} : { fieldOfView: round(view.fov, 1) }
238
+ }
239
+ ],
240
+ target: [
241
+ {
242
+ type: "SpecificResource",
243
+ source: { id: sceneId, type: "Scene" },
244
+ selector: [
245
+ {
246
+ type: "PointSelector",
247
+ x: round(view.position[0], 3),
248
+ y: round(view.position[1], 3),
249
+ z: round(view.position[2], 3)
250
+ }
251
+ ]
252
+ }
253
+ ]
254
+ };
255
+ }
256
+ function clamp(value, min, max) {
257
+ return Math.min(max, Math.max(min, value));
258
+ }
259
+
260
+ // src/iiif/language.ts
261
+ var DEFAULT_FALLBACK_LANGUAGES = ["en", "no", "nb", "nn"];
262
+ function resolveLanguageMap(value, language, fallbacks = DEFAULT_FALLBACK_LANGUAGES) {
263
+ if (value == null) {
264
+ return "";
265
+ }
266
+ if (typeof value === "string") {
267
+ return value;
268
+ }
269
+ if (typeof value !== "object") {
270
+ return "";
271
+ }
272
+ const pick = (tag) => {
273
+ const entry = value[tag];
274
+ if (entry == null) {
275
+ return void 0;
276
+ }
277
+ if (Array.isArray(entry)) {
278
+ return entry.length > 0 ? entry[0] : void 0;
279
+ }
280
+ return entry.length > 0 ? entry : void 0;
281
+ };
282
+ const direct = pick(language);
283
+ if (direct !== void 0) {
284
+ return direct;
285
+ }
286
+ for (const fallback of fallbacks) {
287
+ if (fallback === language) {
288
+ continue;
289
+ }
290
+ const candidate = pick(fallback);
291
+ if (candidate !== void 0) {
292
+ return candidate;
293
+ }
294
+ }
295
+ for (const tag of Object.keys(value)) {
296
+ const candidate = pick(tag);
297
+ if (candidate !== void 0) {
298
+ return candidate;
299
+ }
300
+ }
301
+ return "";
302
+ }
303
+ var LANGUAGE_MAP_KEYS = /* @__PURE__ */ new Set(["label", "value", "summary"]);
304
+ function isLanguageMap(value) {
305
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
306
+ return false;
307
+ }
308
+ const entries = Object.entries(value);
309
+ return entries.length > 0 && entries.every(
310
+ ([, entry]) => typeof entry === "string" || Array.isArray(entry) && entry.every((item) => typeof item === "string")
311
+ );
312
+ }
313
+ function collectLanguages(value, into = /* @__PURE__ */ new Set()) {
314
+ if (value == null || typeof value !== "object") {
315
+ return into;
316
+ }
317
+ if (Array.isArray(value)) {
318
+ value.forEach((item) => collectLanguages(item, into));
319
+ return into;
320
+ }
321
+ Object.entries(value).forEach(([key, entry]) => {
322
+ if (LANGUAGE_MAP_KEYS.has(key) && isLanguageMap(entry)) {
323
+ Object.keys(entry).forEach((tag) => {
324
+ if (tag !== "none") {
325
+ into.add(tag);
326
+ }
327
+ });
328
+ return;
329
+ }
330
+ collectLanguages(entry, into);
331
+ });
332
+ return into;
333
+ }
334
+
335
+ // src/context.tsx
336
+ var import_jsx_runtime = require("react/jsx-runtime");
337
+ var IIIFContext = (0, import_react.createContext)(null);
338
+ function useIIIFManifest() {
339
+ const value = (0, import_react.useContext)(IIIFContext);
340
+ if (!value) {
341
+ throw new Error("useIIIFManifest must be used within an <IIIFViewer>.");
342
+ }
343
+ return value;
344
+ }
345
+ var IIIFProvider = ({
346
+ status,
347
+ error,
348
+ diagnostics,
349
+ parsed,
350
+ scene,
351
+ language,
352
+ setLanguage,
353
+ children
354
+ }) => {
355
+ const { presenter, registerSceneObserver } = (0, import_react_3dhop.useThreeDHopViewer)();
356
+ const [framing, setFraming] = (0, import_react.useState)(null);
357
+ const [visibility, setVisibility] = (0, import_react.useState)({});
358
+ const [transparency, setTransparency] = (0, import_react.useState)({});
359
+ (0, import_react.useEffect)(() => {
360
+ return registerSceneObserver((instance) => {
361
+ const center = instance.sceneCenter;
362
+ const radiusInv = instance.sceneRadiusInv;
363
+ if (Array.isArray(center) && center.length >= 3 && typeof radiusInv === "number") {
364
+ setFraming({ sceneCenter: [center[0], center[1], center[2]], sceneRadiusInv: radiusInv });
365
+ }
366
+ setVisibility({});
367
+ setTransparency({});
368
+ });
369
+ }, [registerSceneObserver]);
370
+ const localize = (0, import_react.useCallback)(
371
+ (value2) => resolveLanguageMap(value2, language),
372
+ [language]
373
+ );
374
+ const goToCamera = (0, import_react.useCallback)(
375
+ (index, durationSeconds = 0.5) => {
376
+ if (!parsed || !framing || !presenter) {
377
+ return;
378
+ }
379
+ const camera = parsed.cameras[index];
380
+ if (!camera) {
381
+ return;
382
+ }
383
+ const view = cameraToView(camera, framing, parsed.models);
384
+ const trackState = view2track(view, framing);
385
+ if (durationSeconds > 0 && typeof presenter.animateToTrackballPosition === "function") {
386
+ presenter.animateToTrackballPosition(trackState, durationSeconds);
387
+ } else {
388
+ presenter.setTrackballPosition?.(trackState);
389
+ presenter.repaint?.();
390
+ }
391
+ if (camera.type === "OrthographicCamera") {
392
+ presenter.setCameraOrthographic?.();
393
+ } else {
394
+ presenter.setCameraPerspective?.();
395
+ }
396
+ },
397
+ [framing, parsed, presenter]
398
+ );
399
+ const saveCurrentView = (0, import_react.useCallback)(
400
+ (label) => {
401
+ if (!presenter || !framing) {
402
+ return null;
403
+ }
404
+ const trackState = presenter.getTrackballPosition?.();
405
+ if (!Array.isArray(trackState) || trackState.length < 6) {
406
+ return null;
407
+ }
408
+ const isOrthographic = presenter.getCameraType?.() === "orthographic";
409
+ const fov = isOrthographic ? 0 : presenter._scene?.space?.cameraFOV ?? 60;
410
+ const view = track2view(trackState, framing, fov);
411
+ return viewToCameraAnnotation(view, { label, sceneId: parsed?.manifest.items?.[0]?.id });
412
+ },
413
+ [framing, parsed, presenter]
414
+ );
415
+ const instanceKeyFor = (0, import_react.useCallback)(
416
+ (modelId) => scene?.instanceIdsByModelId[modelId],
417
+ [scene]
418
+ );
419
+ const isModelVisible = (0, import_react.useCallback)(
420
+ (modelId) => visibility[modelId] ?? true,
421
+ [visibility]
422
+ );
423
+ const setModelVisible = (0, import_react.useCallback)(
424
+ (modelId, visible) => {
425
+ const key = instanceKeyFor(modelId);
426
+ if (!key || !presenter) {
427
+ return;
428
+ }
429
+ if ((visibility[modelId] ?? true) === visible) {
430
+ return;
431
+ }
432
+ presenter.toggleInstanceVisibilityByName?.(key, true);
433
+ setVisibility((current) => ({ ...current, [modelId]: visible }));
434
+ },
435
+ [instanceKeyFor, presenter, visibility]
436
+ );
437
+ const isModelTransparent = (0, import_react.useCallback)(
438
+ (modelId) => transparency[modelId] ?? false,
439
+ [transparency]
440
+ );
441
+ const toggleModelTransparency = (0, import_react.useCallback)(
442
+ (modelId) => {
443
+ const key = instanceKeyFor(modelId);
444
+ if (!key || !presenter) {
445
+ return;
446
+ }
447
+ presenter.toggleInstanceTransparencyByName?.(key, true);
448
+ setTransparency((current) => ({ ...current, [modelId]: !(current[modelId] ?? false) }));
449
+ },
450
+ [instanceKeyFor, presenter]
451
+ );
452
+ const value = (0, import_react.useMemo)(
453
+ () => ({
454
+ status,
455
+ error,
456
+ diagnostics,
457
+ parsed,
458
+ metadata: parsed?.metadata ?? null,
459
+ models: parsed?.models ?? [],
460
+ cameras: parsed?.cameras ?? [],
461
+ languages: parsed?.languages ?? [],
462
+ language,
463
+ setLanguage,
464
+ localize,
465
+ measureUnit: scene?.measureUnit ?? "mm",
466
+ displayUnit: scene?.displayUnit ?? "mm",
467
+ isSceneReady: framing !== null,
468
+ goToCamera,
469
+ saveCurrentView,
470
+ setModelVisible,
471
+ isModelVisible,
472
+ toggleModelTransparency,
473
+ isModelTransparent
474
+ }),
475
+ [
476
+ diagnostics,
477
+ error,
478
+ framing,
479
+ goToCamera,
480
+ isModelTransparent,
481
+ isModelVisible,
482
+ language,
483
+ localize,
484
+ parsed,
485
+ saveCurrentView,
486
+ scene,
487
+ setLanguage,
488
+ setModelVisible,
489
+ status,
490
+ toggleModelTransparency
491
+ ]
492
+ );
493
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IIIFContext.Provider, { value, children });
494
+ };
495
+
496
+ // src/iiif/parser.ts
497
+ var FORMAT_PREFERENCE = {
498
+ glb: 1,
499
+ gltf: 1,
500
+ "model/gltf-binary": 1,
501
+ "model/gltf+json": 1,
502
+ obj: 2,
503
+ "model/obj": 2,
504
+ ply: 3,
505
+ "application/ply": 3,
506
+ nexus: 4,
507
+ nxz: 4,
508
+ nxs: 4,
509
+ "application/octet-stream+nexus": 4
510
+ };
511
+ function first(value) {
512
+ if (Array.isArray(value)) {
513
+ return value.length > 0 ? value[0] : void 0;
514
+ }
515
+ return value ?? void 0;
516
+ }
517
+ function toVector3(selector) {
518
+ return [selector?.x ?? 0, selector?.y ?? 0, selector?.z ?? 0];
519
+ }
520
+ function readPointSelector(target) {
521
+ if (!target || typeof target === "string") {
522
+ return void 0;
523
+ }
524
+ const entry = first(target);
525
+ const selector = first(entry?.selector);
526
+ return selector?.type === "PointSelector" || selector?.x !== void 0 ? selector : void 0;
527
+ }
528
+ function isModelBody(body) {
529
+ return body?.type === "Model" || body?.type === "SpecificResource";
530
+ }
531
+ function isCameraBody(body) {
532
+ return body?.type === "PerspectiveCamera" || body?.type === "OrthographicCamera";
533
+ }
534
+ function selectBestSource(source) {
535
+ if (!source) {
536
+ return void 0;
537
+ }
538
+ if (!Array.isArray(source)) {
539
+ return source;
540
+ }
541
+ if (source.length <= 1) {
542
+ return source[0];
543
+ }
544
+ const rank = (candidate) => FORMAT_PREFERENCE[(candidate.format ?? "").toLowerCase()] ?? 0;
545
+ return [...source].sort((a, b) => rank(b) - rank(a))[0];
546
+ }
547
+ var KNOWN_FIELDS = {
548
+ museum: "museum",
549
+ "inventory number": "inventory",
550
+ inventarnummer: "inventory",
551
+ "object id": "objectId",
552
+ "objekt-id": "objectId",
553
+ "measure unit": "measureUnit",
554
+ "m\xE5leenhet": "measureUnit",
555
+ "display unit": "displayUnit",
556
+ visningsenhet: "displayUnit"
557
+ };
558
+ function parseMetadata(manifest, language) {
559
+ const resolve = (value) => resolveLanguageMap(value, language);
560
+ const metadata = {
561
+ label: resolve(manifest.label),
562
+ fields: []
563
+ };
564
+ const summary = resolve(manifest.summary);
565
+ if (summary) {
566
+ metadata.summary = summary;
567
+ }
568
+ const attribution = resolve(manifest.requiredStatement?.value);
569
+ if (attribution) {
570
+ metadata.attribution = attribution;
571
+ }
572
+ (manifest.metadata ?? []).forEach((entry) => {
573
+ const localizedLabel = resolve(entry.label);
574
+ const value = resolve(entry.value);
575
+ metadata.fields.push({ label: localizedLabel, value });
576
+ const canonicalKey = resolveLanguageMap(entry.label, "en").trim().toLowerCase();
577
+ const known = KNOWN_FIELDS[canonicalKey];
578
+ if (known) {
579
+ metadata[known] = value;
580
+ }
581
+ });
582
+ return metadata;
583
+ }
584
+ function parseModelAnnotation(annotation, body, onDiagnostic) {
585
+ let url;
586
+ let format;
587
+ let measureUnit;
588
+ let transforms = [];
589
+ if (body.type === "Model") {
590
+ url = body.id;
591
+ format = body.format;
592
+ measureUnit = body.measureUnit;
593
+ } else {
594
+ const source = selectBestSource(body.source);
595
+ if (!source) {
596
+ onDiagnostic?.({
597
+ level: "warning",
598
+ message: `Model annotation ${annotation.id ?? "(no id)"} has no usable source; skipping.`
599
+ });
600
+ return void 0;
601
+ }
602
+ url = source.id;
603
+ format = source.format;
604
+ measureUnit = source.measureUnit ?? body.measureUnit;
605
+ if (Array.isArray(body.transform)) {
606
+ transforms = body.transform;
607
+ }
608
+ }
609
+ if (!url) {
610
+ onDiagnostic?.({
611
+ level: "warning",
612
+ message: `Model annotation ${annotation.id ?? "(no id)"} has no source URL; skipping.`
613
+ });
614
+ return void 0;
615
+ }
616
+ const normalizedFormat = (format ?? "").toLowerCase();
617
+ const unrenderable = normalizedFormat.includes("gltf") ? "glTF" : normalizedFormat === "glb" || normalizedFormat === "model/gltf-binary" ? "glTF" : normalizedFormat === "obj" || normalizedFormat === "model/obj" ? "OBJ" : void 0;
618
+ if (unrenderable) {
619
+ onDiagnostic?.({
620
+ level: "warning",
621
+ message: `Model ${url} is ${unrenderable}, which 3DHOP cannot render. Convert it to Nexus (.nxz) or PLY.`
622
+ });
623
+ }
624
+ return {
625
+ id: annotation.id ?? url,
626
+ rawLabel: annotation.label,
627
+ url,
628
+ format,
629
+ measureUnit,
630
+ position: toVector3(readPointSelector(annotation.target)),
631
+ transforms
632
+ };
633
+ }
634
+ function parseCameraAnnotation(annotation, body) {
635
+ const camera = {
636
+ id: body.id,
637
+ type: body.type === "OrthographicCamera" ? "OrthographicCamera" : "PerspectiveCamera",
638
+ rawLabel: body.label ?? annotation.label,
639
+ position: toVector3(readPointSelector(annotation.target))
640
+ };
641
+ const fov = body.fieldOfView ?? body.fov;
642
+ if (typeof fov === "number") {
643
+ camera.fov = fov;
644
+ }
645
+ if (body.lookAt) {
646
+ if (body.lookAt.type === "PointSelector" || body.lookAt.x !== void 0) {
647
+ camera.target = toVector3(body.lookAt);
648
+ }
649
+ if (body.lookAt.id) {
650
+ camera.lookAtId = body.lookAt.id;
651
+ }
652
+ }
653
+ return camera;
654
+ }
655
+ function parseManifest(manifest, options = {}) {
656
+ const { language = "en", onDiagnostic } = options;
657
+ const models = [];
658
+ const cameras = [];
659
+ const scenes = (manifest.items ?? []).filter((item) => item?.type === "Scene");
660
+ if (scenes.length === 0) {
661
+ onDiagnostic?.({
662
+ level: "warning",
663
+ message: "Manifest contains no Scene; there is nothing to render."
664
+ });
665
+ }
666
+ if (scenes.length > 1) {
667
+ onDiagnostic?.({
668
+ level: "info",
669
+ message: `Manifest contains ${scenes.length} scenes; all of them are merged into one view.`
670
+ });
671
+ }
672
+ scenes.forEach((scene) => {
673
+ (scene.items ?? []).forEach((page) => {
674
+ if (page?.type !== "AnnotationPage") {
675
+ return;
676
+ }
677
+ (page.items ?? []).forEach((annotation) => {
678
+ const body = first(annotation?.body);
679
+ if (isModelBody(body)) {
680
+ const model = parseModelAnnotation(annotation, body, onDiagnostic);
681
+ if (model) {
682
+ models.push(model);
683
+ }
684
+ return;
685
+ }
686
+ if (isCameraBody(body)) {
687
+ cameras.push(parseCameraAnnotation(annotation, body));
688
+ return;
689
+ }
690
+ onDiagnostic?.({
691
+ level: "info",
692
+ message: `Skipping annotation ${annotation?.id ?? "(no id)"} of unsupported body type "${body?.type ?? "undefined"}".`
693
+ });
694
+ });
695
+ });
696
+ });
697
+ return {
698
+ manifest,
699
+ metadata: parseMetadata(manifest, language),
700
+ models,
701
+ cameras,
702
+ languages: Array.from(collectLanguages(manifest)).sort()
703
+ };
704
+ }
705
+ async function loadManifest(url, options = {}) {
706
+ const { signal, fetchImpl = fetch, ...parseOptions } = options;
707
+ const response = await fetchImpl(url, { signal });
708
+ if (!response.ok) {
709
+ throw new Error(`Failed to load IIIF manifest (${response.status} ${response.statusText}): ${url}`);
710
+ }
711
+ let json;
712
+ try {
713
+ json = await response.json();
714
+ } catch (cause) {
715
+ const detail = cause instanceof Error ? cause.message : String(cause);
716
+ throw new Error(`IIIF manifest at ${url} is not valid JSON: ${detail}`);
717
+ }
718
+ if (!json || typeof json !== "object") {
719
+ throw new Error(`IIIF manifest at ${url} is not a JSON object.`);
720
+ }
721
+ return parseManifest(json, parseOptions);
722
+ }
723
+
724
+ // src/iiif/transforms.ts
725
+ function transformToMatrix(transform) {
726
+ switch (transform.type) {
727
+ case "ScaleTransform":
728
+ return scaling([transform.x ?? 1, transform.y ?? 1, transform.z ?? 1]);
729
+ case "TranslateTransform":
730
+ return translation([transform.x ?? 0, transform.y ?? 0, transform.z ?? 0]);
731
+ case "RotateTransform": {
732
+ if (transform.axis !== void 0 || transform.angle !== void 0) {
733
+ const angle = degToRad(transform.angle ?? 0);
734
+ switch (transform.axis) {
735
+ case "x":
736
+ return rotationAngleAxis(angle, [1, 0, 0]);
737
+ case "y":
738
+ return rotationAngleAxis(angle, [0, 1, 0]);
739
+ case "z":
740
+ default:
741
+ return rotationAngleAxis(angle, [0, 0, 1]);
742
+ }
743
+ }
744
+ const rx = rotationAngleAxis(degToRad(transform.x ?? 0), [1, 0, 0]);
745
+ const ry = rotationAngleAxis(degToRad(transform.y ?? 0), [0, 1, 0]);
746
+ const rz = rotationAngleAxis(degToRad(transform.z ?? 0), [0, 0, 1]);
747
+ return multiply(multiply(rz, ry), rx);
748
+ }
749
+ default:
750
+ return identity();
751
+ }
752
+ }
753
+ function composeTransforms(transforms) {
754
+ if (!transforms || transforms.length === 0) {
755
+ return identity();
756
+ }
757
+ return transforms.reduce(
758
+ (accumulated, transform) => multiply(transformToMatrix(transform), accumulated),
759
+ identity()
760
+ );
761
+ }
762
+ function buildModelMatrix({
763
+ position = [0, 0, 0],
764
+ transforms,
765
+ sceneScale = 1,
766
+ geometryScale = sceneScale
767
+ }) {
768
+ const relativeGeometryScale = sceneScale === 0 ? 1 : geometryScale / sceneScale;
769
+ let matrix = composeTransforms(transforms);
770
+ if (relativeGeometryScale !== 1) {
771
+ matrix = multiply(matrix, scaling([relativeGeometryScale, relativeGeometryScale, relativeGeometryScale]));
772
+ }
773
+ if (position[0] !== 0 || position[1] !== 0 || position[2] !== 0) {
774
+ matrix = multiply(translation(position), matrix);
775
+ }
776
+ if (sceneScale !== 1) {
777
+ matrix = multiply(scaling([sceneScale, sceneScale, sceneScale]), matrix);
778
+ }
779
+ return matrix;
780
+ }
781
+
782
+ // src/iiif/units.ts
783
+ var UNITS_IN_METRES = {
784
+ km: 1e3,
785
+ m: 1,
786
+ cm: 0.01,
787
+ mm: 1e-3,
788
+ um: 1e-6,
789
+ "\xB5m": 1e-6,
790
+ nm: 1e-9
791
+ };
792
+ function isKnownUnit(unit) {
793
+ return typeof unit === "string" && Object.prototype.hasOwnProperty.call(UNITS_IN_METRES, unit.trim());
794
+ }
795
+ function getUnitScaleFactor(fromUnit, toUnit) {
796
+ const from = typeof fromUnit === "string" ? fromUnit.trim() : "";
797
+ const to = typeof toUnit === "string" ? toUnit.trim() : "";
798
+ if (from === to) {
799
+ return 1;
800
+ }
801
+ const fromFactor = UNITS_IN_METRES[from] ?? 1;
802
+ const toFactor = UNITS_IN_METRES[to] ?? 1;
803
+ return fromFactor / toFactor;
804
+ }
805
+
806
+ // src/iiif/toModels.ts
807
+ var DEFAULT_MEASURE_UNIT = "mm";
808
+ function sanitizeKey(value, fallback) {
809
+ const cleaned = value.replace(/[^A-Za-z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
810
+ return cleaned.length > 0 ? cleaned : fallback;
811
+ }
812
+ function sceneFromManifest(parsed, options = {}) {
813
+ const measureUnit = parsed.metadata.measureUnit?.trim() || DEFAULT_MEASURE_UNIT;
814
+ const displayUnit = options.displayUnit?.trim() || parsed.metadata.displayUnit?.trim() || measureUnit;
815
+ const sceneScale = getUnitScaleFactor(measureUnit, displayUnit);
816
+ const models = {};
817
+ const instanceIdsByModelId = {};
818
+ const meshIdByUrl = /* @__PURE__ */ new Map();
819
+ const usedKeys = /* @__PURE__ */ new Set();
820
+ parsed.models.forEach((model, index) => {
821
+ let meshId = meshIdByUrl.get(model.url);
822
+ if (!meshId) {
823
+ meshId = `iiif_mesh_${meshIdByUrl.size}`;
824
+ meshIdByUrl.set(model.url, meshId);
825
+ }
826
+ let key = sanitizeKey(model.id, `iiif_model_${index}`);
827
+ while (usedKeys.has(key)) {
828
+ key = `${key}_${index}`;
829
+ }
830
+ usedKeys.add(key);
831
+ const geometryScale = model.measureUnit ? getUnitScaleFactor(model.measureUnit, displayUnit) : sceneScale;
832
+ models[key] = {
833
+ url: model.url,
834
+ meshId,
835
+ // Pinned, because the scene builder otherwise derives the instance name from the entry key
836
+ // (`model_<key>`) and the panels address instances by the name recorded below.
837
+ instanceId: key,
838
+ transform: {
839
+ matrix: buildModelMatrix({
840
+ position: model.position,
841
+ transforms: model.transforms,
842
+ sceneScale,
843
+ geometryScale
844
+ })
845
+ },
846
+ tags: ["SPECIMEN"],
847
+ color: [0.5, 0.5, 0.5],
848
+ backfaceColor: [0.5, 0.5, 0.5, 3],
849
+ specularColor: [0, 0, 0, 256]
850
+ };
851
+ instanceIdsByModelId[model.id] = key;
852
+ });
853
+ const space = {
854
+ centerMode: "scene",
855
+ radiusMode: "scene",
856
+ cameraNearFar: [0.01, 5],
857
+ ...options.space
858
+ };
859
+ const config = {
860
+ pickedpointColor: [1, 0, 1],
861
+ measurementColor: [0.5, 1, 0.5],
862
+ showClippingPlanes: true,
863
+ showClippingBorder: true,
864
+ // A half-unit clipping border is invisible on a millimetre-scale object and enormous on a
865
+ // metre-scale one, so the metre case gets its own value.
866
+ clippingBorderSize: displayUnit === "m" ? 2e-3 : 0.5,
867
+ clippingBorderColor: [0, 1, 1],
868
+ ...options.config
869
+ };
870
+ const trackball = {
871
+ type: "TurntablePanTrackball",
872
+ ...options.trackball,
873
+ trackOptions: {
874
+ startPhi: 15,
875
+ startTheta: 15,
876
+ startDistance: 2,
877
+ minMaxPhi: [-180, 180],
878
+ minMaxTheta: [-90, 90],
879
+ minMaxDist: [0.1, 3],
880
+ ...options.trackball?.trackOptions
881
+ }
882
+ };
883
+ return { models, space, config, trackball, displayUnit, measureUnit, instanceIdsByModelId };
884
+ }
885
+
886
+ // src/IIIFViewer.tsx
887
+ var import_jsx_runtime2 = require("react/jsx-runtime");
888
+ function defaultLanguage() {
889
+ if (typeof navigator !== "undefined" && navigator.language) {
890
+ return navigator.language.split("-")[0];
891
+ }
892
+ return "en";
893
+ }
894
+ var IIIFViewer = ({
895
+ manifest,
896
+ language: languageProp,
897
+ displayUnit,
898
+ applyInitialCamera = true,
899
+ onLoad,
900
+ onError,
901
+ onDiagnostic,
902
+ loadingFallback = null,
903
+ errorFallback = null,
904
+ space,
905
+ config,
906
+ trackball,
907
+ children,
908
+ ...viewerProps
909
+ }) => {
910
+ const [language, setLanguage] = (0, import_react2.useState)(() => languageProp ?? defaultLanguage());
911
+ const [status, setStatus] = (0, import_react2.useState)("idle");
912
+ const [error, setError] = (0, import_react2.useState)(null);
913
+ const [parsed, setParsed] = (0, import_react2.useState)(null);
914
+ const [diagnostics, setDiagnostics] = (0, import_react2.useState)([]);
915
+ (0, import_react2.useEffect)(() => {
916
+ if (languageProp) {
917
+ setLanguage(languageProp);
918
+ }
919
+ }, [languageProp]);
920
+ (0, import_react2.useEffect)(() => {
921
+ const controller = new AbortController();
922
+ const collected = [];
923
+ const handleDiagnostic = (diagnostic) => {
924
+ collected.push(diagnostic);
925
+ onDiagnostic?.(diagnostic);
926
+ };
927
+ const finish = (result) => {
928
+ if (controller.signal.aborted) {
929
+ return;
930
+ }
931
+ setParsed(result);
932
+ setDiagnostics(collected);
933
+ setStatus("ready");
934
+ setError(null);
935
+ };
936
+ const fail = (cause) => {
937
+ if (controller.signal.aborted) {
938
+ return;
939
+ }
940
+ const normalized = cause instanceof Error ? cause : new Error(String(cause));
941
+ setParsed(null);
942
+ setDiagnostics(collected);
943
+ setStatus("error");
944
+ setError(normalized);
945
+ onError?.(normalized);
946
+ };
947
+ setStatus("loading");
948
+ if (typeof manifest === "string") {
949
+ loadManifest(manifest, { language, signal: controller.signal, onDiagnostic: handleDiagnostic }).then(finish).catch((cause) => {
950
+ if (cause instanceof DOMException && cause.name === "AbortError") {
951
+ return;
952
+ }
953
+ fail(cause);
954
+ });
955
+ } else {
956
+ try {
957
+ finish(parseManifest(manifest, { language, onDiagnostic: handleDiagnostic }));
958
+ } catch (cause) {
959
+ fail(cause);
960
+ }
961
+ }
962
+ return () => controller.abort();
963
+ }, [manifest, onDiagnostic, onError]);
964
+ const localized = (0, import_react2.useMemo)(() => {
965
+ if (!parsed) {
966
+ return null;
967
+ }
968
+ return parseManifest(parsed.manifest, { language });
969
+ }, [parsed, language]);
970
+ const scene = (0, import_react2.useMemo)(() => {
971
+ if (!localized) {
972
+ return null;
973
+ }
974
+ return sceneFromManifest(localized, { displayUnit, space, config, trackball });
975
+ }, [localized, displayUnit, space, config, trackball]);
976
+ (0, import_react2.useEffect)(() => {
977
+ if (status === "ready" && localized && scene) {
978
+ onLoad?.(localized, scene);
979
+ }
980
+ }, [status, localized, scene]);
981
+ if (status === "error" && error) {
982
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: typeof errorFallback === "function" ? errorFallback(error) : errorFallback });
983
+ }
984
+ if (!scene || !localized) {
985
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: loadingFallback });
986
+ }
987
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
988
+ import_react_3dhop2.ThreeDHopViewer,
989
+ {
990
+ ...viewerProps,
991
+ models: scene.models,
992
+ space: scene.space,
993
+ config: scene.config,
994
+ trackball: scene.trackball,
995
+ measurementUnits: viewerProps.measurementUnits ?? scene.displayUnit,
996
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
997
+ IIIFProvider,
998
+ {
999
+ status,
1000
+ error,
1001
+ diagnostics,
1002
+ parsed: localized,
1003
+ scene,
1004
+ language,
1005
+ setLanguage,
1006
+ children: [
1007
+ applyInitialCamera && localized.cameras.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(InitialCamera, {}) : null,
1008
+ children
1009
+ ]
1010
+ }
1011
+ )
1012
+ }
1013
+ );
1014
+ };
1015
+ var InitialCamera = () => {
1016
+ const [applied, setApplied] = (0, import_react2.useState)(false);
1017
+ const { goToCamera, cameras, isSceneReady } = useIIIFManifest();
1018
+ (0, import_react2.useEffect)(() => {
1019
+ if (applied || !isSceneReady || cameras.length === 0) {
1020
+ return;
1021
+ }
1022
+ goToCamera(0, 0);
1023
+ setApplied(true);
1024
+ }, [applied, cameras.length, goToCamera, isSceneReady]);
1025
+ return null;
1026
+ };
1027
+
1028
+ // src/IIIFMultiManifestViewer.tsx
1029
+ var import_react4 = require("react");
1030
+ var import_react_3dhop4 = require("@ikaros-arch/react-3dhop");
1031
+
1032
+ // src/multiManifestContext.tsx
1033
+ var import_react3 = require("react");
1034
+ var import_react_3dhop3 = require("@ikaros-arch/react-3dhop");
1035
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1036
+ var IIIFMultiManifestContext = (0, import_react3.createContext)(null);
1037
+ function useIIIFMultiManifest() {
1038
+ const value = (0, import_react3.useContext)(IIIFMultiManifestContext);
1039
+ if (!value) {
1040
+ throw new Error("useIIIFMultiManifest must be used within an <IIIFMultiManifestViewer>.");
1041
+ }
1042
+ return value;
1043
+ }
1044
+ function compositeKey(manifestKey, modelId) {
1045
+ return `${manifestKey}::${modelId}`;
1046
+ }
1047
+ var IIIFMultiManifestProvider = ({
1048
+ status,
1049
+ error,
1050
+ diagnostics,
1051
+ scene,
1052
+ children
1053
+ }) => {
1054
+ const { presenter, registerSceneObserver } = (0, import_react_3dhop3.useThreeDHopViewer)();
1055
+ const [isSceneReady, setIsSceneReady] = (0, import_react3.useState)(false);
1056
+ const [visibility, setVisibility] = (0, import_react3.useState)({});
1057
+ const [transparency, setTransparency] = (0, import_react3.useState)({});
1058
+ (0, import_react3.useEffect)(() => {
1059
+ return registerSceneObserver(() => {
1060
+ setIsSceneReady(true);
1061
+ setVisibility({});
1062
+ setTransparency({});
1063
+ });
1064
+ }, [registerSceneObserver]);
1065
+ const instanceKeyFor = (0, import_react3.useCallback)(
1066
+ (manifestKey, modelId) => scene?.manifests.find((manifest) => manifest.key === manifestKey)?.instanceIdsByModelId[modelId],
1067
+ [scene]
1068
+ );
1069
+ const isModelVisible = (0, import_react3.useCallback)(
1070
+ (manifestKey, modelId) => visibility[compositeKey(manifestKey, modelId)] ?? true,
1071
+ [visibility]
1072
+ );
1073
+ const setModelVisible = (0, import_react3.useCallback)(
1074
+ (manifestKey, modelId, visible) => {
1075
+ const key = instanceKeyFor(manifestKey, modelId);
1076
+ const stateKey = compositeKey(manifestKey, modelId);
1077
+ if (!key || !presenter) {
1078
+ return;
1079
+ }
1080
+ if ((visibility[stateKey] ?? true) === visible) {
1081
+ return;
1082
+ }
1083
+ presenter.toggleInstanceVisibilityByName?.(key, true);
1084
+ setVisibility((current) => ({ ...current, [stateKey]: visible }));
1085
+ },
1086
+ [instanceKeyFor, presenter, visibility]
1087
+ );
1088
+ const isModelTransparent = (0, import_react3.useCallback)(
1089
+ (manifestKey, modelId) => transparency[compositeKey(manifestKey, modelId)] ?? false,
1090
+ [transparency]
1091
+ );
1092
+ const toggleModelTransparency = (0, import_react3.useCallback)(
1093
+ (manifestKey, modelId) => {
1094
+ const key = instanceKeyFor(manifestKey, modelId);
1095
+ const stateKey = compositeKey(manifestKey, modelId);
1096
+ if (!key || !presenter) {
1097
+ return;
1098
+ }
1099
+ presenter.toggleInstanceTransparencyByName?.(key, true);
1100
+ setTransparency((current) => ({ ...current, [stateKey]: !(current[stateKey] ?? false) }));
1101
+ },
1102
+ [instanceKeyFor, presenter]
1103
+ );
1104
+ const manifests = (0, import_react3.useMemo)(
1105
+ () => scene?.manifests.map((manifest) => ({
1106
+ key: manifest.key,
1107
+ sourceId: manifest.sourceId,
1108
+ label: manifest.parsed.metadata.label,
1109
+ models: manifest.models
1110
+ })) ?? [],
1111
+ [scene]
1112
+ );
1113
+ const value = (0, import_react3.useMemo)(
1114
+ () => ({
1115
+ status,
1116
+ error,
1117
+ diagnostics,
1118
+ manifests,
1119
+ measureUnit: scene?.measureUnit ?? "mm",
1120
+ displayUnit: scene?.displayUnit ?? "mm",
1121
+ isSceneReady,
1122
+ setModelVisible,
1123
+ isModelVisible,
1124
+ toggleModelTransparency,
1125
+ isModelTransparent
1126
+ }),
1127
+ [
1128
+ diagnostics,
1129
+ error,
1130
+ isModelTransparent,
1131
+ isModelVisible,
1132
+ isSceneReady,
1133
+ manifests,
1134
+ scene,
1135
+ setModelVisible,
1136
+ status,
1137
+ toggleModelTransparency
1138
+ ]
1139
+ );
1140
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(IIIFMultiManifestContext.Provider, { value, children });
1141
+ };
1142
+
1143
+ // src/iiif/toMultiModels.ts
1144
+ function sanitizeKey2(value, fallback) {
1145
+ const cleaned = value.replace(/[^A-Za-z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
1146
+ return cleaned.length > 0 ? cleaned : fallback;
1147
+ }
1148
+ function placementSpreadRadius(models) {
1149
+ if (models.length <= 1) {
1150
+ return 0;
1151
+ }
1152
+ const centroid = models.reduce(
1153
+ (sum, model) => [sum[0] + model.position[0], sum[1] + model.position[1], sum[2] + model.position[2]],
1154
+ [0, 0, 0]
1155
+ );
1156
+ centroid[0] /= models.length;
1157
+ centroid[1] /= models.length;
1158
+ centroid[2] /= models.length;
1159
+ return models.reduce((max, model) => Math.max(max, Math.hypot(
1160
+ model.position[0] - centroid[0],
1161
+ model.position[1] - centroid[1],
1162
+ model.position[2] - centroid[2]
1163
+ )), 0);
1164
+ }
1165
+ function layoutManifests(radii, options) {
1166
+ const { gap, columns } = options;
1167
+ const offsets = [];
1168
+ let cursorX = 0;
1169
+ let cursorZ = 0;
1170
+ let rowMaxRadius = 0;
1171
+ let previousRadius = 0;
1172
+ radii.forEach((radius, index) => {
1173
+ const column = columns > 0 ? index % columns : index;
1174
+ if (column === 0 && index > 0) {
1175
+ cursorZ += rowMaxRadius + gap + radius;
1176
+ cursorX = 0;
1177
+ rowMaxRadius = 0;
1178
+ previousRadius = 0;
1179
+ } else if (index > 0) {
1180
+ cursorX += previousRadius + gap + radius;
1181
+ }
1182
+ offsets.push([cursorX, 0, cursorZ]);
1183
+ rowMaxRadius = Math.max(rowMaxRadius, radius);
1184
+ previousRadius = radius;
1185
+ });
1186
+ if (offsets.length === 0) {
1187
+ return offsets;
1188
+ }
1189
+ const minX = Math.min(...offsets.map((offset) => offset[0]));
1190
+ const maxX = Math.max(...offsets.map((offset) => offset[0]));
1191
+ const minZ = Math.min(...offsets.map((offset) => offset[2]));
1192
+ const maxZ = Math.max(...offsets.map((offset) => offset[2]));
1193
+ const centerX = (minX + maxX) / 2;
1194
+ const centerZ = (minZ + maxZ) / 2;
1195
+ return offsets.map(([x, y, z]) => [x - centerX, y, z - centerZ]);
1196
+ }
1197
+ function sceneFromManifests(parsedList, sourceIds, options = {}) {
1198
+ const gap = options.gap ?? 50;
1199
+ const minRadius = options.minRadius ?? 100;
1200
+ const columns = options.columns ?? parsedList.length;
1201
+ const displayUnit = options.displayUnit?.trim() || parsedList[0]?.metadata.displayUnit?.trim() || parsedList[0]?.metadata.measureUnit?.trim() || "mm";
1202
+ const perManifestScenes = parsedList.map((parsed) => sceneFromManifest(parsed, { displayUnit }));
1203
+ const radii = perManifestScenes.map(
1204
+ (_, index) => Math.max(minRadius, placementSpreadRadius(parsedList[index].models))
1205
+ );
1206
+ const autoOffsets = layoutManifests(radii, { gap, columns });
1207
+ const models = {};
1208
+ const usedManifestKeys = /* @__PURE__ */ new Set();
1209
+ const manifests = [];
1210
+ parsedList.forEach((parsed, index) => {
1211
+ const sourceId = sourceIds[index] ?? `manifest_${index}`;
1212
+ let manifestKey = sanitizeKey2(parsed.manifest.id ?? sourceId, `manifest_${index}`);
1213
+ while (usedManifestKeys.has(manifestKey)) {
1214
+ manifestKey = `${manifestKey}_${index}`;
1215
+ }
1216
+ usedManifestKeys.add(manifestKey);
1217
+ const offset = options.layoutOverrides?.[manifestKey] ?? autoOffsets[index] ?? [0, 0, 0];
1218
+ const offsetMatrix = translation(offset);
1219
+ const manifestScene = perManifestScenes[index];
1220
+ const instanceIdsByModelId = {};
1221
+ Object.entries(manifestScene.models).forEach(([localKey, modelDef]) => {
1222
+ const globalKey = `${manifestKey}__${localKey}`;
1223
+ models[globalKey] = {
1224
+ ...modelDef,
1225
+ meshId: `${manifestKey}__${modelDef.meshId}`,
1226
+ instanceId: globalKey,
1227
+ transform: {
1228
+ matrix: multiply(offsetMatrix, modelDef.transform.matrix)
1229
+ }
1230
+ };
1231
+ });
1232
+ Object.entries(manifestScene.instanceIdsByModelId).forEach(([modelId, localKey]) => {
1233
+ instanceIdsByModelId[modelId] = `${manifestKey}__${localKey}`;
1234
+ });
1235
+ manifests.push({
1236
+ key: manifestKey,
1237
+ sourceId,
1238
+ parsed,
1239
+ models: parsed.models,
1240
+ instanceIdsByModelId,
1241
+ offset,
1242
+ layoutRadius: radii[index]
1243
+ });
1244
+ });
1245
+ const space = {
1246
+ centerMode: "scene",
1247
+ radiusMode: "scene",
1248
+ cameraNearFar: [0.01, 5],
1249
+ ...options.space
1250
+ };
1251
+ const config = {
1252
+ pickedpointColor: [1, 0, 1],
1253
+ measurementColor: [0.5, 1, 0.5],
1254
+ showClippingPlanes: true,
1255
+ showClippingBorder: true,
1256
+ clippingBorderSize: displayUnit === "m" ? 2e-3 : 0.5,
1257
+ clippingBorderColor: [0, 1, 1],
1258
+ ...options.config
1259
+ };
1260
+ const trackball = {
1261
+ type: "TurntablePanTrackball",
1262
+ ...options.trackball,
1263
+ trackOptions: {
1264
+ startPhi: 15,
1265
+ startTheta: 15,
1266
+ startDistance: 2,
1267
+ minMaxPhi: [-180, 180],
1268
+ minMaxTheta: [-90, 90],
1269
+ minMaxDist: [0.1, 3],
1270
+ ...options.trackball?.trackOptions
1271
+ }
1272
+ };
1273
+ return { models, space, config, trackball, displayUnit, measureUnit: displayUnit, manifests };
1274
+ }
1275
+
1276
+ // src/IIIFMultiManifestViewer.tsx
1277
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1278
+ function defaultLanguage2() {
1279
+ if (typeof navigator !== "undefined" && navigator.language) {
1280
+ return navigator.language.split("-")[0];
1281
+ }
1282
+ return "en";
1283
+ }
1284
+ function sourceIdFor(manifest, index) {
1285
+ if (typeof manifest === "string") {
1286
+ return manifest;
1287
+ }
1288
+ return manifest.id ?? `manifest_${index}`;
1289
+ }
1290
+ var IIIFMultiManifestViewer = ({
1291
+ manifests,
1292
+ language: languageProp,
1293
+ displayUnit,
1294
+ gap,
1295
+ minRadius,
1296
+ columns,
1297
+ layoutOverrides,
1298
+ onLoad,
1299
+ onError,
1300
+ onDiagnostic,
1301
+ loadingFallback = null,
1302
+ errorFallback = null,
1303
+ space,
1304
+ config,
1305
+ trackball,
1306
+ children,
1307
+ ...viewerProps
1308
+ }) => {
1309
+ const [language, setLanguage] = (0, import_react4.useState)(() => languageProp ?? defaultLanguage2());
1310
+ const [status, setStatus] = (0, import_react4.useState)("idle");
1311
+ const [error, setError] = (0, import_react4.useState)(null);
1312
+ const [parsedList, setParsedList] = (0, import_react4.useState)(null);
1313
+ const [diagnostics, setDiagnostics] = (0, import_react4.useState)([]);
1314
+ (0, import_react4.useEffect)(() => {
1315
+ if (languageProp) {
1316
+ setLanguage(languageProp);
1317
+ }
1318
+ }, [languageProp]);
1319
+ (0, import_react4.useEffect)(() => {
1320
+ const controller = new AbortController();
1321
+ const collected = [];
1322
+ const handleDiagnostic = (diagnostic) => {
1323
+ collected.push(diagnostic);
1324
+ onDiagnostic?.(diagnostic);
1325
+ };
1326
+ setStatus("loading");
1327
+ Promise.all(
1328
+ manifests.map((manifest, index) => {
1329
+ if (typeof manifest === "string") {
1330
+ return loadManifest(manifest, { language, signal: controller.signal, onDiagnostic: handleDiagnostic });
1331
+ }
1332
+ return Promise.resolve().then(() => parseManifest(manifest, { language, onDiagnostic: handleDiagnostic }));
1333
+ })
1334
+ ).then((results) => {
1335
+ if (controller.signal.aborted) {
1336
+ return;
1337
+ }
1338
+ setParsedList(results);
1339
+ setDiagnostics(collected);
1340
+ setStatus("ready");
1341
+ setError(null);
1342
+ }).catch((cause) => {
1343
+ if (controller.signal.aborted || cause instanceof DOMException && cause.name === "AbortError") {
1344
+ return;
1345
+ }
1346
+ const normalized = cause instanceof Error ? cause : new Error(String(cause));
1347
+ setParsedList(null);
1348
+ setDiagnostics(collected);
1349
+ setStatus("error");
1350
+ setError(normalized);
1351
+ onError?.(normalized, "unknown");
1352
+ });
1353
+ return () => controller.abort();
1354
+ }, [manifests, onDiagnostic, onError]);
1355
+ const sourceIds = (0, import_react4.useMemo)(() => manifests.map(sourceIdFor), [manifests]);
1356
+ const localizedList = (0, import_react4.useMemo)(() => {
1357
+ if (!parsedList) {
1358
+ return null;
1359
+ }
1360
+ return parsedList.map((parsed) => parseManifest(parsed.manifest, { language }));
1361
+ }, [parsedList, language]);
1362
+ const scene = (0, import_react4.useMemo)(() => {
1363
+ if (!localizedList || localizedList.length === 0) {
1364
+ return null;
1365
+ }
1366
+ return sceneFromManifests(localizedList, sourceIds, {
1367
+ displayUnit,
1368
+ space,
1369
+ config,
1370
+ trackball,
1371
+ gap,
1372
+ minRadius,
1373
+ columns,
1374
+ layoutOverrides
1375
+ });
1376
+ }, [localizedList, sourceIds, displayUnit, space, config, trackball, gap, minRadius, columns, layoutOverrides]);
1377
+ (0, import_react4.useEffect)(() => {
1378
+ if (status === "ready" && scene) {
1379
+ onLoad?.(scene);
1380
+ }
1381
+ }, [status, scene]);
1382
+ if (status === "error" && error) {
1383
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: typeof errorFallback === "function" ? errorFallback([{ sourceId: "unknown", error }]) : errorFallback });
1384
+ }
1385
+ if (!scene) {
1386
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: loadingFallback });
1387
+ }
1388
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1389
+ import_react_3dhop4.ThreeDHopViewer,
1390
+ {
1391
+ ...viewerProps,
1392
+ models: scene.models,
1393
+ space: scene.space,
1394
+ config: scene.config,
1395
+ trackball: scene.trackball,
1396
+ measurementUnits: viewerProps.measurementUnits ?? scene.displayUnit,
1397
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(IIIFMultiManifestProvider, { status, error, diagnostics, scene, children })
1398
+ }
1399
+ );
1400
+ };
1401
+
1402
+ // src/collectionContext.tsx
1403
+ var import_react5 = require("react");
1404
+
1405
+ // src/iiif/collection.ts
1406
+ function firstThumbnail(item) {
1407
+ const thumbnail = Array.isArray(item.thumbnail) ? item.thumbnail[0] : item.thumbnail;
1408
+ return thumbnail?.id;
1409
+ }
1410
+ function parseCollection(collection, options = {}) {
1411
+ const { language = "en", onDiagnostic } = options;
1412
+ const resolve = (value) => resolveLanguageMap(value, language);
1413
+ const rawItems = collection.items ?? [];
1414
+ if (rawItems.length === 0) {
1415
+ onDiagnostic?.({ level: "warning", message: "Collection contains no items." });
1416
+ }
1417
+ const items = [];
1418
+ rawItems.forEach((item) => {
1419
+ if (item?.type && item.type !== "Manifest") {
1420
+ onDiagnostic?.({
1421
+ level: "info",
1422
+ message: `Skipping collection item ${item.id ?? "(no id)"} of unsupported type "${item.type}".`
1423
+ });
1424
+ return;
1425
+ }
1426
+ if (!item?.id) {
1427
+ onDiagnostic?.({ level: "warning", message: "Collection item has no id; skipping." });
1428
+ return;
1429
+ }
1430
+ items.push({
1431
+ id: item.id,
1432
+ label: resolve(item.label) || item.id,
1433
+ summary: resolve(item.summary) || void 0,
1434
+ thumbnail: firstThumbnail(item)
1435
+ });
1436
+ });
1437
+ return {
1438
+ collection,
1439
+ label: resolve(collection.label) || "Untitled Collection",
1440
+ summary: resolve(collection.summary) || void 0,
1441
+ items
1442
+ };
1443
+ }
1444
+ async function loadCollection(url, options = {}) {
1445
+ const { signal, fetchImpl = fetch, ...parseOptions } = options;
1446
+ const response = await fetchImpl(url, { signal });
1447
+ if (!response.ok) {
1448
+ throw new Error(`Failed to load IIIF collection (${response.status} ${response.statusText}): ${url}`);
1449
+ }
1450
+ let json;
1451
+ try {
1452
+ json = await response.json();
1453
+ } catch (cause) {
1454
+ const detail = cause instanceof Error ? cause.message : String(cause);
1455
+ throw new Error(`IIIF collection at ${url} is not valid JSON: ${detail}`);
1456
+ }
1457
+ if (!json || typeof json !== "object") {
1458
+ throw new Error(`IIIF collection at ${url} is not a JSON object.`);
1459
+ }
1460
+ return parseCollection(json, parseOptions);
1461
+ }
1462
+
1463
+ // src/collectionContext.tsx
1464
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1465
+ var IIIFCollectionContext = (0, import_react5.createContext)(null);
1466
+ function useIIIFCollection() {
1467
+ const value = (0, import_react5.useContext)(IIIFCollectionContext);
1468
+ if (!value) {
1469
+ throw new Error("useIIIFCollection must be used within an <IIIFCollectionProvider>.");
1470
+ }
1471
+ return value;
1472
+ }
1473
+ var IIIFCollectionProvider = ({
1474
+ collection,
1475
+ language = "en",
1476
+ initialManifestId,
1477
+ onLoad,
1478
+ onError,
1479
+ onDiagnostic,
1480
+ children
1481
+ }) => {
1482
+ const [status, setStatus] = (0, import_react5.useState)("idle");
1483
+ const [error, setError] = (0, import_react5.useState)(null);
1484
+ const [label, setLabel] = (0, import_react5.useState)("");
1485
+ const [summary, setSummary] = (0, import_react5.useState)(null);
1486
+ const [items, setItems] = (0, import_react5.useState)([]);
1487
+ const [diagnostics, setDiagnostics] = (0, import_react5.useState)([]);
1488
+ const [selectedId, setSelectedId] = (0, import_react5.useState)(initialManifestId ?? null);
1489
+ (0, import_react5.useEffect)(() => {
1490
+ const controller = new AbortController();
1491
+ const collected = [];
1492
+ const handleDiagnostic = (diagnostic) => {
1493
+ collected.push(diagnostic);
1494
+ onDiagnostic?.(diagnostic);
1495
+ };
1496
+ const finish = (parsed) => {
1497
+ if (controller.signal.aborted) {
1498
+ return;
1499
+ }
1500
+ setLabel(parsed.label);
1501
+ setSummary(parsed.summary ?? null);
1502
+ setItems(parsed.items);
1503
+ setDiagnostics(collected);
1504
+ setStatus("ready");
1505
+ setError(null);
1506
+ setSelectedId((current) => current ?? parsed.items[0]?.id ?? null);
1507
+ onLoad?.(parsed.items);
1508
+ };
1509
+ const fail = (cause) => {
1510
+ if (controller.signal.aborted) {
1511
+ return;
1512
+ }
1513
+ const normalized = cause instanceof Error ? cause : new Error(String(cause));
1514
+ setStatus("error");
1515
+ setError(normalized);
1516
+ setDiagnostics(collected);
1517
+ onError?.(normalized);
1518
+ };
1519
+ setStatus("loading");
1520
+ if (typeof collection === "string") {
1521
+ loadCollection(collection, { language, signal: controller.signal, onDiagnostic: handleDiagnostic }).then(finish).catch((cause) => {
1522
+ if (cause instanceof DOMException && cause.name === "AbortError") {
1523
+ return;
1524
+ }
1525
+ fail(cause);
1526
+ });
1527
+ } else {
1528
+ try {
1529
+ finish(parseCollection(collection, { language, onDiagnostic: handleDiagnostic }));
1530
+ } catch (cause) {
1531
+ fail(cause);
1532
+ }
1533
+ }
1534
+ return () => controller.abort();
1535
+ }, [collection, language, onLoad, onError]);
1536
+ const selectedIndex = (0, import_react5.useMemo)(() => items.findIndex((item) => item.id === selectedId), [items, selectedId]);
1537
+ const selectedItem = selectedIndex >= 0 ? items[selectedIndex] : null;
1538
+ const selectManifest = (0, import_react5.useCallback)((id) => {
1539
+ setSelectedId(id);
1540
+ }, []);
1541
+ const step = (0, import_react5.useCallback)(
1542
+ (delta) => {
1543
+ if (items.length === 0) {
1544
+ return;
1545
+ }
1546
+ const base = selectedIndex >= 0 ? selectedIndex : 0;
1547
+ const nextIndex = (base + delta + items.length) % items.length;
1548
+ setSelectedId(items[nextIndex].id);
1549
+ },
1550
+ [items, selectedIndex]
1551
+ );
1552
+ const next = (0, import_react5.useCallback)(() => step(1), [step]);
1553
+ const previous = (0, import_react5.useCallback)(() => step(-1), [step]);
1554
+ const value = (0, import_react5.useMemo)(
1555
+ () => ({
1556
+ status,
1557
+ error,
1558
+ diagnostics,
1559
+ label,
1560
+ summary,
1561
+ items,
1562
+ selectedId,
1563
+ selectedIndex,
1564
+ selectedItem,
1565
+ selectManifest,
1566
+ next,
1567
+ previous
1568
+ }),
1569
+ [status, error, diagnostics, label, summary, items, selectedId, selectedIndex, selectedItem, selectManifest, next, previous]
1570
+ );
1571
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(IIIFCollectionContext.Provider, { value, children });
1572
+ };
1573
+
1574
+ // src/panels/IIIFCollectionPicker.tsx
1575
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1576
+ var IIIFCollectionPicker = ({
1577
+ label = "Object",
1578
+ emptyMessage = "No objects in this collection.",
1579
+ className = "iiif-collection-picker",
1580
+ labelClassName = "iiif-collection-picker__label",
1581
+ selectClassName = "iiif-collection-picker__select"
1582
+ }) => {
1583
+ const { items, selectedId, selectManifest, status } = useIIIFCollection();
1584
+ if (status === "ready" && items.length === 0) {
1585
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: `${className}__empty`, children: emptyMessage });
1586
+ }
1587
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className, children: [
1588
+ label ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("label", { className: labelClassName, htmlFor: "iiif-collection-select", children: label }) : null,
1589
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1590
+ "select",
1591
+ {
1592
+ id: "iiif-collection-select",
1593
+ className: selectClassName,
1594
+ value: selectedId ?? "",
1595
+ onChange: (event) => selectManifest(event.target.value),
1596
+ children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: item.id, children: item.label }, item.id))
1597
+ }
1598
+ )
1599
+ ] });
1600
+ };
1601
+
1602
+ // src/panels/IIIFCollectionCarousel.tsx
1603
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1604
+ var IIIFCollectionCarousel = ({
1605
+ previousLabel = "Previous object",
1606
+ nextLabel = "Next object",
1607
+ emptyMessage = "No objects in this collection.",
1608
+ className = "iiif-carousel",
1609
+ trackClassName = "iiif-carousel__track",
1610
+ itemClassName = "iiif-carousel__item",
1611
+ activeItemClassName = "iiif-carousel__item--active",
1612
+ thumbnailClassName = "iiif-carousel__thumbnail",
1613
+ labelClassName = "iiif-carousel__label",
1614
+ controlClassName = "iiif-carousel__control"
1615
+ }) => {
1616
+ const { items, selectedId, selectManifest, next, previous, status } = useIIIFCollection();
1617
+ if (status === "ready" && items.length === 0) {
1618
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: `${className}__empty`, children: emptyMessage });
1619
+ }
1620
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className, children: [
1621
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1622
+ "button",
1623
+ {
1624
+ type: "button",
1625
+ className: controlClassName,
1626
+ onClick: previous,
1627
+ disabled: items.length < 2,
1628
+ "aria-label": previousLabel,
1629
+ children: "\u2039"
1630
+ }
1631
+ ),
1632
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("ul", { className: trackClassName, children: items.map((item) => {
1633
+ const active = item.id === selectedId;
1634
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("li", { className: active ? `${itemClassName} ${activeItemClassName}` : itemClassName, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("button", { type: "button", onClick: () => selectManifest(item.id), "aria-current": active || void 0, children: [
1635
+ item.thumbnail ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("img", { className: thumbnailClassName, src: item.thumbnail, alt: "", loading: "lazy" }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: thumbnailClassName, "aria-hidden": "true" }),
1636
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: labelClassName, children: item.label })
1637
+ ] }) }, item.id);
1638
+ }) }),
1639
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1640
+ "button",
1641
+ {
1642
+ type: "button",
1643
+ className: controlClassName,
1644
+ onClick: next,
1645
+ disabled: items.length < 2,
1646
+ "aria-label": nextLabel,
1647
+ children: "\u203A"
1648
+ }
1649
+ )
1650
+ ] });
1651
+ };
1652
+
1653
+ // src/panels/IIIFSummary.tsx
1654
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1655
+ var IIIFSummary = ({
1656
+ className = "iiif-summary",
1657
+ summaryClassName = "iiif-summary__text",
1658
+ attributionClassName = "iiif-summary__attribution"
1659
+ }) => {
1660
+ const { metadata } = useIIIFManifest();
1661
+ const summary = metadata?.summary;
1662
+ const attribution = metadata?.attribution;
1663
+ if (!summary && !attribution) {
1664
+ return null;
1665
+ }
1666
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className, children: [
1667
+ summary ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: summaryClassName, children: summary }) : null,
1668
+ attribution ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: attributionClassName, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("small", { children: attribution }) }) : null
1669
+ ] });
1670
+ };
1671
+
1672
+ // src/panels/IIIFMetadataPanel.tsx
1673
+ var import_react6 = require("react");
1674
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1675
+ var PRIORITY_FIELDS = [
1676
+ { key: "label", label: "Title" },
1677
+ { key: "museum", label: "Museum" },
1678
+ { key: "inventory", label: "Inventory" },
1679
+ { key: "objectId", label: "Object ID" },
1680
+ { key: "measureUnit", label: "Measure unit" },
1681
+ { key: "displayUnit", label: "Display unit" },
1682
+ { key: "summary", label: "Description" },
1683
+ { key: "attribution", label: "Attribution" }
1684
+ ];
1685
+ var IIIFMetadataPanel = ({
1686
+ heading = "Metadata",
1687
+ emptyMessage = "No metadata available",
1688
+ hideDuplicateFields = true,
1689
+ className = "iiif-metadata",
1690
+ headingClassName = "iiif-metadata__heading",
1691
+ tableClassName = "iiif-metadata__table",
1692
+ labelClassName = "iiif-metadata__label",
1693
+ valueClassName = "iiif-metadata__value"
1694
+ }) => {
1695
+ const { metadata } = useIIIFManifest();
1696
+ const rows = (0, import_react6.useMemo)(() => {
1697
+ if (!metadata) {
1698
+ return [];
1699
+ }
1700
+ const entries = [];
1701
+ const shown = /* @__PURE__ */ new Set();
1702
+ PRIORITY_FIELDS.forEach(({ key, label }) => {
1703
+ const value = metadata[key];
1704
+ if (typeof value === "string" && value.length > 0) {
1705
+ entries.push({ label, value });
1706
+ shown.add(value);
1707
+ }
1708
+ });
1709
+ metadata.fields.forEach((field) => {
1710
+ if (!field.value) {
1711
+ return;
1712
+ }
1713
+ if (hideDuplicateFields && shown.has(field.value)) {
1714
+ return;
1715
+ }
1716
+ entries.push(field);
1717
+ });
1718
+ return entries;
1719
+ }, [hideDuplicateFields, metadata]);
1720
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { className, children: [
1721
+ heading ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h2", { className: headingClassName, children: heading }) : null,
1722
+ rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "iiif-metadata__empty", children: emptyMessage }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("table", { className: tableClassName, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("tbody", { children: rows.map((row, index) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("tr", { children: [
1723
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("th", { scope: "row", className: labelClassName, children: row.label }),
1724
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { className: valueClassName, children: row.value })
1725
+ ] }, `${row.label}-${index}`)) }) })
1726
+ ] });
1727
+ };
1728
+
1729
+ // src/panels/IIIFModelsPanel.tsx
1730
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1731
+ function fallbackName(model, index) {
1732
+ const filename = model.url.split("/").pop() ?? "";
1733
+ const stem = filename.split("?")[0].split("#")[0].split(".")[0];
1734
+ return stem || `Model ${index + 1}`;
1735
+ }
1736
+ var IIIFModelsPanel = ({
1737
+ heading = "Models",
1738
+ emptyMessage = "No models in manifest",
1739
+ visibilityLabel = "Show model",
1740
+ transparencyLabel = "Toggle transparency",
1741
+ className = "iiif-models",
1742
+ headingClassName = "iiif-models__heading",
1743
+ listClassName = "iiif-models__list",
1744
+ itemClassName = "iiif-models__item"
1745
+ }) => {
1746
+ const { models, localize, isModelVisible, setModelVisible, isModelTransparent, toggleModelTransparency } = useIIIFManifest();
1747
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("section", { className, children: [
1748
+ heading ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h2", { className: headingClassName, children: heading }) : null,
1749
+ models.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "iiif-models__empty", children: emptyMessage }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ul", { className: listClassName, children: models.map((model, index) => {
1750
+ const name = localize(model.rawLabel) || fallbackName(model, index);
1751
+ const visible = isModelVisible(model.id);
1752
+ const transparent = isModelTransparent(model.id);
1753
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("li", { className: itemClassName, children: [
1754
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("label", { className: "iiif-models__visibility", children: [
1755
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1756
+ "input",
1757
+ {
1758
+ type: "checkbox",
1759
+ checked: visible,
1760
+ onChange: (event) => setModelVisible(model.id, event.target.checked),
1761
+ "aria-label": `${visibilityLabel}: ${name}`
1762
+ }
1763
+ ),
1764
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "iiif-models__name", children: name })
1765
+ ] }),
1766
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("label", { className: "iiif-models__transparency", children: [
1767
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1768
+ "input",
1769
+ {
1770
+ type: "checkbox",
1771
+ checked: transparent,
1772
+ onChange: () => toggleModelTransparency(model.id),
1773
+ "aria-label": `${transparencyLabel}: ${name}`
1774
+ }
1775
+ ),
1776
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "iiif-models__transparency-text", children: transparencyLabel })
1777
+ ] })
1778
+ ] }, model.id);
1779
+ }) })
1780
+ ] });
1781
+ };
1782
+
1783
+ // src/panels/IIIFMultiManifestModelsPanel.tsx
1784
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1785
+ function fallbackName2(model, index) {
1786
+ const filename = model.url.split("/").pop() ?? "";
1787
+ const stem = filename.split("?")[0].split("#")[0].split(".")[0];
1788
+ return stem || `Model ${index + 1}`;
1789
+ }
1790
+ var IIIFMultiManifestModelsPanel = ({
1791
+ heading = "Models",
1792
+ emptyMessage = "No manifests selected",
1793
+ visibilityLabel = "Show model",
1794
+ transparencyLabel = "Toggle transparency",
1795
+ className = "iiif-multi-models",
1796
+ headingClassName = "iiif-multi-models__heading",
1797
+ groupClassName = "iiif-multi-models__group",
1798
+ groupHeadingClassName = "iiif-multi-models__group-heading",
1799
+ listClassName = "iiif-multi-models__list",
1800
+ itemClassName = "iiif-multi-models__item"
1801
+ }) => {
1802
+ const { manifests, isModelVisible, setModelVisible, isModelTransparent, toggleModelTransparency } = useIIIFMultiManifest();
1803
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { className, children: [
1804
+ heading ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h2", { className: headingClassName, children: heading }) : null,
1805
+ manifests.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "iiif-multi-models__empty", children: emptyMessage }) : manifests.map((manifest) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: groupClassName, children: [
1806
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h3", { className: groupHeadingClassName, children: manifest.label }),
1807
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("ul", { className: listClassName, children: manifest.models.map((model, index) => {
1808
+ const name = model.rawLabel && typeof model.rawLabel === "string" ? model.rawLabel : fallbackName2(model, index);
1809
+ const visible = isModelVisible(manifest.key, model.id);
1810
+ const transparent = isModelTransparent(manifest.key, model.id);
1811
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("li", { className: itemClassName, children: [
1812
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("label", { className: "iiif-multi-models__visibility", children: [
1813
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1814
+ "input",
1815
+ {
1816
+ type: "checkbox",
1817
+ checked: visible,
1818
+ onChange: (event) => setModelVisible(manifest.key, model.id, event.target.checked),
1819
+ "aria-label": `${visibilityLabel}: ${name}`
1820
+ }
1821
+ ),
1822
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "iiif-multi-models__name", children: name })
1823
+ ] }),
1824
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("label", { className: "iiif-multi-models__transparency", children: [
1825
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1826
+ "input",
1827
+ {
1828
+ type: "checkbox",
1829
+ checked: transparent,
1830
+ onChange: () => toggleModelTransparency(manifest.key, model.id),
1831
+ "aria-label": `${transparencyLabel}: ${name}`
1832
+ }
1833
+ ),
1834
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "iiif-multi-models__transparency-text", children: transparencyLabel })
1835
+ ] })
1836
+ ] }, model.id);
1837
+ }) })
1838
+ ] }, manifest.key))
1839
+ ] });
1840
+ };
1841
+
1842
+ // src/panels/IIIFSavedViewsPanel.tsx
1843
+ var import_react7 = require("react");
1844
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1845
+ var IIIFSavedViewsPanel = ({
1846
+ heading = "Saved views",
1847
+ emptyMessage = "No saved views in manifest",
1848
+ hideSaveControl = false,
1849
+ saveLabel = "Save current view",
1850
+ onSaveView,
1851
+ className = "iiif-views",
1852
+ headingClassName = "iiif-views__heading",
1853
+ listClassName = "iiif-views__list",
1854
+ buttonClassName = "iiif-views__button"
1855
+ }) => {
1856
+ const { cameras, localize, goToCamera, saveCurrentView, isSceneReady } = useIIIFManifest();
1857
+ const [captured, setCaptured] = (0, import_react7.useState)(null);
1858
+ const handleSave = () => {
1859
+ const annotation = saveCurrentView();
1860
+ if (!annotation) {
1861
+ return;
1862
+ }
1863
+ if (onSaveView) {
1864
+ onSaveView(annotation);
1865
+ return;
1866
+ }
1867
+ setCaptured(JSON.stringify(annotation, null, 2));
1868
+ };
1869
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("section", { className, children: [
1870
+ heading ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("h2", { className: headingClassName, children: heading }) : null,
1871
+ cameras.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "iiif-views__empty", children: emptyMessage }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ul", { className: listClassName, children: cameras.map((camera, index) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1872
+ "button",
1873
+ {
1874
+ type: "button",
1875
+ className: buttonClassName,
1876
+ onClick: () => goToCamera(index),
1877
+ disabled: !isSceneReady,
1878
+ children: localize(camera.rawLabel) || `View ${index + 1}`
1879
+ }
1880
+ ) }, camera.id ?? index)) }),
1881
+ hideSaveControl ? null : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1882
+ "button",
1883
+ {
1884
+ type: "button",
1885
+ className: "iiif-views__save",
1886
+ onClick: handleSave,
1887
+ disabled: !isSceneReady,
1888
+ children: saveLabel
1889
+ }
1890
+ ),
1891
+ captured ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("pre", { className: "iiif-views__output", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("code", { children: captured }) }) : null
1892
+ ] });
1893
+ };
1894
+
1895
+ // src/panels/IIIFLanguageSwitcher.tsx
1896
+ var import_jsx_runtime13 = require("react/jsx-runtime");
1897
+ var LANGUAGE_NAMES = {
1898
+ en: "English",
1899
+ no: "Norsk",
1900
+ nn: "Nynorsk",
1901
+ nb: "Bokm\xE5l",
1902
+ de: "Deutsch",
1903
+ fr: "Fran\xE7ais",
1904
+ es: "Espa\xF1ol",
1905
+ it: "Italiano",
1906
+ se: "Davvis\xE1megiella"
1907
+ };
1908
+ var IIIFLanguageSwitcher = ({
1909
+ label = "Language",
1910
+ languageNames,
1911
+ alwaysShow = false,
1912
+ className = "iiif-language",
1913
+ labelClassName = "iiif-language__label",
1914
+ selectClassName = "iiif-language__select"
1915
+ }) => {
1916
+ const { languages, language, setLanguage } = useIIIFManifest();
1917
+ if (!alwaysShow && languages.length <= 1) {
1918
+ return null;
1919
+ }
1920
+ const names = languageNames ? { ...LANGUAGE_NAMES, ...languageNames } : LANGUAGE_NAMES;
1921
+ const options = languages.includes(language) ? languages : [language, ...languages];
1922
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className, children: [
1923
+ label ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("label", { className: labelClassName, htmlFor: "iiif-language-select", children: label }) : null,
1924
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1925
+ "select",
1926
+ {
1927
+ id: "iiif-language-select",
1928
+ className: selectClassName,
1929
+ value: language,
1930
+ onChange: (event) => setLanguage(event.target.value),
1931
+ children: options.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("option", { value: tag, children: names[tag] ?? tag.toUpperCase() }, tag))
1932
+ }
1933
+ )
1934
+ ] });
1935
+ };
1936
+ // Annotate the CommonJS export names for ESM import in node:
1937
+ 0 && (module.exports = {
1938
+ DEFAULT_FALLBACK_LANGUAGES,
1939
+ DEFAULT_MEASURE_UNIT,
1940
+ FORMAT_PREFERENCE,
1941
+ IIIFCollectionCarousel,
1942
+ IIIFCollectionPicker,
1943
+ IIIFCollectionProvider,
1944
+ IIIFLanguageSwitcher,
1945
+ IIIFMetadataPanel,
1946
+ IIIFModelsPanel,
1947
+ IIIFMultiManifestModelsPanel,
1948
+ IIIFMultiManifestProvider,
1949
+ IIIFMultiManifestViewer,
1950
+ IIIFProvider,
1951
+ IIIFSavedViewsPanel,
1952
+ IIIFSummary,
1953
+ IIIFViewer,
1954
+ LANGUAGE_NAMES,
1955
+ UNITS_IN_METRES,
1956
+ buildModelMatrix,
1957
+ cameraToView,
1958
+ collectLanguages,
1959
+ composeTransforms,
1960
+ getUnitScaleFactor,
1961
+ isKnownUnit,
1962
+ layoutManifests,
1963
+ loadCollection,
1964
+ loadManifest,
1965
+ parseCollection,
1966
+ parseManifest,
1967
+ resolveLanguageMap,
1968
+ sceneFromManifest,
1969
+ sceneFromManifests,
1970
+ selectBestSource,
1971
+ track2view,
1972
+ transformToMatrix,
1973
+ useIIIFCollection,
1974
+ useIIIFManifest,
1975
+ useIIIFMultiManifest,
1976
+ view2track,
1977
+ viewToCameraAnnotation
1978
+ });
1979
+ //# sourceMappingURL=index.js.map