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