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
27 changes: 27 additions & 0 deletions client/dive-common/use/useModeManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,3 +456,30 @@ describe('centerline editing continuity', () => {
expect(track.features[0].bounds).toEqual([0, 0, 100, 100]);
});
});

describe('entering polygon editing', () => {
const polygon = (key: string): GeoJSON.Feature<GeoJSON.Polygon> => ({
type: 'Feature',
properties: { key },
geometry: { type: 'Polygon', coordinates: [[[0, 0], [10, 0], [10, 10], [0, 0]]] },
});

it('lands on the keyed mask a detection already has', () => {
const { cameraStore, modeManager: manager } = makeHarness();
const id = manager.handler.trackAdd();
cameraStore.getTrack(id, 'left').setFeature({ frame: 0, keyframe: true, bounds: [0, 0, 10, 10] }, [polygon('SegmentationPolygon')]);
manager.handler.setAnnotationState({ editing: 'LineString', key: 'HeadTails' });
manager.handler.setAnnotationState({ editing: 'Polygon', key: '' });
expect(manager.selectedKey.value).toBe('SegmentationPolygon');
});

it('keeps the default polygon and an explicitly requested new key', () => {
const { cameraStore, modeManager: manager } = makeHarness();
const id = manager.handler.trackAdd();
cameraStore.getTrack(id, 'left').setFeature({ frame: 0, keyframe: true, bounds: [0, 0, 10, 10] }, [polygon(''), polygon('1')]);
manager.handler.setAnnotationState({ editing: 'Polygon', key: '' });
expect(manager.selectedKey.value).toBe('');
manager.handler.setAnnotationState({ editing: 'Polygon', key: '2' });
expect(manager.selectedKey.value).toBe('2');
});
});
15 changes: 14 additions & 1 deletion client/dive-common/use/useModeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1270,6 +1270,19 @@ export default function useModeManager({
}
}

/**
* Entering polygon editing without naming a polygon: land on one the
* detection already has (masks are often keyed, e.g. SegmentationPolygon)
* so its vertices are editable at once rather than starting a new polygon.
*/
function existingPolygonKey(): string {
if (selectedTrackId.value === null) return '';
const track = cameraStore.getPossibleTrack(selectedTrackId.value, selectedCamera.value);
const keys = track?.getPolygonFeatures(selectedCameraFrame()).map((p) => p.key) ?? [];
if (!keys.length || keys.includes('')) return '';
return keys.includes(selectedKey.value) ? selectedKey.value : keys[0];
}

function handleSetAnnotationState({
visible, editing, key, recipeName,
}: SetAnnotationStateArgs) {
Expand All @@ -1278,7 +1291,7 @@ export default function useModeManager({
}
if (editing) {
annotationModes.editing = editing;
_selectKey(key);
_selectKey(editing === 'Polygon' && !key ? existingPolygonKey() : key);
handleSelectTrack(selectedTrackId.value, true);
recipes.forEach((r) => {
if (recipeName !== r.name) {
Expand Down
6 changes: 5 additions & 1 deletion client/platform/desktop/frontend/components/ViewerLoader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1848,10 +1848,14 @@ export default defineComponent({
if (first[0] !== last[0] || first[1] !== last[1]) {
closedPolygon.push([...first] as [number, number]);
}
// Same key as the source polygon, so polygon editing reaches both.
const [sourceFeature] = sourceTrack?.getFeature(params.frameNum) ?? [null];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sourceKey = sourceFeature?.geometry?.features.find((f: any) => f.geometry.type === 'Polygon')?.properties?.key ?? '';
const segGeometry: GeoJSON.Feature[] = [{
type: 'Feature',
geometry: { type: 'Polygon', coordinates: [closedPolygon] },
properties: { key: '' },
properties: { key: sourceKey },
}];
const segBounds = response.bounds || [
Math.min(...response.polygon.map((p: [number, number]) => p[0])),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Vue, { ref } from 'vue';
import useAnnotationClickHandling from './useAnnotationClickHandling';

function harness(type = 'LineString') {
function harness(type = 'LineString', selectedCamera = 'left') {
const layer = () => ({ bus: new Vue() });
const polygon = layer();
const selectedKey = ref(type === 'LineString' ? 'HeadTails' : '');
Expand All @@ -11,7 +11,7 @@ function harness(type = 'LineString') {
const refresh = vi.fn();
useAnnotationClickHandling({
camera: 'left',
selectedCamera: ref('left'),
selectedCamera: ref(selectedCamera),
selectedTrackIdRef: ref(1),
selectedKeyRef: selectedKey,
frameNumberRef: ref(0),
Expand Down Expand Up @@ -44,6 +44,14 @@ it('defers polygon key selection to the edit layer while polygon editing', () =>
expect(h.selectFeatureHandle).not.toHaveBeenCalled();
});

it('defers to the edit layer on a camera that is not selected while polygon editing', () => {
const h = harness('Polygon', 'right');
h.polygon.bus.$emit('polygon-right-clicked', 1, 'segmentation');
h.polygon.bus.$emit('polygon-right-clicked-outside');
expect(h.selectFeatureHandle).not.toHaveBeenCalled();
expect(h.cancelCreation).not.toHaveBeenCalled();
});

it('does not cancel in-progress line creation when a mask is right-clicked', () => {
const h = harness(); h.edit.getMode = () => 'creation';
h.polygon.bus.$emit('polygon-right-clicked', 1, 'segmentation');
Expand Down
13 changes: 9 additions & 4 deletions client/src/components/layerManager/useAnnotationClickHandling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,11 @@ export default function useAnnotationClickHandling(options: {
});
editAnnotationLayer.bus.$on('polygon-edit-right-click', (geo: { x: number; y: number }) => {
const trackId = selectedTrackIdRef.value;
if (selectedCamera.value !== camera || trackId === null || editingModeRef.value !== 'Polygon') return;
if (trackId === null || editingModeRef.value !== 'Polygon') return;
// The editor that took the click is live on every camera holding the
// detection, so navigate its polygons here after selecting this camera.
if (selectedCamera.value !== camera) handler.selectCamera(camera, false);
if (selectedCamera.value !== camera) return;
const point = alignedView.mapNativePoint(geo.x, geo.y);
const hit = pickPolygon(polyAnnotationLayer.formattedData, trackId as number, point);
finishPolygonClick(trackId, hit?.polygonKey);
Expand All @@ -170,10 +174,11 @@ export default function useAnnotationClickHandling(options: {
if (editingModeRef.value === 'LineString'
|| (editAnnotationLayer.type === 'LineString' && editAnnotationLayer.getMode() !== 'disabled')) return;
if (polygonNavigationPending) return;
if (selectedCamera.value === camera && trackId === selectedTrackIdRef.value
if (trackId === selectedTrackIdRef.value
&& editingModeRef.value === 'Polygon' && editAnnotationLayer.getMode() !== 'creation') {
// The edit-layer click resolves the actual polygon hit (including
// holes) and applies the switch after GeoJS finishes this mouse event.
// holes) and applies the switch after GeoJS finishes this mouse event,
// on whichever camera's editor holds the detection.
return;
}
if (editAnnotationLayer.getMode() === 'creation') {
Expand All @@ -196,7 +201,7 @@ export default function useAnnotationClickHandling(options: {
polyAnnotationLayer.bus.$on('polygon-right-clicked-outside', () => {
if (editingModeRef.value === 'LineString'
|| (editAnnotationLayer.type === 'LineString' && editAnnotationLayer.getMode() !== 'disabled')) return;
if (selectedCamera.value === camera && selectedTrackIdRef.value !== null
if (selectedTrackIdRef.value !== null
&& editingModeRef.value === 'Polygon' && editAnnotationLayer.getMode() !== 'creation') {
// The edit layer also receives clicks in gaps between polygons.
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ function harness() {
selectFeatureHandle: vi.fn((_index, selectedKey) => { key.value = selectedKey; }),
registerFinalizeCreation: vi.fn(),
cancelCreation: vi.fn(),
selectCamera: vi.fn(),
};
const rectangle = layer();
const refresh = vi.fn();
Expand Down Expand Up @@ -122,7 +123,16 @@ it('allows switching to a polygon with the default empty key', () => {
expect(h.mode.value).toBe('Polygon');
});

it('ignores another camera and non-polygon editing modes', () => {
it('selects the clicked camera before navigating its polygons', () => {
const h = harness(); h.camera.value = 'right';
h.handler.selectCamera.mockImplementation((next: string) => { h.camera.value = next; });
h.click(21, 5); vi.runAllTimers();
expect(h.handler.selectCamera).toHaveBeenCalledExactlyOnceWith('left', false);
expect(h.key.value).toBe('second');
expect(h.mode.value).toBe('Polygon');
});

it('ignores a camera it cannot select and non-polygon editing modes', () => {
const h = harness(); h.camera.value = 'right'; h.click(21, 5);
h.camera.value = 'left'; h.mode.value = 'LineString'; h.click(21, 5);
vi.runAllTimers();
Expand Down
Loading