Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -502,10 +502,24 @@ export default defineComponent({
() => correspondences.value.length > 0 || hasLoadedTransform.value,
);

// Short L/R labels keep the toggle from overflowing on long camera
// names; the full names stay available as the button tooltip.
const alignmentModeItems = computed(() => [
{ text: 'Picking', value: 'original', disabled: false },
{ text: `${camLeft.value ?? 'A'} → ${camRight.value ?? 'B'}`, value: 'AtoB', disabled: !hasTransform.value },
{ text: `${camRight.value ?? 'B'} → ${camLeft.value ?? 'A'}`, value: 'BtoA', disabled: !hasTransform.value },
{
text: 'Picking', title: undefined, value: 'original', disabled: false,
},
{
text: 'L → R',
title: `${camLeft.value ?? 'A'} → ${camRight.value ?? 'B'}`,
value: 'AtoB',
disabled: !hasTransform.value,
},
{
text: 'R → L',
title: `${camRight.value ?? 'B'} → ${camLeft.value ?? 'A'}`,
value: 'BtoA',
disabled: !hasTransform.value,
},
]);

function setTransformType(type: TransformType) {
Expand Down Expand Up @@ -535,6 +549,18 @@ export default defineComponent({
return entries.length ? entries.join(' · ') : 'present (no displayable fields)';
});

/**
* Short L/R label for a camera in the active pair (falls back to the full
* name for anything else, though the readout only ever sees pair
* cameras) -- keeps the monospace cursor readout from overflowing on
* long camera names, matching the Overlay Warp toggle's labels.
*/
function shortCameraLabel(cam: string): string {
if (cam === camLeft.value) return 'L';
if (cam === camRight.value) return 'R';
return cam;
}

/** Live cursor readout text: this camera's coord, and its linked point in the other camera. */
const cursorReadout = computed(() => {
const cursor = registration.cursorCoord.value;
Expand All @@ -543,12 +569,12 @@ export default defineComponent({
}
const [x, y] = cursor.coord;
const other = registration.linkedPoint(cursor.camera, cursor.coord);
const here = `${cursor.camera}: (${x.toFixed(1)}, ${y.toFixed(1)})`;
const here = `${shortCameraLabel(cursor.camera)}: (${x.toFixed(1)}, ${y.toFixed(1)})`;
if (!other) {
return here;
}
const [ox, oy] = other.coord;
return `${here} -> ${other.camera}: (${ox.toFixed(1)}, ${oy.toFixed(1)})`;
return `${here} -> ${shortCameraLabel(other.camera)}: (${ox.toFixed(1)}, ${oy.toFixed(1)})`;
});

/**
Expand Down Expand Up @@ -1339,6 +1365,7 @@ export default defineComponent({
:key="item.value"
:value="item.value"
:disabled="item.disabled"
:title="item.title"
small
class="flex-grow-1"
style="text-transform: none;"
Expand Down
26 changes: 17 additions & 9 deletions client/dive-common/components/Viewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -333,20 +333,20 @@ export default defineComponent({
const showUserSettingsDialog = ref(false);

// When the Camera Registration panel opens, minimize the workspace chrome
// to give the picking view more room: collapse the left type-filter
// sidebar. The bottom controls deliberately stay as they are -- the
// timeline hosts the registration-frame marker row, which is most useful
// exactly while this panel is open. This is a soft default -- the normal
// sidebar toggle still works while registering -- and whatever layout the
// user had before is restored on close.
// to give the picking view more room: hide the type-filter sidebar,
// whichever side it is on. Bottom especially -- it competes for the same
// vertical room the two picking panes want. The bottom controls
// deliberately stay as they are -- the timeline hosts the
// registration-frame marker row, which is most useful exactly while this
// panel is open. This is a soft default -- the normal sidebar toggle
// still works while registering -- and whatever layout the user had
// before is restored on close.
const registrationActive = computed(() => context.state.active === RegistrationToolsVue.name);
let preRegistrationSidebarMode: 'left' | 'bottom' | 'collapsed' | null = null;
watch(registrationActive, (active) => {
if (active) {
preRegistrationSidebarMode = sidebarMode.value;
if (sidebarMode.value === 'left') {
sidebarMode.value = 'collapsed';
}
sidebarMode.value = 'collapsed';
} else if (preRegistrationSidebarMode !== null) {
sidebarMode.value = preRegistrationSidebarMode;
preRegistrationSidebarMode = null;
Expand Down Expand Up @@ -2372,6 +2372,7 @@ export default defineComponent({
cycleSidebarMode,
sidebarModeIcon,
sidebarModeTooltip,
registrationActive,
bottomRightPanelView,
toggleBottomRightPanel,
colorBy,
Expand Down Expand Up @@ -2543,7 +2544,14 @@ export default defineComponent({
</span>
<v-spacer />
<template #extension>
<!--
No sidebar while registering: the picking panes want the room, and
the bottom layout in particular doesn't lay them out usably. The
panel collapses the sidebar on open and restores it on close, so
the toggle is simply not offered in between.
-->
<v-tooltip
v-if="!registrationActive"
bottom
>
<template #activator="{ on }">
Expand Down
14 changes: 13 additions & 1 deletion client/platform/desktop/frontend/components/Export.vue
Original file line number Diff line number Diff line change
Expand Up @@ -409,9 +409,12 @@ export default defineComponent({
depressed
block
class="my-1"
:title="`Registration: ${file.camera}${file.destination ? ` → ${file.destination}` : ''}`"
@click="exportRegistration(file.camera)"
>
Registration: {{ file.camera }}{{ file.destination ? ` → ${file.destination}` : '' }}
<span class="registration-export-btn__label">
Registration: {{ file.camera }}{{ file.destination ? ` → ${file.destination}` : '' }}
</span>
</v-btn>
</v-col>
</v-row>
Expand Down Expand Up @@ -454,4 +457,13 @@ export default defineComponent({
font-weight: 600;
line-height: 1.3;
}

/* Long camera names overflow the block button otherwise; the full label is
still available via the button's native title tooltip on hover. */
.registration-export-btn__label {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>
14 changes: 13 additions & 1 deletion client/platform/web-girder/views/Export.vue
Original file line number Diff line number Diff line change
Expand Up @@ -548,9 +548,12 @@ export default defineComponent({
depressed
block
class="my-1"
:title="`Registration: ${file.camera}${file.destination ? ` → ${file.destination}` : ''}`"
@click="exportRegistration(file.camera)"
>
Registration: {{ file.camera }}{{ file.destination ? ` → ${file.destination}` : '' }}
<span class="registration-export-btn__label">
Registration: {{ file.camera }}{{ file.destination ? ` → ${file.destination}` : '' }}
</span>
</v-btn>
</v-col>
</v-row>
Expand Down Expand Up @@ -622,4 +625,13 @@ export default defineComponent({
overflow-y: auto;
overflow-x: hidden;
}

/* Long camera names overflow the block button otherwise; the full label is
still available via the button's native title tooltip on hover. */
.registration-export-btn__label {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>
7 changes: 7 additions & 0 deletions client/src/components/annotators/VideoAnnotator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,13 @@ export default defineComponent({
// is switching from number -> undefined, or vice versa.
function pendingUpdate() {
data.syncedFrame = Math.round(video.currentTime * props.frameRate);
// The aligned-view warp is a canvas snapshot of this <video> element,
// redrawn only on an imageRevision bump -- unlike the native pane,
// which the browser keeps live on its own. loadedmetadata bumps it
// once for the initial frame; without another bump here, a scrub
// leaves the warp showing whatever the video displayed mid-seek
// (often a black frame) instead of the frame the seek landed on.
data.imageRevision += 1;
}
video.addEventListener('loadedmetadata', loadedMetadata);
video.addEventListener('seeked', pendingUpdate);
Expand Down
34 changes: 22 additions & 12 deletions client/src/components/layerManager/useLayerManagerAlignedView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,21 +108,24 @@ export default function useLayerManagerAlignedView(options: {
return mapped;
}

/** This camera's currently displayed frame image/video, or null. */
function resolveCameraImage() {
try {
return getCameraQuadMedia(
(cam) => aggregateController.value.getController(cam),
camera,
);
} catch {
// Controllers may be cleared mid-poll during a dataset reload.
return null;
}
}

// Created before the annotation layers in LayerManager so its geojs layer
// z-orders beneath boxes/polygons/text (geojs stacks layers by creation order).
const alignedImageLayer = new AlignedImageLayer({
annotator,
getImage: () => {
try {
return getCameraQuadMedia(
(cam) => aggregateController.value.getController(cam),
camera,
);
} catch {
// Controllers may be cleared mid-poll during a dataset reload.
return null;
}
},
getImage: resolveCameraImage,
getTransform: () => alignedDisplayTransform.value,
// Follows the camera-lock toggle, the existing control for "the camera may
// recenter itself". Never while creating/editing geometry, where
Expand Down Expand Up @@ -151,8 +154,15 @@ export default function useLayerManagerAlignedView(options: {
// seek itself (blanking promptly when a frame has no image, e.g. an
// aligned-timeline gap); the element the seek eventually loads arrives
// through the imageRevision watch below.
//
// Video panes skip this: a video-backed camera never actually hits that
// gap case (see VideoAnnotator's seek(), unreachable by construction),
// and the browser hasn't decoded the target frame yet at this instant --
// repainting here only catches the stale pre-seek frame, flickering
// before the imageRevision watch below repaints with the frame the seek
// actually lands on. Image panes still need the immediate repaint.
watch(frameNumberRef, () => {
if (alignedDisplayTransform.value) {
if (alignedDisplayTransform.value && resolveCameraImage()?.kind !== 'video') {
alignedImageLayer.update();
}
});
Expand Down
Loading