Skip to content

Commit 98cf4d3

Browse files
committed
Make PanoData more flexible
1 parent b7c79fd commit 98cf4d3

File tree

8 files changed

+281
-80
lines changed

8 files changed

+281
-80
lines changed

docs/guide/adapters/equirectangular-video.md

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,11 @@ Can by used to define cropping information if the video does not cover a full sp
101101
```js
102102
panorama: {
103103
source: 'path/video.mp4',
104-
data: (video) => {
105-
return {
106-
fullWidth: 6000,
107-
fullHeight: 3000,
108-
croppedWidth: video.videoWidth,
109-
croppedHeight: video.videoHeight,
110-
croppedX: (6000 - video.videoWidth) / 2,
111-
croppedY: (3000 - video.videoHeight) / 2,
112-
};
104+
data: {
105+
fullWidth: 6000,
106+
// "fullHeight" optional, always "fullWidth / 2"
107+
croppedX: 1000,
108+
croppedY: 500,
113109
},
114110
}
115111
```

docs/guide/adapters/equirectangular.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,9 @@ const viewer = new Viewer({
127127
// cropping information
128128
panoData: {
129129
fullWidth: 6000,
130-
fullHeight: 3000,
131-
croppedWidth: 4000,
132-
croppedHeight: 2000,
130+
fullHeight: 3000, // optional
131+
croppedWidth: 4000, // optional
132+
croppedHeight: 2000, // optional
133133
croppedX: 1000,
134134
croppedY: 500,
135135
},

docs/guide/config.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,15 +222,15 @@ All parameters are optional.
222222
```js
223223
panoData: {
224224
fullWidth: 6000,
225-
fullHeight: 3000,
226-
croppedWidth: 4000,
227-
croppedHeight: 2000,
225+
fullHeight: 3000, // optional
226+
croppedWidth: 4000, // optional
227+
croppedHeight: 2000, // optional
228228
croppedX: 1000,
229229
croppedY: 500,
230230
}
231231
```
232232

233-
It can also be a function to dynamically compute the cropping config depending on the loaded image.
233+
It can also be a function to dynamically compute the cropping config depending on the loaded image (note that a [default setting](./adapters/equirectangular.md#default-parameters) is already applied when no data is found).
234234

235235
```js
236236
panoData: (image, xmpData) => ({

packages/core/src/adapters/EquirectangularAdapter.ts

Lines changed: 2 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { Viewer } from '../Viewer';
44
import { SPHERE_RADIUS } from '../data/constants';
55
import { SYSTEM } from '../data/system';
66
import { EquirectangularPanorama, PanoData, PanoDataProvider, PanoramaPosition, Position, TextureData } from '../model';
7-
import { createTexture, firstNonNull, getConfigParser, getXMPValue, isNil, logWarn } from '../utils';
7+
import { createTexture, getConfigParser, getXMPValue, isNil, logWarn, mergePanoData } from '../utils';
88
import { AbstractAdapter } from './AbstractAdapter';
99

1010
/**
@@ -166,7 +166,7 @@ export class EquirectangularAdapter extends AbstractAdapter<string | Equirectang
166166
cleanPanorama.data = cleanPanorama.data(img, xmpPanoData);
167167
}
168168

169-
const panoData = this.mergePanoData(img.width, img.height, cleanPanorama.data, xmpPanoData);
169+
const panoData = mergePanoData(img.width, img.height, cleanPanorama.data, xmpPanoData);
170170

171171
const texture = this.createEquirectangularTexture(img);
172172

@@ -292,59 +292,4 @@ export class EquirectangularAdapter extends AbstractAdapter<string | Equirectang
292292
mesh.material.dispose();
293293
}
294294

295-
/**
296-
* @internal
297-
*/
298-
mergePanoData(width: number, height: number, newPanoData?: PanoData, xmpPanoData?: PanoData): PanoData {
299-
if (!newPanoData && !xmpPanoData) {
300-
const fullWidth = Math.max(width, height * 2);
301-
const fullHeight = Math.round(fullWidth / 2);
302-
const croppedX = Math.round((fullWidth - width) / 2);
303-
const croppedY = Math.round((fullHeight - height) / 2);
304-
305-
newPanoData = {
306-
fullWidth: fullWidth,
307-
fullHeight: fullHeight,
308-
croppedWidth: width,
309-
croppedHeight: height,
310-
croppedX: croppedX,
311-
croppedY: croppedY,
312-
};
313-
}
314-
315-
const panoData: PanoData = {
316-
isEquirectangular: true,
317-
fullWidth: firstNonNull(newPanoData?.fullWidth, xmpPanoData?.fullWidth, width),
318-
fullHeight: firstNonNull(newPanoData?.fullHeight, xmpPanoData?.fullHeight, height),
319-
croppedWidth: firstNonNull(newPanoData?.croppedWidth, xmpPanoData?.croppedWidth, width),
320-
croppedHeight: firstNonNull(newPanoData?.croppedHeight, xmpPanoData?.croppedHeight, height),
321-
croppedX: firstNonNull(newPanoData?.croppedX, xmpPanoData?.croppedX, 0),
322-
croppedY: firstNonNull(newPanoData?.croppedY, xmpPanoData?.croppedY, 0),
323-
poseHeading: firstNonNull(newPanoData?.poseHeading, xmpPanoData?.poseHeading, 0),
324-
posePitch: firstNonNull(newPanoData?.posePitch, xmpPanoData?.posePitch, 0),
325-
poseRoll: firstNonNull(newPanoData?.poseRoll, xmpPanoData?.poseRoll, 0),
326-
initialHeading: xmpPanoData?.initialHeading,
327-
initialPitch: xmpPanoData?.initialPitch,
328-
initialFov: xmpPanoData?.initialFov,
329-
};
330-
331-
if (panoData.croppedWidth !== width || panoData.croppedHeight !== height) {
332-
logWarn(`Invalid panoData, croppedWidth/croppedHeight is not coherent with the loaded image.
333-
panoData: ${panoData.croppedWidth}x${panoData.croppedHeight}, image: ${width}x${height}`);
334-
}
335-
if (Math.abs(panoData.fullWidth - panoData.fullHeight * 2) > 1) {
336-
logWarn('Invalid panoData, fullWidth should be twice fullHeight');
337-
panoData.fullWidth = panoData.fullHeight * 2;
338-
}
339-
if (panoData.croppedX + panoData.croppedWidth > panoData.fullWidth) {
340-
logWarn('Invalid panoData, croppedX + croppedWidth > fullWidth');
341-
panoData.croppedX = panoData.fullWidth - panoData.croppedWidth;
342-
}
343-
if (panoData.croppedY + panoData.croppedHeight > panoData.fullHeight) {
344-
logWarn('Invalid panoData, croppedY + croppedHeight > fullHeight');
345-
panoData.croppedY = panoData.fullHeight - panoData.croppedHeight;
346-
}
347-
348-
return panoData;
349-
}
350295
}

packages/core/src/model.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,9 @@ export type EquirectangularPanorama = {
106106
export type PanoData = {
107107
isEquirectangular?: true;
108108
fullWidth: number;
109-
fullHeight: number;
110-
croppedWidth: number;
111-
croppedHeight: number;
109+
fullHeight?: number;
110+
croppedWidth?: number;
111+
croppedHeight?: number;
112112
croppedX: number;
113113
croppedY: number;
114114
poseHeading?: number;

packages/core/src/utils/psv.spec.ts

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import assert from 'assert';
2-
import { cleanCssPosition, getXMPValue, parseAngle, parsePoint, parseSpeed, speedToDuration } from './psv';
2+
import { cleanCssPosition, getXMPValue, mergePanoData, parseAngle, parsePoint, parseSpeed, speedToDuration } from './psv';
3+
import { PanoData } from '../model';
34

45
describe('utils:psv:parseAngle', () => {
56
it('should normalize number', () => {
@@ -379,3 +380,190 @@ describe('utils:psv:speedToDuration', () => {
379380
assert.strictEqual(speedToDuration('2rpm', Math.PI), 15000);
380381
});
381382
});
383+
384+
describe('utils:psv:mergePanoData', () => {
385+
it('should generate default panoData for 2:1 image', () => {
386+
assertDeepEqualLenient(mergePanoData(2000, 1000), {
387+
fullWidth: 2000,
388+
fullHeight: 1000,
389+
croppedWidth: 2000,
390+
croppedHeight: 1000,
391+
croppedX: 0,
392+
croppedY: 0,
393+
} satisfies PanoData);
394+
});
395+
396+
it('should generate default panoData for partial image (horizontal)', () => {
397+
assertDeepEqualLenient(mergePanoData(2000, 500), {
398+
fullWidth: 2000,
399+
fullHeight: 1000,
400+
croppedWidth: 2000,
401+
croppedHeight: 500,
402+
croppedX: 0,
403+
croppedY: 250,
404+
} satisfies PanoData);
405+
});
406+
407+
it('should generate default panoData for partial image (vertical)', () => {
408+
assertDeepEqualLenient(mergePanoData(1000, 1000), {
409+
fullWidth: 2000,
410+
fullHeight: 1000,
411+
croppedWidth: 1000,
412+
croppedHeight: 1000,
413+
croppedX: 500,
414+
croppedY: 0,
415+
} satisfies PanoData);
416+
});
417+
418+
it('should keep XMP data', () => {
419+
assertDeepEqualLenient(mergePanoData(2000, 500, undefined, {
420+
fullWidth: 2000,
421+
fullHeight: 1000,
422+
croppedWidth: 2000,
423+
croppedHeight: 500,
424+
croppedX: 0,
425+
croppedY: 500,
426+
}), {
427+
fullWidth: 2000,
428+
fullHeight: 1000,
429+
croppedWidth: 2000,
430+
croppedHeight: 500,
431+
croppedX: 0,
432+
croppedY: 500,
433+
} satisfies PanoData);
434+
});
435+
436+
it('should keep custom data over XMP', () => {
437+
assertDeepEqualLenient(mergePanoData(2000, 500, {
438+
fullWidth: 3000,
439+
fullHeight: 1500,
440+
croppedWidth: 2000,
441+
croppedHeight: 500,
442+
croppedX: 500,
443+
croppedY: 1000,
444+
}, {
445+
fullWidth: 2000,
446+
fullHeight: 1000,
447+
croppedWidth: 2000,
448+
croppedHeight: 500,
449+
croppedX: 0,
450+
croppedY: 500,
451+
}), {
452+
fullWidth: 3000,
453+
fullHeight: 1500,
454+
croppedWidth: 2000,
455+
croppedHeight: 500,
456+
croppedX: 500,
457+
croppedY: 1000,
458+
} satisfies PanoData);
459+
});
460+
461+
it('should fix invalid fullWidth/fullHeight', () => {
462+
assertDeepEqualLenient(mergePanoData(2000, 500, {
463+
fullWidth: 2000,
464+
fullHeight: 990, // KO
465+
croppedWidth: 2000,
466+
croppedHeight: 500,
467+
croppedX: 0,
468+
croppedY: 500,
469+
}), {
470+
fullWidth: 2000,
471+
fullHeight: 1000,
472+
croppedWidth: 2000,
473+
croppedHeight: 500,
474+
croppedX: 0,
475+
croppedY: 500,
476+
} satisfies PanoData);
477+
});
478+
479+
it('should fix invalid croppedY', () => {
480+
assertDeepEqualLenient(mergePanoData(2000, 500, {
481+
fullWidth: 2000,
482+
fullHeight: 1000,
483+
croppedWidth: 2000,
484+
croppedHeight: 500,
485+
croppedX: 0,
486+
croppedY: 1000, // KO
487+
}), {
488+
fullWidth: 2000,
489+
fullHeight: 1000,
490+
croppedWidth: 2000,
491+
croppedHeight: 500,
492+
croppedX: 0,
493+
croppedY: 500,
494+
} satisfies PanoData);
495+
496+
assertDeepEqualLenient(mergePanoData(2000, 500, {
497+
fullWidth: 2000,
498+
fullHeight: 1000,
499+
croppedWidth: 2000,
500+
croppedHeight: 500,
501+
croppedX: 0,
502+
croppedY: -500, // KO
503+
}), {
504+
fullWidth: 2000,
505+
fullHeight: 1000,
506+
croppedWidth: 2000,
507+
croppedHeight: 500,
508+
croppedX: 0,
509+
croppedY: 0,
510+
} satisfies PanoData);
511+
});
512+
513+
it('should fix invalid croppedX', () => {
514+
assertDeepEqualLenient(mergePanoData(1000, 1000, {
515+
fullWidth: 2000,
516+
fullHeight: 1000,
517+
croppedWidth: 1000,
518+
croppedHeight: 1000,
519+
croppedX: 1500, // KO
520+
croppedY: 0,
521+
}), {
522+
fullWidth: 2000,
523+
fullHeight: 1000,
524+
croppedWidth: 1000,
525+
croppedHeight: 1000,
526+
croppedX: 1000,
527+
croppedY: 0,
528+
} satisfies PanoData);
529+
530+
assertDeepEqualLenient(mergePanoData(1000, 1000, {
531+
fullWidth: 2000,
532+
fullHeight: 1000,
533+
croppedWidth: 1000,
534+
croppedHeight: 1000,
535+
croppedX: -500, // KO
536+
croppedY: 0,
537+
}), {
538+
fullWidth: 2000,
539+
fullHeight: 1000,
540+
croppedWidth: 1000,
541+
croppedHeight: 1000,
542+
croppedX: 0,
543+
croppedY: 0,
544+
} satisfies PanoData);
545+
});
546+
547+
it('should complete missing fullHeight', () => {
548+
assertDeepEqualLenient(mergePanoData(1000, 1000, {
549+
fullWidth: 2000,
550+
croppedX: 500,
551+
croppedY: 0,
552+
}), {
553+
fullWidth: 2000,
554+
fullHeight: 1000,
555+
croppedWidth: 1000,
556+
croppedHeight: 1000,
557+
croppedX: 500,
558+
croppedY: 0,
559+
});
560+
});
561+
});
562+
563+
function assertDeepEqualLenient(actual: any, expected: any) {
564+
const picked = {} as any;
565+
Object.keys(expected).forEach(key => {
566+
picked[key] = actual[key];
567+
});
568+
assert.deepStrictEqual(picked, expected);
569+
}

0 commit comments

Comments
 (0)