Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default defineComponent({
const { saveConfig } = useApi();
const { prompt } = usePrompt();

const cameras = computed(() => [...cameraStore.camMap.value.keys()]);
const cameras = computed(() => cameraStore.orderedCameraNames());
/**
* Per-camera alignment status for the whole rig, driving the status block:
* the first camera (display order) is the reference (identity); every other
Expand Down
2 changes: 1 addition & 1 deletion client/dive-common/components/ImportAnnotations.vue
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export default defineComponent({
...Object.keys(cameraRegistration.homographies.value),
...Object.keys(cameraRegistration.correspondences.value),
];
const cams = [...cameraStore.camMap.value.keys()];
const cams = cameraStore.orderedCameraNames();
const reference = alignedView.reference.value ?? cams[0];
return cams.filter((camera) => camera !== reference).map((camera) => ({
camera,
Expand Down
2 changes: 1 addition & 1 deletion client/dive-common/components/MultiCamToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default defineComponent({
const enabledTracksRef = useTrackFilters().enabledAnnotations;
const inEditingMode = useEditingMode();

const cameras = computed(() => [...cameraStore.camMap.value.keys()]);
const cameras = computed(() => cameraStore.orderedCameraNames());
const canary = ref(false);
const STORAGE_KEY = 'multiCamToolbar.expanded';

Expand Down
2 changes: 1 addition & 1 deletion client/dive-common/components/MultiCamTools.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default defineComponent({
const { frame } = useTime();
const selectedTrackId = useSelectedTrackId();
const cameraStore = useCameraStore();
const cameras = computed(() => [...cameraStore.camMap.value.keys()]);
const cameras = computed(() => cameraStore.orderedCameraNames());
const canary = ref(false);
function _depend(): boolean {
return canary.value;
Expand Down
6 changes: 6 additions & 0 deletions client/dive-common/components/Viewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1534,6 +1534,9 @@ export default defineComponent({
if (meta.multiCamMedia) {
/* We're loading a multicamera dataset */
multiCamList.value = orderedMultiCamCameraNames(meta.multiCamMedia);
// Publish the persisted rig order for consumers that need to know
// which camera is first/last (see CameraStore.displayOrder).
cameraStore.displayOrder.value = multiCamList.value;
defaultCamera.value = meta.multiCamMedia.defaultDisplay;
changeCamera(defaultCamera.value);
baseMulticamDatasetId.value = datasetId.value;
Expand All @@ -1542,6 +1545,9 @@ export default defineComponent({
}
} else {
multiCamList.value = ['singleCam'];
// Clear any order carried over from a previously loaded multicam
// dataset, so orderedCameraNames falls back to camMap.
cameraStore.displayOrder.value = [];
resetMulticamAlignment();
}
cameraStore.setCameraOrder(multiCamList.value);
Expand Down
27 changes: 26 additions & 1 deletion client/src/CameraStore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
ComputedRef, Ref, computed, shallowRef, triggerRef,
ComputedRef, Ref, computed, ref, shallowRef, triggerRef,
} from 'vue';
import { cloneDeep, uniq } from 'lodash';
import {
Expand Down Expand Up @@ -68,11 +68,25 @@ export default class CameraStore {

private projectionCache: Map<AnnotationId, ComputedRef<TrackProjection | null>>;

/**
* The dataset's persisted camera display order (multiCamMedia.cameraOrder,
* via orderedMultiCamCameraNames), set by the viewer at load.
*
* camMap's own key order is insertion order: cameras are added one at a
* time inside an awaited per-camera load loop, and entries can survive a
* dataset switch, so it is not a dependable statement of rig order. Anything
* where "which camera is first/last" carries meaning -- the registration
* reference camera, the direction a loop-closure residual is measured in --
* must read this instead.
*/
displayOrder: Ref<string[]>;

constructor({ markChangesPending }: { markChangesPending: MarkChangesPending }) {
this.markChangesPending = markChangesPending;
const cameraName = 'singleCam';
this.defaultGroup = ['no-group', 1.0];
this.projectionCache = new Map();
this.displayOrder = ref([]);
this.camMap = shallowRef(new Map([[cameraName, {
trackStore: new TrackStore({ markChangesPending, cameraName }),
groupStore: new GroupStore({ markChangesPending, cameraName }),
Expand Down Expand Up @@ -224,6 +238,17 @@ export default class CameraStore {
};
}

/**
* Camera names in persisted display order, restricted to cameras actually
* present. Falls back to camMap order when no order has been set (single
* camera datasets, or before the viewer has loaded one).
*/
orderedCameraNames(): string[] {
const present = this.camMap.value;
const ordered = this.displayOrder.value.filter((name) => present.has(name));
return ordered.length === present.size ? ordered : [...present.keys()];
}

addCamera(cameraName: string) {
if (this.camMap.value.get(cameraName) === undefined) {
this.camMap.value.set(cameraName, {
Expand Down
60 changes: 60 additions & 0 deletions client/src/cameraStoreOrder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Camera display order. camMap's key order is insertion order from an awaited
* per-camera load loop, and entries survive a dataset switch, so anything that
* reads "first" or "last" camera as rig geometry -- the registration reference
* camera, the direction a loop-closure residual is measured in -- has to go
* through the persisted order instead.
*/
import { describe, expect, it } from 'vitest';
import CameraStore from './CameraStore';

/**
* A loaded multicam rig: cameras added (in whatever order the awaited load
* loop produced) and the constructor's 'singleCam' placeholder pruned, as
* Viewer.vue does once loading completes.
*/
function loadedStore(names: string[]): CameraStore {
const s = new CameraStore({ markChangesPending: () => {} });
names.forEach((n) => s.addCamera(n));
s.removeCamera('singleCam');
return s;
}

describe('CameraStore.orderedCameraNames', () => {
it('falls back to camMap order when no order is published', () => {
const s = loadedStore(['rgb', 'uv']);
expect(s.orderedCameraNames()).toEqual(['rgb', 'uv']);
});

it('returns the persisted order regardless of insertion order', () => {
// Arrive out of order, as an awaited load loop can produce.
const s = loadedStore(['uv', 'rgb', 'ir']);
expect([...s.camMap.value.keys()]).toEqual(['uv', 'rgb', 'ir']);
s.displayOrder.value = ['rgb', 'ir', 'uv'];
expect(s.orderedCameraNames()).toEqual(['rgb', 'ir', 'uv']);
});

it('ignores a stale order left by a previous dataset', () => {
// Order naming cameras this dataset does not have must not win.
const s = loadedStore(['left', 'right']);
s.displayOrder.value = ['rgb', 'ir', 'uv'];
expect(s.orderedCameraNames()).toEqual(['left', 'right']);
});

it('ignores a partial order that omits a present camera', () => {
const s = loadedStore(['rgb', 'ir', 'uv']);
s.displayOrder.value = ['rgb', 'ir'];
expect(s.orderedCameraNames()).toEqual(['rgb', 'ir', 'uv']);
});

it('falls back mid-load, while the singleCam placeholder is still present', () => {
// The transient that produced a spurious loop-closure warning: order is
// published but the rig is not fully assembled yet.
const s = new CameraStore({ markChangesPending: () => {} });
s.addCamera('rgb');
s.addCamera('ir');
s.displayOrder.value = ['rgb', 'ir', 'uv'];
expect(s.orderedCameraNames()).toEqual([...s.camMap.value.keys()]);
expect(s.orderedCameraNames()).toContain('singleCam');
});
});
Loading