@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/README.md
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
# @ikaros-arch/react-3dhop-iiif
|
|
2
|
+
|
|
3
|
+
Renders [IIIF Presentation 4.0 / IIIF 3D](https://github.com/IIIF/3d/blob/main/temp-draft-4.md)
|
|
4
|
+
manifests with [3DHOP](https://3dhop.net/), on top of
|
|
5
|
+
[`@ikaros-arch/react-3dhop`](https://github.com/ikaros-arch/react-3dhop/tree/main/packages/react-3dhop#readme).
|
|
6
|
+
|
|
7
|
+
The IIIF 3D specification is still a draft. Keeping it in its own package means the core viewer
|
|
8
|
+
does not have to move every time the draft does.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @ikaros-arch/react-3dhop-iiif @ikaros-arch/react-3dhop @ikaros-arch/3dhop
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`react`, `react-dom` and `@ikaros-arch/react-3dhop` are peer dependencies (and `@ikaros-arch/3dhop`
|
|
15
|
+
is a peer of that, holding the 3DHOP runtime assets). The package itself has **no runtime
|
|
16
|
+
dependencies**.
|
|
17
|
+
|
|
18
|
+
This README is the reference for manifest authoring and specification coverage.
|
|
19
|
+
[docs/iiif.md](https://github.com/ikaros-arch/react-3dhop/blob/main/docs/iiif.md) explains how the
|
|
20
|
+
package is put together and where to extend it.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { IIIFViewer, IIIFMetadataPanel, IIIFSavedViewsPanel } from '@ikaros-arch/react-3dhop-iiif';
|
|
28
|
+
|
|
29
|
+
export function Viewer() {
|
|
30
|
+
return (
|
|
31
|
+
<IIIFViewer
|
|
32
|
+
manifest="https://example.org/iiif/object/manifest.json"
|
|
33
|
+
assetBaseUrl="/3dhop"
|
|
34
|
+
width={800}
|
|
35
|
+
height={600}
|
|
36
|
+
>
|
|
37
|
+
<IIIFMetadataPanel />
|
|
38
|
+
<IIIFSavedViewsPanel />
|
|
39
|
+
</IIIFViewer>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`<IIIFViewer>` accepts everything `<ThreeDHopViewer>` does, except `models` and `modelUrl`, which
|
|
45
|
+
it derives from the manifest. `manifest` takes either a URL to fetch or an already-parsed manifest
|
|
46
|
+
object.
|
|
47
|
+
|
|
48
|
+
Panels must be rendered **inside** `<IIIFViewer>` to reach its context. The viewer renders its
|
|
49
|
+
children into a fixed-size, clipped box intended for canvas overlays, so to place panels elsewhere
|
|
50
|
+
on the page, portal them out — React context passes through portals. See
|
|
51
|
+
[`examples/react-3dhop-demo/src/IIIFDemo.tsx`](https://github.com/ikaros-arch/react-3dhop/blob/main/examples/react-3dhop-demo/src/IIIFDemo.tsx).
|
|
52
|
+
|
|
53
|
+
### Reading the manifest yourself
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
const { metadata, models, cameras, language, setLanguage, goToCamera, saveCurrentView } =
|
|
57
|
+
useIIIFManifest();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Every parsing and geometry function is also exported standalone, with no React and no dependency on
|
|
61
|
+
3DHOP's globals — usable in Node, and unit-tested there:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { loadManifest, sceneFromManifest, buildModelMatrix, view2track } from '@ikaros-arch/react-3dhop-iiif';
|
|
65
|
+
|
|
66
|
+
const parsed = await loadManifest(url);
|
|
67
|
+
const scene = sceneFromManifest(parsed, { displayUnit: 'cm' });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Manifest authoring
|
|
73
|
+
|
|
74
|
+
### Model formats
|
|
75
|
+
|
|
76
|
+
3DHOP streams [Nexus](https://vcg.isti.cnr.it/nexus/) (`.nxz`/`.nxs`) and loads `.ply`. When one
|
|
77
|
+
annotation offers several sources, the best supported one wins:
|
|
78
|
+
|
|
79
|
+
| Rank | `format` | Support |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| 1 | `nexus`, `nxz`, `nxs`, `application/octet-stream+nexus` | Streamed progressively — the format to publish |
|
|
82
|
+
| 2 | `ply`, `application/ply` | Loaded whole |
|
|
83
|
+
| 3 | `obj`, `model/obj` | **Cannot be rendered** — ranked here only so it is preferred over glTF |
|
|
84
|
+
| 4 | `glb`, `gltf`, `model/gltf-binary`, `model/gltf+json` | **Cannot be rendered** |
|
|
85
|
+
|
|
86
|
+
Comparison is case-insensitive, so `"format": "Nexus"` works. An unrecognised format ranks below
|
|
87
|
+
every known one rather than being discarded.
|
|
88
|
+
|
|
89
|
+
3DHOP has loaders for Nexus and PLY only. A model in any other format is still parsed, so the
|
|
90
|
+
manifest continues to describe the object, and a diagnostic says why nothing appeared. Adding an
|
|
91
|
+
OBJ or glTF loader is out of scope; convert to Nexus with
|
|
92
|
+
[`nxsbuild`](https://vcg.isti.cnr.it/nexus/) instead.
|
|
93
|
+
|
|
94
|
+
### Units
|
|
95
|
+
|
|
96
|
+
Two non-standard metadata fields control scale, recognised by their English or Norwegian labels:
|
|
97
|
+
|
|
98
|
+
| Label | Meaning | Default |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `Measure Unit` / `Måleenhet` | The unit the manifest's own coordinates are authored in | `mm` |
|
|
101
|
+
| `Display Unit` / `Visningsenhet` | The unit the scene is rendered and measured in | the measure unit |
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{ "label": { "en": ["Measure Unit"] }, "value": { "en": ["m"] } }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A single model whose mesh is authored in a different unit from the rest of the manifest can say so
|
|
108
|
+
on its source, without disturbing anything else:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{ "id": "…/alvim.nxz", "type": "Model", "format": "Nexus", "measureUnit": "mm" }
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Recognised units are `km`, `m`, `cm`, `mm`, `um` (or `µm`) and `nm`. An unrecognised unit is
|
|
115
|
+
treated as a factor of 1, so a typo renders the object unscaled rather than failing.
|
|
116
|
+
|
|
117
|
+
The `Display Unit` also sets the label on measurements taken with the measuring tool, and picks the
|
|
118
|
+
clipping-border width, which otherwise looks invisible on a millimetre-scale object and enormous on
|
|
119
|
+
a metre-scale one.
|
|
120
|
+
|
|
121
|
+
### Placement and transforms
|
|
122
|
+
|
|
123
|
+
A model's position comes from the `PointSelector` on the annotation's target:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
"target": {
|
|
127
|
+
"type": "SpecificResource",
|
|
128
|
+
"source": [{ "id": "…/scene/1", "type": "Scene" }],
|
|
129
|
+
"selector": [{ "type": "PointSelector", "x": 2.0, "y": 0.0, "z": 0.0 }]
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`transform` is an **ordered list**, applied first entry first, and this package composes it in
|
|
134
|
+
order rather than flattening it. Order is significant: scaling then translating moves the model by
|
|
135
|
+
the unscaled offset, while translating then scaling moves it by the scaled one. The same holds for
|
|
136
|
+
rotation.
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
"transform": [
|
|
140
|
+
{ "type": "ScaleTransform", "x": 1.5, "y": 1.5, "z": 1.5 },
|
|
141
|
+
{ "type": "RotateTransform", "x": -90.0, "y": -90.0, "z": 0.0 },
|
|
142
|
+
{ "type": "TranslateTransform", "x": 0.0, "y": 0.5, "z": 0.0 }
|
|
143
|
+
]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`RotateTransform` accepts either per-axis Euler angles in degrees (`x`/`y`/`z`, composed Z then Y
|
|
147
|
+
then X, matching 3DHOP's own rotation triple) or a single axis-angle (`{ "axis": "y", "angle": 45 }`).
|
|
148
|
+
|
|
149
|
+
The complete model matrix is:
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
M = S(displayUnit ← measureUnit) · T(pointSelector) · L(transform list) · S(geometry unit override)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Read right to left: bring the mesh's vertices into the manifest's unit, apply the transform list,
|
|
156
|
+
place the result at the point selector, then convert the whole scene into the display unit. Keeping
|
|
157
|
+
the unit conversions outermost and innermost is what makes positions and translations scale
|
|
158
|
+
alongside the geometry.
|
|
159
|
+
|
|
160
|
+
Models sharing a source URL are collapsed onto one mesh, so placing the same object several times
|
|
161
|
+
downloads it once.
|
|
162
|
+
|
|
163
|
+
### Cameras
|
|
164
|
+
|
|
165
|
+
Camera annotations become buttons in `<IIIFSavedViewsPanel>`, and the first one is applied as the
|
|
166
|
+
opening view unless `applyInitialCamera={false}`:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"type": "Annotation",
|
|
171
|
+
"body": {
|
|
172
|
+
"type": "PerspectiveCamera",
|
|
173
|
+
"label": { "en": ["Front"] },
|
|
174
|
+
"fieldOfView": 50.0,
|
|
175
|
+
"lookAt": { "type": "PointSelector", "x": 0, "y": 0.5, "z": 0 }
|
|
176
|
+
},
|
|
177
|
+
"target": {
|
|
178
|
+
"type": "SpecificResource",
|
|
179
|
+
"source": [{ "id": "…/scene/1", "type": "Scene" }],
|
|
180
|
+
"selector": [{ "type": "PointSelector", "x": 0.0, "y": 3.0, "z": -8.0 }]
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The target's `PointSelector` is the camera position; `lookAt` is what it points at. `lookAt` may
|
|
186
|
+
instead reference another annotation by `id`, in which case the camera aims at that model's
|
|
187
|
+
position. Both `fieldOfView` and the shorthand `fov` are read. With no `lookAt`, the camera aims at
|
|
188
|
+
the scene centre.
|
|
189
|
+
|
|
190
|
+
`saveCurrentView()` returns an annotation in exactly this shape, ready to paste back into a
|
|
191
|
+
manifest.
|
|
192
|
+
|
|
193
|
+
### Language
|
|
194
|
+
|
|
195
|
+
Any IIIF language map is resolved against the active language, falling back through `en`, `no`,
|
|
196
|
+
`nb`, `nn` and then any remaining language, so a partially translated manifest still renders. The
|
|
197
|
+
language defaults to the browser's and can be overridden with the `language` prop or switched at
|
|
198
|
+
runtime through `<IIIFLanguageSwitcher>`, which hides itself when there is only one language to
|
|
199
|
+
choose from.
|
|
200
|
+
|
|
201
|
+
Metadata fields are recognised by their **English** label even when another language is displayed,
|
|
202
|
+
so switching language does not lose the unit or inventory fields.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Panels
|
|
207
|
+
|
|
208
|
+
All read from `useIIIFManifest()` unless noted otherwise, use semantic markup with no CSS
|
|
209
|
+
framework, and accept `className` props on every element. There is no bundled stylesheet; the
|
|
210
|
+
demo's [`IIIFDemo.css`](https://github.com/ikaros-arch/react-3dhop/blob/main/examples/react-3dhop-demo/src/IIIFDemo.css) is a starting point.
|
|
211
|
+
|
|
212
|
+
| Component | Shows |
|
|
213
|
+
|---|---|
|
|
214
|
+
| `<IIIFSummary>` | `summary` and the `requiredStatement` attribution |
|
|
215
|
+
| `<IIIFMetadataPanel>` | Recognised fields first, then the rest in manifest order |
|
|
216
|
+
| `<IIIFModelsPanel>` | Per-model visibility and transparency toggles |
|
|
217
|
+
| `<IIIFSavedViewsPanel>` | The manifest's cameras, plus "save current view" |
|
|
218
|
+
| `<IIIFLanguageSwitcher>` | Language selector; hidden when the manifest has one language |
|
|
219
|
+
| `<IIIFCollectionPicker>` | A `<select>` of every manifest in a collection |
|
|
220
|
+
| `<IIIFCollectionCarousel>` | A thumbnail strip of every manifest in a collection, with prev/next |
|
|
221
|
+
| `<IIIFMultiManifestModelsPanel>` | Visibility/transparency toggles grouped by manifest; reads `useIIIFMultiManifest()`, for use inside `<IIIFMultiManifestViewer>` instead of `<IIIFViewer>` |
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Collections
|
|
226
|
+
|
|
227
|
+
A IIIF Collection lists several manifests — e.g. every object in a museum sub-catalogue — so a
|
|
228
|
+
picker or carousel can switch between them. `<IIIFCollectionProvider>` fetches and parses one, and
|
|
229
|
+
tracks which manifest is selected; it renders nothing itself, so it composes with `<IIIFViewer>`
|
|
230
|
+
rather than replacing it:
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
import {
|
|
234
|
+
IIIFCollectionProvider,
|
|
235
|
+
IIIFCollectionPicker,
|
|
236
|
+
IIIFCollectionCarousel,
|
|
237
|
+
IIIFViewer,
|
|
238
|
+
useIIIFCollection
|
|
239
|
+
} from '@ikaros-arch/react-3dhop-iiif';
|
|
240
|
+
|
|
241
|
+
function CollectionBrowser() {
|
|
242
|
+
return (
|
|
243
|
+
<IIIFCollectionProvider collection="/manifests/bitfrost/collection.json">
|
|
244
|
+
<IIIFCollectionPicker />
|
|
245
|
+
<IIIFCollectionCarousel />
|
|
246
|
+
<SelectedManifestViewer />
|
|
247
|
+
</IIIFCollectionProvider>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function SelectedManifestViewer() {
|
|
252
|
+
const { selectedId } = useIIIFCollection();
|
|
253
|
+
if (!selectedId) return null;
|
|
254
|
+
// Remounting on selection change avoids carrying one object's camera into the next.
|
|
255
|
+
return <IIIFViewer key={selectedId} manifest={selectedId} assetBaseUrl="/3dhop" width={760} height={620} />;
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`collection` accepts a URL or an already-fetched object, mirroring `<IIIFViewer manifest>`.
|
|
260
|
+
`useIIIFCollection()` also exposes `items` (id, label, thumbnail), `status`/`error`, and
|
|
261
|
+
`next()`/`previous()` for building a custom control. See
|
|
262
|
+
[`examples/react-3dhop-demo/src/IIIFDemo.tsx`](https://github.com/ikaros-arch/react-3dhop/blob/main/examples/react-3dhop-demo/src/IIIFDemo.tsx)
|
|
263
|
+
for a full example, including the [generator
|
|
264
|
+
script](https://github.com/ikaros-arch/react-3dhop/blob/main/examples/react-3dhop-demo/scripts/generate-bitfrost-collection.mjs) that builds its
|
|
265
|
+
sample collection from a museum catalogue export.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Multiple manifests in one viewer
|
|
270
|
+
|
|
271
|
+
`<IIIFMultiManifestViewer>` renders several manifests together in a single `<ThreeDHopViewer>`
|
|
272
|
+
instance, laid out side by side rather than switched between one at a time:
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
import { IIIFMultiManifestViewer, IIIFMultiManifestModelsPanel } from '@ikaros-arch/react-3dhop-iiif';
|
|
276
|
+
|
|
277
|
+
function Comparison() {
|
|
278
|
+
return (
|
|
279
|
+
<IIIFMultiManifestViewer
|
|
280
|
+
manifests={['/manifests/a.json', '/manifests/b.json']}
|
|
281
|
+
assetBaseUrl="/3dhop"
|
|
282
|
+
width={760}
|
|
283
|
+
height={620}
|
|
284
|
+
>
|
|
285
|
+
<IIIFMultiManifestModelsPanel />
|
|
286
|
+
</IIIFMultiManifestViewer>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
It is a parallel component, not an extension of `<IIIFViewer>`: it fetches and parses each manifest
|
|
292
|
+
independently, and fails fast if any one of them can't be loaded, since a partially-failed shared
|
|
293
|
+
scene isn't a state worth trying to render. Its context, `useIIIFMultiManifest()`, only covers
|
|
294
|
+
per-model visibility and transparency, scoped by manifest — cameras, saved views, metadata and
|
|
295
|
+
language are per-manifest concepts without a defined multi-manifest behaviour yet, so `<IIIFViewer>`
|
|
296
|
+
remains the way to reach those.
|
|
297
|
+
|
|
298
|
+
There is no mesh geometry available at layout time — meshes stream in later, and the manifest only
|
|
299
|
+
says where each annotation is *placed*, not how large it is — so the side-by-side layout is a
|
|
300
|
+
heuristic: each manifest's own placement spread (the spread between its own annotations), floored by
|
|
301
|
+
`minRadius` and separated by `gap`. A manifest with a single model at the origin (the common case)
|
|
302
|
+
has a spread of zero and falls back entirely to `minRadius`. Tune both options for your data:
|
|
303
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
<IIIFMultiManifestViewer manifests={ids} minRadius={150} gap={80} columns={3} />
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
`useIIIFMultiManifest()` addresses models with a `(manifestKey, modelId)` pair rather than the bare
|
|
309
|
+
IIIF annotation id `useIIIFManifest()` uses, because two manifests can otherwise declare colliding
|
|
310
|
+
ids:
|
|
311
|
+
|
|
312
|
+
```tsx
|
|
313
|
+
const { manifests, isModelVisible, setModelVisible } = useIIIFMultiManifest();
|
|
314
|
+
// manifests: [{ key, sourceId, label, models }, …]
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
See [`examples/react-3dhop-demo/src/IIIFDemo.tsx`](https://github.com/ikaros-arch/react-3dhop/blob/main/examples/react-3dhop-demo/src/IIIFDemo.tsx)
|
|
318
|
+
(`"Multi-manifest viewer"` mode) for a full example that lets the user check off any number of
|
|
319
|
+
objects from a collection.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Error handling
|
|
324
|
+
|
|
325
|
+
A manifest that cannot be fetched or parsed puts the viewer into an error state and renders
|
|
326
|
+
`errorFallback` — it does not throw or leave a blank canvas.
|
|
327
|
+
|
|
328
|
+
Problems affecting only part of a manifest are reported as **diagnostics** and the rest still
|
|
329
|
+
renders: an annotation with no usable source, a glTF-only model, an unrecognised body type, a
|
|
330
|
+
manifest with no `Scene`, or one with several. Diagnostics reach you through the `onDiagnostic`
|
|
331
|
+
prop or the `diagnostics` array on the context.
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Supported IIIF 3D subset
|
|
336
|
+
|
|
337
|
+
Against the [draft specification](https://github.com/IIIF/3d/blob/main/temp-draft-4.md). ✅ full,
|
|
338
|
+
⚠️ partial, ❌ not implemented.
|
|
339
|
+
|
|
340
|
+
| Feature | Parsed | Rendered | Notes |
|
|
341
|
+
|---|---|---|---|
|
|
342
|
+
| **Containers** |
|
|
343
|
+
| `Scene` | ✅ | ✅ | |
|
|
344
|
+
| Multiple scenes | ✅ | ⚠️ | Merged into one view, with a diagnostic |
|
|
345
|
+
| Canvas in Scene | ❌ | ❌ | |
|
|
346
|
+
| Nested scenes | ❌ | ❌ | |
|
|
347
|
+
| **Resources** |
|
|
348
|
+
| `Model` as body | ✅ | ✅ | |
|
|
349
|
+
| `Model` via `SpecificResource` | ✅ | ✅ | Required for transforms |
|
|
350
|
+
| Several sources per annotation | ✅ | ✅ | Best supported format wins |
|
|
351
|
+
| `PerspectiveCamera` | ✅ | ✅ | Position, `lookAt`, `fieldOfView` |
|
|
352
|
+
| `OrthographicCamera` | ✅ | ✅ | Position, `lookAt` |
|
|
353
|
+
| **Lights** (`Ambient`/`Directional`/`Point`/`Spot`) | ❌ | ❌ | 3DHOP has one directional light, controlled by the viewer |
|
|
354
|
+
| **Transforms** |
|
|
355
|
+
| `ScaleTransform` | ✅ | ✅ | |
|
|
356
|
+
| `TranslateTransform` | ✅ | ✅ | |
|
|
357
|
+
| `RotateTransform` | ✅ | ✅ | Euler and axis-angle forms |
|
|
358
|
+
| Ordered composition | ✅ | ✅ | Composed as a matrix, not flattened |
|
|
359
|
+
| **Selectors** |
|
|
360
|
+
| `PointSelector` | ✅ | ✅ | Positions models and cameras |
|
|
361
|
+
| `PolygonZSelector` | ❌ | ❌ | For Canvas placement |
|
|
362
|
+
| **Properties** |
|
|
363
|
+
| `label`, `summary`, `metadata` | ✅ | ✅ | Multilingual |
|
|
364
|
+
| `requiredStatement` | ✅ | ✅ | Shown as attribution |
|
|
365
|
+
| `fieldOfView` / `fov` | ✅ | ✅ | |
|
|
366
|
+
| `lookAt` | ✅ | ✅ | Point or annotation reference |
|
|
367
|
+
| `backgroundColor` | ❌ | ❌ | Use the viewer's `backgroundUrl` |
|
|
368
|
+
| `duration` | ❌ | ❌ | Temporal scenes |
|
|
369
|
+
| `near` / `far` | ❌ | ❌ | Derived from scene extents instead |
|
|
370
|
+
| `exclude` | ❌ | ❌ | |
|
|
371
|
+
| **Formats** |
|
|
372
|
+
| Nexus (`.nxz`/`.nxs`) | ✅ | ✅ | Streamed progressively |
|
|
373
|
+
| PLY | ✅ | ✅ | |
|
|
374
|
+
| OBJ | ✅ | ❌ | Reported as a diagnostic; convert to Nexus |
|
|
375
|
+
| glTF / GLB | ✅ | ❌ | Reported as a diagnostic; convert to Nexus |
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## Licence
|
|
380
|
+
|
|
381
|
+
GPL-3.0-or-later, matching 3DHOP.
|