Skip to content
Merged
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
5 changes: 4 additions & 1 deletion client/dive-common/components/AnnotationVisibilityMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,10 @@ export default defineComponent({
</script>

<template>
<span class="toolbar-group-host">
<span
class="toolbar-group-host"
:class="{ 'toolbar-group-host--expanded': isExpanded }"
>
<!-- Dropdown mode when collapsed -->
<v-menu
v-if="!isExpanded"
Expand Down
27 changes: 19 additions & 8 deletions client/dive-common/components/EditorMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,10 @@ export default defineComponent({
</div>
</div>
<!-- Collapsed mode for edit buttons -->
<span class="toolbar-group-host">
<span
class="toolbar-group-host"
:class="{ 'toolbar-group-host--expanded': isEditButtonsExpanded }"
>
<v-menu
v-if="!isEditButtonsExpanded"
:key="editButtonsMenuKey"
Expand All @@ -448,13 +451,14 @@ export default defineComponent({
<v-btn
v-bind="attrs"
:disabled="!editingMode || activeEditButton?.loading"
:loading="!!activeEditButton?.loading"
:color="activeEditButton?.active ? editingHeader.color : ''"
class="mx-1 mode-button toolbar-group-activator"
small
v-on="on"
>
<pre v-if="activeEditButton?.mousetrap">{{ activeEditButton.mousetrap[0].bind }}:</pre>
<v-icon :class="{ 'mdi-spin': activeEditButton?.loading }">
<v-icon>
{{ activeEditButton?.icon }}
</v-icon>
<toolbar-expand-toggle
Expand All @@ -480,6 +484,7 @@ export default defineComponent({
>
<v-btn
:disabled="button.unavailable ? !!button.loading : (!editingMode || !!button.loading)"
:loading="!!button.loading"
:outlined="!button.active"
:color="button.active ? editingHeader.color : ''"
:class="{ 'edit-btn-unavailable': button.unavailable && !button.loading }"
Expand All @@ -488,7 +493,7 @@ export default defineComponent({
@click="button.click"
>
<pre v-if="button.mousetrap">{{ button.mousetrap[0].bind }}:</pre>
<v-icon :class="{ 'mdi-spin': button.loading }">
<v-icon>
{{ button.icon }}
</v-icon>
</v-btn>
Expand Down Expand Up @@ -535,6 +540,7 @@ export default defineComponent({
>
<v-btn
:disabled="button.unavailable ? !!button.loading : (!editingMode || !!button.loading)"
:loading="!!button.loading"
:outlined="!button.active"
:color="button.active ? editingHeader.color : ''"
:class="{ 'edit-btn-unavailable': button.unavailable && !button.loading }"
Expand All @@ -543,7 +549,7 @@ export default defineComponent({
@click="button.click"
>
<pre v-if="button.mousetrap">{{ button.mousetrap[0].bind }}:</pre>
<v-icon :class="{ 'mdi-spin': button.loading }">
<v-icon>
{{ button.icon }}
</v-icon>
</v-btn>
Expand All @@ -555,10 +561,6 @@ export default defineComponent({
</span>
<!-- Segmentation Reset button -->
<template v-if="activeSegmentationRecipe && editingMode === 'Point'">
<v-divider
vertical
class="mx-2"
/>
<v-btn
color="error"
class="mx-1"
Expand Down Expand Up @@ -773,4 +775,13 @@ export default defineComponent({
border: 1px solid grey;
min-width: 36px;
}

/*
* Keep the segmentation reset divider from stretching to the full toolbar
* height (the flex row can be tall when the edit-types group is expanded).
*/
.segmentation-divider {
align-self: center;
max-height: 28px;
}
</style>
1 change: 1 addition & 0 deletions client/dive-common/components/MultiCamToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ export default defineComponent({
v-if="selectedTrackId !== null && cameras.length > 1"
v-mousetrap="mousetrap"
class="toolbar-group-host"
:class="{ 'toolbar-group-host--expanded': isExpanded }"
>
<!-- Dropdown mode when collapsed -->
<v-menu
Expand Down
80 changes: 75 additions & 5 deletions client/dive-common/components/Viewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
FilterList,
} from 'vue-media-annotator/components';
import type { AnnotationId } from 'vue-media-annotator/BaseAnnotation';
import { getResponseError } from 'vue-media-annotator/utils';
import { getResponseError, featureHasSegmentationPolygon } from 'vue-media-annotator/utils';

/* DIVE COMMON */
import PolygonBase from 'dive-common/recipes/polygonbase';
Expand Down Expand Up @@ -736,7 +736,7 @@ export default defineComponent({
}
}

const selectCamera = async (camera: string, editMode = false) => {
const selectCamera = async (camera: string, editMode = false, preserveSelection = false) => {
if (linkingCamera.value !== '' && linkingCamera.value !== camera) {
await prompt({
title: 'In Linking Mode',
Expand All @@ -746,8 +746,20 @@ export default defineComponent({
});
return;
}
// EditTrack is set false by the LayerMap before executing this
if (selectedTrackId.value !== null) {
// Segmentation prompt points belong to the current camera's image: lock
// in any pending mask (committed to the still-selected camera) and clear
// the points before switching, so they cannot leak into a prediction on
// the new camera. No-op when nothing is pending.
if (selectedCamera.value !== camera) {
handler.segmentationFinalizePending();
}
// EditTrack is set false by the LayerMap before executing this.
// Skip during cross-camera continuation (preserveSelection): the source
// camera's track is legitimately empty because the geometry is being
// drawn on the target camera under the same track id. Aborting here would
// remove that track and null selectedTrackId, so the in-progress draw
// would then commit with no selected track and throw.
if (!preserveSelection && selectedTrackId.value !== null) {
// If we had a track selected and it still exists with
// a feature length of 0 we need to remove it
const track = cameraStore.getPossibleTrack(selectedTrackId.value, selectedCamera.value);
Expand Down Expand Up @@ -785,6 +797,35 @@ export default defineComponent({
}
return track.getFeature(aggregateController.value.frame.value)[0] == null;
};
// While editing, the creation cursor is live on any camera still missing
// the selected track's geometry at this frame (see LayerManager's
// cameraAwaitingGeometry, which this must mirror), so the detection can
// be drawn on each camera in turn without switching first. A left-click
// on such a camera is the start of that draw -- don't steal it to switch
// cameras. For Point mode (point-click segmentation) and Polygon mode,
// "missing" means no polygon at the selected key here yet, so a box-only
// detection still accepts a draw.
const isExtendingDetectionToCamera = (camera: string): boolean => {
if (selectedTrackId.value === null || !editingTrack.value) {
return false;
}
const editingType = editingMode.value;
if (!editingType) {
return false;
}
const track = cameraStore.getPossibleTrack(selectedTrackId.value, camera);
if (!track) {
return true;
}
const [feature] = track.getFeature(aggregateController.value.frame.value);
if (feature == null) {
return true;
}
if (editingType === 'Point' || editingType === 'Polygon') {
return !featureHasSegmentationPolygon(feature, selectedKey.value);
}
return false;
};
// Handles changing camera using the dropdown or mouse clicks
// When using mouse clicks and right button it will remain in edit mode for the selected track
const changeCamera = (camera: string, event?: MouseEvent) => {
Expand All @@ -796,7 +837,36 @@ export default defineComponent({
if (isCreatingNewDetection()) {
return;
}
if (event) {
// Likewise, when a camera is still missing the selected track's
// geometry its creation cursor is live (see LayerManager): a left-click
// there starts a draw and a right-click cancels creation -- finalize
// what was committed and deselect, exactly like the single-camera
// behavior (the edit layer's own right-click handler does this).
// Neither must be stolen to switch cameras: switching mid-draw runs
// trackEdit -> finalizeInProgress, which interrupts the in-progress
// line instead of finalizing the detection. The dropdown (no event) is
// unaffected.
if (event && isExtendingDetectionToCamera(camera)) {
return;
}
// A right-click while editing must finalize and deselect the detection
// in a single press -- matching single-camera behavior -- not merely
// switch cameras (which used to leave the detection selected until a
// second right-click on the new camera). Right-clicks ON an annotation
// never reach here: the annotation layers' right-click handoff switches
// the selected camera synchronously first, so this handler returns at
// the top (same camera).
if (event?.button === 2 && editingTrack.value) {
handler.trackSelect(null, false);
return;
}
// While editing a track that exists on the target camera, its edit
// handles are live there too (see LayerManager): this mousedown may be
// the start of a handle drag, which preventDefault would kill. The
// switch still proceeds so the edit commits to the target camera.
const editingOnTarget = editingTrack.value && selectedTrackId.value !== null
&& !!cameraStore.getPossibleTrack(selectedTrackId.value, camera);
if (event && !editingOnTarget) {
event.preventDefault();
}
// Left click should kick out of editing mode, unless the selected track
Expand Down
8 changes: 8 additions & 0 deletions client/dive-common/components/toolbarGroup.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
display: inline-flex;
align-items: center;
vertical-align: middle;
}

/*
* Only shift the group up when expanded, to compensate for the fieldset legend
* that pokes above the toolbar. In collapsed mode there is no legend, so the
* shift would misalign the activator button with the rest of the toolbar.
*/
.toolbar-group-host--expanded {
margin-top: -9px;
}

Expand Down
36 changes: 36 additions & 0 deletions client/dive-common/use/useModeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,41 @@ export default function useModeManager({
activeSegRecipes.forEach((r) => r.activate());
}

/**
* Finalize any pending segmentation on the current camera WITHOUT
* deselecting the track, then re-arm the recipe. Used by the cross-camera
* continuation flow: when a segmentation point click lands on another
* camera, the current camera's pending mask is locked in first, and the
* recipe's accumulated prompt points are cleared so they cannot leak into
* the other camera's prediction (the points belong to this camera's image).
*/
function handleSegmentationFinalizePending() {
const activeSegRecipes = recipes.filter(
(r): r is SegmentationPointClick => r instanceof SegmentationPointClick && r.active.value,
);
if (!activeSegRecipes.length) {
return;
}
if (!activeSegRecipes.some((r) => r.hasPendingPrediction() || r.hasPoints())) {
// Nothing in progress on this camera: the recipe is already fresh.
return;
}
// confirm() commits the pending mask (or just deactivates when there is
// none); the commit targets the still-selected camera's track.
activeSegRecipes.forEach((r) => r.confirm());
// The confirmed polygons are now permanent.
preSegmentationFeatures.clear();
onStereoSegmentationFinalize?.();
// Re-activate so the next click (on the other camera) segments fresh.
// completeActivation re-emits Point editing mode and keeps the current
// selection (handleSetAnnotationState re-selects the same track).
activeSegRecipes.forEach((r) => {
if (!r.active.value) {
r.activate();
}
});
}

/**
* Merge: Enabled whenever there are candidates in the merge list
*/
Expand Down Expand Up @@ -1542,6 +1577,7 @@ export default function useModeManager({
handler: {
commitMerge: handleCommitMerge,
confirmRecipe: handleConfirmRecipe,
segmentationFinalizePending: handleSegmentationFinalizePending,
groupAdd: handleAddGroup,
deleteSelectedTracks: handleDeleteSelectedTracks,
groupEdit: handleGroupEdit,
Expand Down
Loading
Loading