Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions .codesandbox/ci.json

This file was deleted.

3 changes: 2 additions & 1 deletion .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: ['20', '22']
node: ['22']

name: Node ${{ matrix.node }} build
steps:
Expand Down Expand Up @@ -42,3 +42,4 @@ jobs:
- run: pnpm run test
- run: node pkg-tests/node-load.cjs
- run: node pkg-tests/node-load.mjs
- run: pnpm dlx pkg-pr-new publish
34 changes: 34 additions & 0 deletions __tests__/activations-helper.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, test } from 'vitest';
import { createActivationsHelper } from '../src/activations';
import { compatVault } from '../src/compat';
import type { SceneAnnotation } from '../src/scenes/types';

describe('activations helper', () => {
test('Parsing an activating annotation into an ordered transaction', () => {
const helper = createActivationsHelper(compatVault);

const activating: SceneAnnotation = {
id: 'https://example.org/anno/1',
type: 'Annotation',
motivation: ['activating'],
body: {
type: 'SpecificResource',
source: { id: 'https://example.org/model/1', type: 'Model' },
selector: [{ type: 'AnimationSelector', value: 'open' }],
transform: [{ type: 'RotateTransform', y: 90 }],
action: ['play'],
},
target: { id: 'https://example.org/scene/1', type: 'Scene' },
};

const parsed = helper.parseActivatingAnnotation(activating);
if (!parsed) {
throw new Error('Expected parsed activation transaction');
}
expect(parsed?.annotationId).toBe('https://example.org/anno/1');
expect(parsed?.steps).toHaveLength(1);
expect(parsed?.steps[0].source?.type).toBe('Model');
expect(parsed?.steps[0].transform).toEqual([{ type: 'RotateTransform', y: 90 }]);
expect(parsed?.steps[0].actions).toEqual(['play']);
});
});
5 changes: 3 additions & 2 deletions __tests__/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Test utility.

import { emptyManifest } from '@iiif/parser';
import type { Manifest, Range, RangeItems } from '@iiif/presentation-3';
import type { Manifest, Range, RangeItems } from '@iiif/parser/presentation-3/types';
import { getValue, type RangeTableOfContentsNode } from '../src';

// Render range in ascii with children + indentation.
Expand Down Expand Up @@ -93,5 +93,6 @@ export function renderRange(range: RangeTableOfContentsNode | null, skipCanvases
str += `${spaces}${isLastItem ? treeChars.corner : treeChars.tee}${treeChars.horizontal}${treeChars.horizontal}${nn} ${renderRange(item, skipCanvases, indent + 2)}`;
}
});
return str;
// Keep snapshots stable by stripping trailing spaces before line breaks.
return str.replace(/[ \t]+\n/g, '\n');
}
2 changes: 1 addition & 1 deletion __tests__/i18n.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { buildLocaleString, getValue, getAvailableLanguagesFromResource, iiifStr
import { describe, test, expect } from "vitest";
import delftExample from "../fixtures/exhibitions/novieten.json";
import compositeTest from "../fixtures/cookbook/composite.json";
import type { InternationalString } from "@iiif/presentation-3";
import type { InternationalString } from "@iiif/parser/presentation-3/types";

describe("i18n helper", () => {
describe("buildLocaleString()", () => {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/image-service.tests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { canonicalServiceUrl, fixedSizesFromScales, isImageService, supportsCustomSizes } from '@iiif/parser/image-3';
import type { ImageService } from '@iiif/presentation-3';
import type { ImageService } from '@iiif/parser/presentation-3/types';
import { describe, expect, test } from 'vitest';
import {
getCustomSizeFromService,
Expand Down
2 changes: 1 addition & 1 deletion __tests__/nav-date.tests.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Collection } from '@iiif/presentation-3';
import { Collection } from '@iiif/parser/presentation-3/types';
import { createDateNavigation } from '../src/nav-date';
import { Vault } from '../src';

Expand Down
125 changes: 124 additions & 1 deletion __tests__/parse-selector.tests.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,134 @@
import type { Selector } from '@iiif/presentation-3';
import type { Selector } from '@iiif/parser/presentation-3/types';
import type { Selector as SelectorV4 } from '@iiif/parser/presentation-4/types';
import { JSDOM } from 'jsdom';
import { describe, expect, test } from 'vitest';
import ghentChoices from '../fixtures/presentation-3/ghent-choices.json';
import { expandTarget } from '../src';
import { parseSelector } from '../src/annotation-targets/parse-selector';

describe('parse selector', () => {
test('Parsing a 3D PointSelector (including zeros + z)', () => {
expect(
parseSelector({
type: 'PointSelector',
x: 0,
y: 0,
z: 0,
} as SelectorV4)
).toMatchInlineSnapshot(`
{
"selector": {
"spatial": {
"x": 0,
"y": 0,
"z": 0,
},
"type": "PointSelector",
},
"selectors": [
{
"spatial": {
"x": 0,
"y": 0,
"z": 0,
},
"type": "PointSelector",
},
],
}
`);
});

test('Parsing a PolygonZSelector WKT', () => {
expect(
parseSelector({
type: 'PolygonZSelector',
value: 'POLYGONZ((-1.0843 2.8273 -2, 1.0843 2.8273 -2, 1.0843 0 -2, -1.0843 0 -2, -1.0843 2.8273 -2))',
} as SelectorV4)
).toMatchInlineSnapshot(`
{
"selector": {
"points3d": [
[
-1.0843,
2.8273,
-2,
],
[
1.0843,
2.8273,
-2,
],
[
1.0843,
0,
-2,
],
[
-1.0843,
0,
-2,
],
[
-1.0843,
2.8273,
-2,
],
],
"spatial": {
"height": 2.8273,
"unit": "pixel",
"width": 2.1686,
"x": -1.0843,
"y": 0,
},
"type": "PolygonZSelector",
"value": "POLYGONZ((-1.0843 2.8273 -2, 1.0843 2.8273 -2, 1.0843 0 -2, -1.0843 0 -2, -1.0843 2.8273 -2))",
},
"selectors": [
{
"points3d": [
[
-1.0843,
2.8273,
-2,
],
[
1.0843,
2.8273,
-2,
],
[
1.0843,
0,
-2,
],
[
-1.0843,
0,
-2,
],
[
-1.0843,
2.8273,
-2,
],
],
"spatial": {
"height": 2.8273,
"unit": "pixel",
"width": 2.1686,
"x": -1.0843,
"y": 0,
},
"type": "PolygonZSelector",
"value": "POLYGONZ((-1.0843 2.8273 -2, 1.0843 2.8273 -2, 1.0843 0 -2, -1.0843 0 -2, -1.0843 2.8273 -2))",
},
],
}
`);
});

describe('SVG Selectors', () => {
test('Parsing a rectangle selector', () => {
expect(
Expand Down
28 changes: 14 additions & 14 deletions __tests__/range.tests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Range, RangeItems } from '@iiif/presentation-3';
import type { ManifestNormalized } from '@iiif/presentation-3-normalized';
import type { Range, RangeItems } from '@iiif/parser/presentation-3/types';
import type { ManifestNormalized } from '@iiif/parser/presentation-3-normalized/types';
import invariant from 'tiny-invariant';
import tableOfContentManifests from '../fixtures/cookbook/toc.json';
import tableOfContentsAvManifest from '../fixtures/cookbook/toc-av.json';
Expand Down Expand Up @@ -634,35 +634,35 @@ describe('range helper', () => {
expect(renderRange(tree, true)).toMatchInlineSnapshot(`
"Wiltshire and Dorset, dub of disks / Fanny Rumble, A. Collins, Perrier (01:07:55)
├── The turmut hoeing (02:29)
├── [no-nav]
├── [no-nav]
├── She stole my heart away (02:08)
├── Dumble dum dollicky (Richard of Taunton Dean) (03:01)
├── Mrs Fanny Rumble talks about herself (01:44)
├── What shall I wear to the wedding, John? (03:25)
├── Country courtship (05:50)
├── Herbert Prince (05:00)
├── [no-nav]
├── [no-nav]
├── Introductory talk: 'The young sailor cut down in his prime' (01:25)
├── [no-nav]
├── [no-nav]
├── The young sailor cut down in his prime (02:49)
└── [no-nav]
└── [no-nav]
├── Fanny Rumble / Albert Collins / Fred Perrier (25:17)
├── [no-nav]
├── [no-nav]
├── O what shall I wear to the wedding, John? (03:57)
├── [no-nav]
├── [no-nav]
├── O what shall I wear to the wedding, John? (02:47)
├── [no-nav]
├── [no-nav]
├── The vly on the turmut (03:10)
├── [no-nav]
├── [no-nav]
├── The vly on the turmut (01:37)
├── [no-nav]
├── [no-nav]
├── Twas on a Monday morning (02:04)
├── [no-nav]
├── [no-nav]
├── Twas on a Monday morning (02:22)
├── Dumble dum dollicky (04:23)
├── [no-nav]
├── [no-nav]
└── Talk about herself (01:53)
└── [no-nav]
└── [no-nav]
"
`);
});
Expand Down
109 changes: 109 additions & 0 deletions __tests__/scene-helper.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { SceneNormalized } from '@iiif/parser/presentation-4-normalized/types';
import { describe, expect, test } from 'vitest';
import modelInScene from '../fixtures/presentation-4/scenes/01-model-in-scene.json';
import rotatedModel from '../fixtures/presentation-4/scenes/14-rotated-model.json';
import { createSceneHelper, KNOWN_SCENE_PAINTABLE_TYPES } from '../src/scenes';
import { VaultAuto } from '../src/vault/vault-auto';

describe('scene helper', () => {
test('Extracting model paintables from a Scene', () => {
const vault = new VaultAuto({ enablePresentation4: true });
vault.loadManifestSync(modelInScene.id, modelInScene);

const manifest = vault.get<{ type?: string; items: Array<{ id: string }> }>(modelInScene.id);
if (!manifest) {
throw new Error('Manifest was not loaded');
}
const sceneId = manifest.items[0].id;

const helper = createSceneHelper(vault);
const paintables = helper.getPaintables(sceneId);

expect(paintables.items).toHaveLength(1);
expect(paintables.items[0].type).toBe('model');
expect(paintables.items[0].resource.id).toBe(
'https://raw.githubusercontent.com/IIIF/3d/main/assets/astronaut/astronaut.glb'
);
expect(paintables.items[0].target?.source?.type).toBe('Scene');
});

test('Extracting transforms and 3D target PointSelector', () => {
const vault = new VaultAuto({ enablePresentation4: true });
vault.loadManifestSync(rotatedModel.id, rotatedModel);

const manifest = vault.get<{ type?: string; items: Array<{ id: string }> }>(rotatedModel.id);
if (!manifest) {
throw new Error('Manifest was not loaded');
}
const sceneId = manifest.items[0].id;
const scene = vault.get<SceneNormalized>(sceneId);
if (!scene) {
throw new Error('Scene was not loaded');
}

const helper = createSceneHelper(vault);
const paintables = helper.getPaintables(sceneId);

expect(paintables.items).toHaveLength(1);
expect(paintables.items[0].type).toBe('model');
expect(paintables.items[0].bodyTransform).toEqual([
{
type: 'RotateTransform',
x: 0,
y: 180,
z: 0,
},
]);

const target = paintables.items[0].target;
if (!target?.selector || target.selector.type !== 'PointSelector') {
throw new Error('Expected PointSelector target');
}
expect(target.selector.type).toBe('PointSelector');
expect(target.selector.spatial).toEqual({ x: 0, y: 0, z: 0 });
});

test('Includes available paintable type catalog and marks unknown resource types', () => {
const manifest = {
id: 'https://example.org/manifest',
type: 'Manifest',
items: [
{
id: 'https://example.org/scene/1',
type: 'Scene',
items: [
{
id: 'https://example.org/scene/1/page/1',
type: 'AnnotationPage',
items: [
{
id: 'https://example.org/scene/1/anno/1',
type: 'Annotation',
motivation: ['painting'],
body: {
id: 'https://example.org/resource/custom',
type: 'CustomMesh',
},
target: { id: 'https://example.org/scene/1', type: 'Scene' },
},
],
},
],
},
],
};

const vault = new VaultAuto({ enablePresentation4: true });
vault.loadManifestSync(manifest.id, manifest);

const helper = createSceneHelper(vault);
const paintables = helper.getPaintables('https://example.org/scene/1');

expect(paintables.availableTypes).toEqual(KNOWN_SCENE_PAINTABLE_TYPES);
expect(paintables.items).toHaveLength(1);
expect(paintables.items[0].type).toBe('unknown');
expect(paintables.items[0].rawType).toBe('CustomMesh');
expect(paintables.types).toEqual(['unknown']);
expect(paintables.rawTypes).toEqual(['CustomMesh']);
});
});
Loading
Loading