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