@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/LICENSE +674 -0
- package/README.md +381 -0
- package/dist/index.d.mts +808 -0
- package/dist/index.d.ts +808 -0
- package/dist/index.js +1979 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1913 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +71 -0
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
|