From 8abe79b256d8912c868ef3e62f903829d4713c07 Mon Sep 17 00:00:00 2001 From: yannik131 Date: Wed, 17 Jun 2026 13:17:05 +0200 Subject: [PATCH 01/34] Fixed lawnmower center section first bug --- resources/assets/js/annotations/annotatorContainer.vue | 5 +++++ .../assets/js/annotations/components/annotationCanvas.vue | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/resources/assets/js/annotations/annotatorContainer.vue b/resources/assets/js/annotations/annotatorContainer.vue index ff5596dcf2..fde6ec2e9c 100644 --- a/resources/assets/js/annotations/annotatorContainer.vue +++ b/resources/assets/js/annotations/annotatorContainer.vue @@ -676,6 +676,11 @@ export default { let [image, annotations] = await Promise.all(this.getImageAndAnnotationsPromises(id)); this.image = image; this.annotations = annotations; + + // Wait for the canvas to be updated, otherwise lawnmower mode + // could compute the current section using the old viewport + await this.$nextTick(); + this.maybeUpdateAnnotationMode(); this.maybeShowTilingInProgressMessage(); } catch (e) { diff --git a/resources/assets/js/annotations/components/annotationCanvas.vue b/resources/assets/js/annotations/components/annotationCanvas.vue index 884d9ae1b8..60d2b5f7e9 100644 --- a/resources/assets/js/annotations/components/annotationCanvas.vue +++ b/resources/assets/js/annotations/components/annotationCanvas.vue @@ -155,7 +155,7 @@ export default { }, draftAnnotationUsesLabelColor: { type: Boolean, - default: true, + default: true, }, }, data() { From 319d081d08cc66672f0587af887368e9612f20b9 Mon Sep 17 00:00:00 2001 From: yannik131 Date: Wed, 17 Jun 2026 16:57:20 +0200 Subject: [PATCH 02/34] Added playPause widget --- .../components/annotationModesTab.vue | 7 ++ .../assets/js/core/components/playPause.vue | 84 +++++++++++++++++++ .../show/tabs/annotationModes.blade.php | 9 +- 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 resources/assets/js/core/components/playPause.vue diff --git a/resources/assets/js/annotations/components/annotationModesTab.vue b/resources/assets/js/annotations/components/annotationModesTab.vue index 975d55f655..ce77cb528c 100644 --- a/resources/assets/js/annotations/components/annotationModesTab.vue +++ b/resources/assets/js/annotations/components/annotationModesTab.vue @@ -2,6 +2,8 @@ import Events from '@/core/events.js'; import Keyboard from '@/core/keyboard.js'; import PowerToggle from '@/core/components/powerToggle.vue'; +import PlayPause from '@/core/components/playPause.vue'; +import { PlayPauseState } from '@/core/components/playPause.vue'; import Settings from '../stores/settings.js'; import {urlParams as UrlParams} from '@/core/utils.js'; @@ -19,6 +21,7 @@ export default { ], components: { powerToggle: PowerToggle, + playPause: PlayPause }, data() { return { @@ -38,6 +41,7 @@ export default { randomSamplingNumber: 9, regularSamplingRows: 3, regularSamplingColumns: 3, + playPauseState: PlayPauseState.STOPPED, }; }, computed: { @@ -81,6 +85,9 @@ export default { emitCreateSample() { this.$emit('create-sample'); }, + handlePlayPauseStateTransition(target) { + this.playPauseState = target; + } }, watch: { mode(mode, oldMode) { diff --git a/resources/assets/js/core/components/playPause.vue b/resources/assets/js/core/components/playPause.vue new file mode 100644 index 0000000000..f4d5a74529 --- /dev/null +++ b/resources/assets/js/core/components/playPause.vue @@ -0,0 +1,84 @@ + + + \ No newline at end of file diff --git a/resources/views/annotations/show/tabs/annotationModes.blade.php b/resources/views/annotations/show/tabs/annotationModes.blade.php index 3b61240737..dedbe4219a 100644 --- a/resources/views/annotations/show/tabs/annotationModes.blade.php +++ b/resources/views/annotations/show/tabs/annotationModes.blade.php @@ -15,7 +15,14 @@ @can('add-annotation', $image) diff --git a/resources/views/annotations/show/tabs/annotationModes.blade.php b/resources/views/annotations/show/tabs/annotationModes.blade.php index dedbe4219a..ada270b087 100644 --- a/resources/views/annotations/show/tabs/annotationModes.blade.php +++ b/resources/views/annotations/show/tabs/annotationModes.blade.php @@ -1,5 +1,10 @@ - + @push('scripts') @@ -16,13 +21,12 @@ @can('add-annotation', $image) From 5f732cbdb643c7ffedf93015137e518a2cf17088 Mon Sep 17 00:00:00 2001 From: yannik131 Date: Thu, 18 Jun 2026 16:47:35 +0200 Subject: [PATCH 05/34] Renamed play/pause states --- .../js/annotations/annotatorContainer.vue | 4 ++-- .../components/annotationCanvas/lawnmower.vue | 2 +- .../components/annotationModesTab.vue | 2 +- .../assets/js/core/components/playPause.vue | 20 +++++++++---------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/resources/assets/js/annotations/annotatorContainer.vue b/resources/assets/js/annotations/annotatorContainer.vue index 842505cbd0..fa570d42c6 100644 --- a/resources/assets/js/annotations/annotatorContainer.vue +++ b/resources/assets/js/annotations/annotatorContainer.vue @@ -97,7 +97,7 @@ export default { imageFilenames: {}, labelTrees: [], projectIds: [], - currentLawnmowerState: PlayPauseState.STOPPED, + currentLawnmowerState: PlayPauseState.INACTIVE, lawnmowerSaveState: "" }; }, @@ -624,7 +624,7 @@ export default { }, handleAnnotationModeChange(mode, data) { if (mode !== 'default' && mode !== 'lawnmower') { - this.handleLawnmowerStateTransitionRequest(PlayPauseState.STOPPED); + this.handleLawnmowerStateTransitionRequest(PlayPauseState.INACTIVE); } this.annotationMode = mode; this.annotationModeCarry = null; diff --git a/resources/assets/js/annotations/components/annotationCanvas/lawnmower.vue b/resources/assets/js/annotations/components/annotationCanvas/lawnmower.vue index 6226e07b10..bbc7e76503 100644 --- a/resources/assets/js/annotations/components/annotationCanvas/lawnmower.vue +++ b/resources/assets/js/annotations/components/annotationCanvas/lawnmower.vue @@ -29,7 +29,7 @@ export default { imageSection: [0, 0], // Actual center point of the current image section. imageSectionCenter: [0, 0], - lawnmowerState: PlayPauseState.STOPPED, + lawnmowerState: PlayPauseState.INACTIVE, pendingLawnmowerState: null }; }, diff --git a/resources/assets/js/annotations/components/annotationModesTab.vue b/resources/assets/js/annotations/components/annotationModesTab.vue index d43ffb2f2c..07c7cae6fd 100644 --- a/resources/assets/js/annotations/components/annotationModesTab.vue +++ b/resources/assets/js/annotations/components/annotationModesTab.vue @@ -139,7 +139,7 @@ export default { Settings.set('regularSamplingColumns', number); }, currentLawnmowerState(newState) { - if (newState === PlayPauseState.PLAYING) { + if (newState === PlayPauseState.ACTIVE) { this.startLawnmower(); } else if (newState === PlayPauseState.PAUSED) { this.resetMode(); diff --git a/resources/assets/js/core/components/playPause.vue b/resources/assets/js/core/components/playPause.vue index f4d5a74529..a7c1b16481 100644 --- a/resources/assets/js/core/components/playPause.vue +++ b/resources/assets/js/core/components/playPause.vue @@ -19,13 +19,13 @@ diff --git a/resources/assets/js/annotations/components/annotationCanvas/volare.js b/resources/assets/js/annotations/components/annotationCanvas/volare.js new file mode 100644 index 0000000000..576a86d434 --- /dev/null +++ b/resources/assets/js/annotations/components/annotationCanvas/volare.js @@ -0,0 +1,119 @@ +import { ref, computed, watch } from 'vue'; +import { required } from '@/utils.js'; + + +export function useVolareMode({ + filteredAnnotations = required('filteredAnnotations'), + selectedAnnotations = required('selectedAnnotations'), + focusAnnotationInCanvas = required('focusAnnotationInCanvas'), + fitImageInCanvas = required('fitImageInCanvas'), + volareModeIsActive = required('volareModeIsActive'), + annotationFilter = required('annotationFilter') +}) { + const focussedAnnotationIndex = ref(null); + const userUpdateVolareResolution = ref(false); + + const focussedAnnotation = computed(() => { + return filteredAnnotations.value[focussedAnnotationIndex.value]; + }); + + function focusAnnotation(annotation, fast, keepResolution) { + focusAnnotationInCanvas(annotation, fast, keepResolution); + } + + function selectAndFocusAnnotation(annotation, keepResolution = false) { + selectedAnnotations.value.forEach(a => { + a.selected = false; + }); + annotation.selected = true; + focusAnnotationInCanvas(annotation, true, keepResolution); + } + + function updateFocussedAnnotation() { + if (!volareModeIsActive.value) { + focussedAnnotationIndex.value = null; + return; + } else if (filteredAnnotations.value.length === 0) { + // Show the whole image if there are no annotations. + focussedAnnotationIndex.value = null; + fitImageInCanvas(); + return; + } + + if (focussedAnnotationIndex.value === Infinity) { + // Show the last annotation if the previous image is shown. + focussedAnnotationIndex.value = filteredAnnotations.value.length - 1; + } else { + // Show the first annotation if the next image is shown or + // the annotation filter changed. + focussedAnnotationIndex.value = 0; + } + } + + function handleNextAnnotation() { + if (!volareModeIsActive.value) { + return false; + } + + if (focussedAnnotationIndex.value < (filteredAnnotations.value.length - 1)) { + focussedAnnotationIndex.value++; + return true; + } else if (focussedAnnotationIndex.value === Infinity) { + // This may happen if the volume has only one image and we can't + // switch to the next image. Here, we want to go to the second + // annotation of the image. + focussedAnnotationIndex.value = Math.min(1, filteredAnnotations.value.length - 1); + } else { + // Show the first annotation of the next image in this case, so + // don't return. + focussedAnnotationIndex.value = -Infinity; + } + + return false; + } + + function handlePreviousAnnotation() { + if (!volareModeIsActive.value) { + return false; + } + + if (focussedAnnotationIndex.value > 0) { + focussedAnnotationIndex.value--; + return true; + } else if (focussedAnnotationIndex.value === -Infinity) { + // This may happen if the volume has only one image and we can't + // switch to the next image. Here, we want to go to the second to + // last annotation of the image. + focussedAnnotationIndex.value = Math.max(filteredAnnotations.value.length - 2, 0); + } else { + // Show the last annotation of the previous image in this case, + // so don't return. + focussedAnnotationIndex.value = Infinity; + } + + return false; + } + + watch(focussedAnnotation, (annotation) => { + if (annotation) { + selectAndFocusAnnotation(annotation, userUpdateVolareResolution.value); + } + }); + watch(() => annotationFilter?.value, updateFocussedAnnotation); + watch(volareModeIsActive, (enabled) => { + if (!enabled) { + userUpdateVolareResolution.value = false; + } + }) + + return { + focussedAnnotationIndex, + focussedAnnotation, + focusAnnotation, + userUpdateVolareResolution, + selectAndFocusAnnotation, + updateFocussedAnnotation, + handleNextAnnotation, + handlePreviousAnnotation, + }; +} \ No newline at end of file diff --git a/resources/assets/js/utils.js b/resources/assets/js/utils.js index da7b47efdc..1c639dc7aa 100644 --- a/resources/assets/js/utils.js +++ b/resources/assets/js/utils.js @@ -62,4 +62,8 @@ let declare = function (namespace, object) { return object; }; -export {mount, require, declare}; +function required(name) { + throw new Error(`Missing required parameter '${name}'`); +} + +export {mount, require, declare, required}; From 1f6e23516dff1ea312ac00d7d05623be8ff2b633 Mon Sep 17 00:00:00 2001 From: yannik131 Date: Fri, 19 Jun 2026 17:37:48 +0200 Subject: [PATCH 17/34] Moved more code to volare component --- .../js/annotations/annotatorContainer.vue | 20 +------------- .../components/annotationCanvas/volare.js | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/resources/assets/js/annotations/annotatorContainer.vue b/resources/assets/js/annotations/annotatorContainer.vue index d94ef6f100..45dddd2c2b 100644 --- a/resources/assets/js/annotations/annotatorContainer.vue +++ b/resources/assets/js/annotations/annotatorContainer.vue @@ -777,25 +777,7 @@ export default { volareModeIsActive: computed(() => this.isVolareAnnotationMode), annotationFilter: this.annotationFilter }); - - Events.on('annotations.focus', this.volare.focusAnnotation); - - if (UrlParams.get('annotation')) { - let id = parseInt(UrlParams.get('annotation')); - Events.once('images.change', () => { - let annotations = this.annotations; - for (let i = annotations.length - 1; i >= 0; i--) { - if (annotations[i].id === id) { - // Use $nextTick so the annotationCanvas component has time to - // render the image. - this.$nextTick( - () => this.volare.selectAndFocusAnnotation(annotations[i]) - ); - return; - } - } - }); - } + this.volare.registerEvents(this.annotations); } }; diff --git a/resources/assets/js/annotations/components/annotationCanvas/volare.js b/resources/assets/js/annotations/components/annotationCanvas/volare.js index 576a86d434..e3f921ce01 100644 --- a/resources/assets/js/annotations/components/annotationCanvas/volare.js +++ b/resources/assets/js/annotations/components/annotationCanvas/volare.js @@ -1,5 +1,7 @@ import { ref, computed, watch } from 'vue'; import { required } from '@/utils.js'; +import { urlParams as UrlParams } from '@/core/utils.js'; +import Events from '@/core/events.js'; export function useVolareMode({ @@ -17,10 +19,6 @@ export function useVolareMode({ return filteredAnnotations.value[focussedAnnotationIndex.value]; }); - function focusAnnotation(annotation, fast, keepResolution) { - focusAnnotationInCanvas(annotation, fast, keepResolution); - } - function selectAndFocusAnnotation(annotation, keepResolution = false) { selectedAnnotations.value.forEach(a => { a.selected = false; @@ -94,6 +92,23 @@ export function useVolareMode({ return false; } + function registerEvents(annotations) { + if (UrlParams.get('annotation')) { + const id = parseInt(UrlParams.get('annotation')); + Events.once('images.change', () => { + const annotation = annotations.value.find(a => a.id === id); + if (!annotation) { + return; + } + nextTick(() => { + selectAndFocusAnnotation(annotation); + }); + }); + } + + Events.on('annotations.focus', focusAnnotationInCanvas); + } + watch(focussedAnnotation, (annotation) => { if (annotation) { selectAndFocusAnnotation(annotation, userUpdateVolareResolution.value); @@ -104,16 +119,16 @@ export function useVolareMode({ if (!enabled) { userUpdateVolareResolution.value = false; } - }) + }); return { focussedAnnotationIndex, focussedAnnotation, - focusAnnotation, userUpdateVolareResolution, selectAndFocusAnnotation, updateFocussedAnnotation, handleNextAnnotation, handlePreviousAnnotation, + registerEvents }; } \ No newline at end of file From f866ab56d2ef19af76538ed178d6994015d8be00 Mon Sep 17 00:00:00 2001 From: yannik131 Date: Fri, 19 Jun 2026 18:14:58 +0200 Subject: [PATCH 18/34] Implemented load/save for volare --- .../js/annotations/annotatorContainer.vue | 29 ++++++++++-- .../components/annotationCanvas/volare.js | 46 ++++++++++++------- resources/views/annotations/show.blade.php | 2 +- 3 files changed, 56 insertions(+), 21 deletions(-) diff --git a/resources/assets/js/annotations/annotatorContainer.vue b/resources/assets/js/annotations/annotatorContainer.vue index 45dddd2c2b..41e265c312 100644 --- a/resources/assets/js/annotations/annotatorContainer.vue +++ b/resources/assets/js/annotations/annotatorContainer.vue @@ -18,7 +18,7 @@ import SettingsTab from './components/settingsTab.vue'; import Sidebar from '@/core/components/sidebar.vue'; import SidebarTab from '@/core/components/sidebarTab.vue'; import VolumeImageAreaApi from './api/volumes.js'; -import {computed, defineAsyncComponent} from 'vue' +import {computed, defineAsyncComponent, watch} from 'vue' import {CrossOriginTiffError} from './stores/images.js'; import {debounce} from '@/core/utils.js'; import {handleErrorResponse} from '@/core/messages/store.js'; @@ -559,7 +559,7 @@ export default { this.currentLawnmowerState = targetState; }, - restoreLawnmowerImage(imageId) { + loadImageWithId(imageId) { const index = this.imagesIds.indexOf(imageId); if (index !== -1) { this.imageIndex = index; @@ -750,6 +750,26 @@ export default { Events.on('annotations.deselect', this.handleDeselectAnnotation); Events.on('annotations.detachLabel', this.handleDetachAnnotationLabel); Events.on('annotations.delete', this.handleDeleteAnnotation); + Events.on('annotations.focus', this.focusAnnotation); + + // TODO Are these necessary? Should they be moved to the new volare composable? + + if (UrlParams.get('annotation')) { + let id = parseInt(UrlParams.get('annotation')); + Events.once('images.change', () => { + let annotations = this.annotations; + for (let i = annotations.length - 1; i >= 0; i--) { + if (annotations[i].id === id) { + // Use $nextTick so the annotationCanvas component has time to + // render the image. + this.$nextTick( + () => this.volare.selectAndFocusAnnotation(annotations[i]) + ); + return; + } + } + }); + } if (Settings.has('openTab')) { let openTab = Settings.get('openTab'); @@ -775,9 +795,10 @@ export default { focusAnnotationInCanvas: this.focusAnnotation, fitImageInCanvas: (...args) => this.$refs.canvas.fitImage(...args), volareModeIsActive: computed(() => this.isVolareAnnotationMode), - annotationFilter: this.annotationFilter + annotationFilter: this.annotationFilter, + image: computed(() => this.image) }); - this.volare.registerEvents(this.annotations); + watch(this.volare.requestedImageId, this.loadImageWithId); } }; diff --git a/resources/assets/js/annotations/components/annotationCanvas/volare.js b/resources/assets/js/annotations/components/annotationCanvas/volare.js index e3f921ce01..29d5b6601f 100644 --- a/resources/assets/js/annotations/components/annotationCanvas/volare.js +++ b/resources/assets/js/annotations/components/annotationCanvas/volare.js @@ -1,4 +1,4 @@ -import { ref, computed, watch } from 'vue'; +import { ref, computed, watch, nextTick } from 'vue'; import { required } from '@/utils.js'; import { urlParams as UrlParams } from '@/core/utils.js'; import Events from '@/core/events.js'; @@ -10,10 +10,14 @@ export function useVolareMode({ focusAnnotationInCanvas = required('focusAnnotationInCanvas'), fitImageInCanvas = required('fitImageInCanvas'), volareModeIsActive = required('volareModeIsActive'), - annotationFilter = required('annotationFilter') + annotationFilter = required('annotationFilter'), + image = required('image') }) { const focussedAnnotationIndex = ref(null); const userUpdateVolareResolution = ref(false); + const requestedImageId = ref(null); + let pendingVolareState = null; + let savedState = null; const focussedAnnotation = computed(() => { return filteredAnnotations.value[focussedAnnotationIndex.value]; @@ -92,21 +96,26 @@ export function useVolareMode({ return false; } - function registerEvents(annotations) { - if (UrlParams.get('annotation')) { - const id = parseInt(UrlParams.get('annotation')); - Events.once('images.change', () => { - const annotation = annotations.value.find(a => a.id === id); - if (!annotation) { - return; - } - nextTick(() => { - selectAndFocusAnnotation(annotation); - }); - }); + function saveState() { + savedState = { + imageId: image.value.id, + focussedAnnotationIndex: focussedAnnotationIndex.value, + }; + } + + function loadState() { + if (!savedState) { + return; + } else if (savedState.imageId !== image.value.id) { + requestedImageId.value = savedState.imageId; + return; } - Events.on('annotations.focus', focusAnnotationInCanvas); + focussedAnnotationIndex.value = null; + nextTick(() => { + focussedAnnotationIndex.value = savedState.focussedAnnotationIndex; + savedState = null; + }); } watch(focussedAnnotation, (annotation) => { @@ -120,6 +129,9 @@ export function useVolareMode({ userUpdateVolareResolution.value = false; } }); + watch(() => image?.value, () => { + nextTick(loadState); + }); return { focussedAnnotationIndex, @@ -129,6 +141,8 @@ export function useVolareMode({ updateFocussedAnnotation, handleNextAnnotation, handlePreviousAnnotation, - registerEvents + requestedImageId, + saveState, + loadState, }; } \ No newline at end of file diff --git a/resources/views/annotations/show.blade.php b/resources/views/annotations/show.blade.php index e353a4c75c..3ccd61c4bd 100644 --- a/resources/views/annotations/show.blade.php +++ b/resources/views/annotations/show.blade.php @@ -116,7 +116,7 @@ v-on:measuring="fetchImagesArea" v-on:requires-selected-label="handleRequiresSelectedLabel" v-on:is-invalid-shape="handleInvalidShape" - v-on:restore-lawnmower-image="restoreLawnmowerImage" + v-on:restore-lawnmower-image="loadImageWithId" ref="canvas" > From b27e065d53a9642ad4e169400583846d402ded83 Mon Sep 17 00:00:00 2001 From: yannik131 Date: Fri, 19 Jun 2026 18:18:37 +0200 Subject: [PATCH 19/34] TODO for removing watchers maybe --- .../assets/js/annotations/components/annotationCanvas/volare.js | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/assets/js/annotations/components/annotationCanvas/volare.js b/resources/assets/js/annotations/components/annotationCanvas/volare.js index 29d5b6601f..5a3b1584fe 100644 --- a/resources/assets/js/annotations/components/annotationCanvas/volare.js +++ b/resources/assets/js/annotations/components/annotationCanvas/volare.js @@ -111,6 +111,7 @@ export function useVolareMode({ return; } + // TODO Instead of this hack to trigger the watcher maybe use direct calls and don't use watchers in this composable focussedAnnotationIndex.value = null; nextTick(() => { focussedAnnotationIndex.value = savedState.focussedAnnotationIndex; From befcb4be5f35f9124f281e5eb44cd8ea69e8dffb Mon Sep 17 00:00:00 2001 From: yannik131 Date: Sat, 20 Jun 2026 14:15:20 +0200 Subject: [PATCH 20/34] Fixing volare pause/resume bugs --- .../js/annotations/annotatorContainer.vue | 38 ++----- .../components/annotationCanvas/volare.js | 104 ++++++++++++------ .../components/annotationModesTab.vue | 42 ++++++- resources/views/annotations/show.blade.php | 2 +- .../show/tabs/annotationModes.blade.php | 19 ++-- 5 files changed, 129 insertions(+), 76 deletions(-) diff --git a/resources/assets/js/annotations/annotatorContainer.vue b/resources/assets/js/annotations/annotatorContainer.vue index 41e265c312..129d254a3c 100644 --- a/resources/assets/js/annotations/annotatorContainer.vue +++ b/resources/assets/js/annotations/annotatorContainer.vue @@ -90,7 +90,6 @@ export default { draftAnnotationUsesLabelColor: true, imagesArea: null, openTab: null, - userUpdatedVolareResolution: false, userId: null, crossOriginError: false, maybeCorsTiffError: false, @@ -98,7 +97,8 @@ export default { labelTrees: [], projectIds: [], currentLawnmowerState: PlayPauseState.INACTIVE, - lawnmowerSaveState: "" + lawnmowerSaveState: "", + volare: null, }; }, provide() { @@ -137,9 +137,6 @@ export default { isDefaultAnnotationMode() { return this.annotationMode === 'default'; }, - isVolareAnnotationMode() { - return this.annotationMode === 'volare'; - }, isLawnmowerAnnotationMode() { return this.annotationMode === 'lawnmower'; }, @@ -281,7 +278,6 @@ export default { } }, maybeUpdateAnnotationMode(data) { - this.volare.updateFocussedAnnotation(); this.maybeUpdateShownImageSection(); this.maybeUpdateShownSampling(data); }, @@ -417,7 +413,7 @@ export default { confidence: 1, }; - if (this.isVolareAnnotationMode) { + if (this.volare.volareModeIsActive) { this.$refs.canvas.blinkAnnotation(annotation); } @@ -545,7 +541,7 @@ export default { setLawnmowerState(targetState) { const transition = `${this.currentLawnmowerState}->${targetState}`; - switch(transition) { + switch (transition) { case 'active->paused': this.lawnmowerSaveState = LawnmowerSaveState.SAVE; break; @@ -559,21 +555,13 @@ export default { this.currentLawnmowerState = targetState; }, - loadImageWithId(imageId) { + showImageWithId(imageId) { const index = this.imagesIds.indexOf(imageId); if (index !== -1) { this.imageIndex = index; } }, - updateLawnmowerState(oldMode, newMode) { - // Switching from lawnmower to default is caused by pausing, - // switching to a different mode inactivates lawnmower - if (oldMode === "lawnmower" && newMode !== 'default') { - this.setLawnmowerState(PlayPauseState.INACTIVE); - } - }, handleAnnotationModeChange(mode, data) { - this.updateLawnmowerState(this.annotationMode, mode); this.annotationMode = mode; this.annotationModeCarry = null; this.maybeUpdateAnnotationMode(data); @@ -697,11 +685,6 @@ export default { this.fetchImagesArea(); } }, - mapResolution() { - if (this.isVolareAnnotationMode) { - this.userUpdatedVolareResolution = true; - } - }, image(image) { this.crossOriginError = image?.crossOrigin; }, @@ -787,18 +770,17 @@ export default { Keyboard.on('C', this.selectLastAnnotation, 0, this.listenerSet); this.initLabelBot(); - }, - mounted() { + this.volare = useVolareMode({ filteredAnnotations: computed(() => this.filteredAnnotations), selectedAnnotations: computed(() => this.selectedAnnotations), focusAnnotationInCanvas: this.focusAnnotation, fitImageInCanvas: (...args) => this.$refs.canvas.fitImage(...args), - volareModeIsActive: computed(() => this.isVolareAnnotationMode), annotationFilter: this.annotationFilter, - image: computed(() => this.image) + image: computed(() => this.image), + mapResolution: computed(() => this.mapResolution), + showImageWithId: this.showImageWithId, }); - watch(this.volare.requestedImageId, this.loadImageWithId); - } + }, }; diff --git a/resources/assets/js/annotations/components/annotationCanvas/volare.js b/resources/assets/js/annotations/components/annotationCanvas/volare.js index 5a3b1584fe..97f6eb59ad 100644 --- a/resources/assets/js/annotations/components/annotationCanvas/volare.js +++ b/resources/assets/js/annotations/components/annotationCanvas/volare.js @@ -1,7 +1,8 @@ -import { ref, computed, watch, nextTick } from 'vue'; +import { ref, computed, watch, nextTick, readonly } from 'vue'; import { required } from '@/utils.js'; import { urlParams as UrlParams } from '@/core/utils.js'; import Events from '@/core/events.js'; +import { PlayPauseState } from '@/core/components/playPause.vue'; export function useVolareMode({ @@ -9,20 +10,25 @@ export function useVolareMode({ selectedAnnotations = required('selectedAnnotations'), focusAnnotationInCanvas = required('focusAnnotationInCanvas'), fitImageInCanvas = required('fitImageInCanvas'), - volareModeIsActive = required('volareModeIsActive'), annotationFilter = required('annotationFilter'), - image = required('image') + image = required('image'), + mapResolution = required('mapResolution'), + showImageWithId = required('showImageWithId'), }) { const focussedAnnotationIndex = ref(null); - const userUpdateVolareResolution = ref(false); - const requestedImageId = ref(null); - let pendingVolareState = null; - let savedState = null; + const resolutionWasChangedByUser = ref(false); + const state = ref(PlayPauseState.INACTIVE); + let resumeContext = null; + let resuming = false; const focussedAnnotation = computed(() => { return filteredAnnotations.value[focussedAnnotationIndex.value]; }); + const volareModeIsActive = computed(() => { + return state.value === PlayPauseState.ACTIVE; + }); + function selectAndFocusAnnotation(annotation, keepResolution = false) { selectedAnnotations.value.forEach(a => { a.selected = false; @@ -32,7 +38,9 @@ export function useVolareMode({ } function updateFocussedAnnotation() { - if (!volareModeIsActive.value) { + if (resumeContext) { + return; + } else if (!volareModeIsActive.value) { focussedAnnotationIndex.value = null; return; } else if (filteredAnnotations.value.length === 0) { @@ -66,8 +74,7 @@ export function useVolareMode({ // annotation of the image. focussedAnnotationIndex.value = Math.min(1, filteredAnnotations.value.length - 1); } else { - // Show the first annotation of the next image in this case, so - // don't return. + // Show the first annotation of the next image in this case focussedAnnotationIndex.value = -Infinity; } @@ -88,62 +95,93 @@ export function useVolareMode({ // last annotation of the image. focussedAnnotationIndex.value = Math.max(filteredAnnotations.value.length - 2, 0); } else { - // Show the last annotation of the previous image in this case, - // so don't return. + // Show the last annotation of the previous image in this case focussedAnnotationIndex.value = Infinity; } return false; } - function saveState() { - savedState = { + function setState(targetState) { + const transition = `${state.value}->${targetState}`; + + switch (transition) { + case 'active->paused': + pause(); + break; + case 'paused->active': + resume(); + break; + default: + resumeContext = null; + } + + state.value = targetState; + } + + function pause() { + resumeContext = { imageId: image.value.id, focussedAnnotationIndex: focussedAnnotationIndex.value, }; } - function loadState() { - if (!savedState) { + function resume() { + if (!resumeContext) { return; - } else if (savedState.imageId !== image.value.id) { - requestedImageId.value = savedState.imageId; + } + + resuming = true; + if (resumeContext.imageId !== image.value.id) { + showImageWithId(resumeContext.imageId); return; } - // TODO Instead of this hack to trigger the watcher maybe use direct calls and don't use watchers in this composable + const savedIndex = resumeContext.focussedAnnotationIndex; + resumeContext = null; + resuming = false; + focussedAnnotationIndex.value = null; nextTick(() => { - focussedAnnotationIndex.value = savedState.focussedAnnotationIndex; - savedState = null; - }); + // TODO Handle changes of filtered annotations while volare is active or paused + if (savedIndex !== null && savedIndex < filteredAnnotations.value.length) { + focussedAnnotationIndex.value = savedIndex; + } + }) } watch(focussedAnnotation, (annotation) => { - if (annotation) { - selectAndFocusAnnotation(annotation, userUpdateVolareResolution.value); + if (volareModeIsActive.value && annotation) { + selectAndFocusAnnotation(annotation, resolutionWasChangedByUser.value); } }); watch(() => annotationFilter?.value, updateFocussedAnnotation); watch(volareModeIsActive, (enabled) => { if (!enabled) { - userUpdateVolareResolution.value = false; + resolutionWasChangedByUser.value = false; + } else { + updateFocussedAnnotation(); } }); watch(() => image?.value, () => { - nextTick(loadState); + if (resuming) { + nextTick(resume); + } else if (volareModeIsActive.value) { + nextTick(updateFocussedAnnotation); + } + }); + watch(mapResolution, () => { + if (volareModeIsActive.value) { + resolutionWasChangedByUser.value = true; + } }); return { - focussedAnnotationIndex, - focussedAnnotation, - userUpdateVolareResolution, selectAndFocusAnnotation, - updateFocussedAnnotation, handleNextAnnotation, handlePreviousAnnotation, - requestedImageId, - saveState, - loadState, + setState, + volareModeIsActive, + state: readonly(state), }; } \ No newline at end of file diff --git a/resources/assets/js/annotations/components/annotationModesTab.vue b/resources/assets/js/annotations/components/annotationModesTab.vue index f6b95c187f..643ec6e52c 100644 --- a/resources/assets/js/annotations/components/annotationModesTab.vue +++ b/resources/assets/js/annotations/components/annotationModesTab.vue @@ -18,13 +18,18 @@ export default { type: String, required: true }, + currentVolareState: { + type: String, + required: true + }, }, template: '#annotation-modes-tab-template', emits: [ 'attach-label', 'change', 'create-sample', - 'lawnmowerStateRequested' + 'lawnmowerStateRequested', + 'volare-state-requested', ], components: { powerToggle: PowerToggle, @@ -77,13 +82,30 @@ export default { startRegularSampling() { this.setMode('regularSampling'); }, - setMode(mode) { - if (this.modes.indexOf(mode) !== -1) { - this.mode = mode; + deactivateResumableModes(oldMode, newMode) { + // Switching from lawnmower/volare to default is caused by pausing and does not deactivate the mode + if (newMode === 'default') { + return; + } + + if (oldMode === 'lawnmower' || this.currentLawnmowerState === PlayPauseState.PAUSED && newMode !== 'lawnmower') { + this.emitLawnmowerStateRequested(PlayPauseState.INACTIVE); + } else if (oldMode === 'volare' || this.currentVolareState === PlayPauseState.PAUSED && newMode !== 'volare') { + this.emitVolareStateRequested(PlayPauseState.INACTIVE); } }, + setMode(newMode) { + if (this.modes.indexOf(newMode) === -1) { + return; + } + + const oldMode = this.mode; + this.mode = newMode; + + this.deactivateResumableModes(oldMode, newMode); + }, resetMode() { - this.mode = 'default'; + this.setMode('default'); }, emitAttachLabel() { this.$emit('attach-label'); @@ -93,6 +115,9 @@ export default { }, emitLawnmowerStateRequested(targetState) { this.$emit('lawnmowerStateRequested', targetState); + }, + emitVolareStateRequested(targetState) { + this.$emit('volare-state-requested', targetState); } }, watch: { @@ -144,6 +169,13 @@ export default { } else if (newState === PlayPauseState.PAUSED) { this.resetMode(); } + }, + currentVolareState(newState) { + if (newState === PlayPauseState.ACTIVE) { + this.startVolare(); + } else if (newState === PlayPauseState.PAUSED) { + this.resetMode(); + } } }, created() { diff --git a/resources/views/annotations/show.blade.php b/resources/views/annotations/show.blade.php index 3ccd61c4bd..ce2b616f0c 100644 --- a/resources/views/annotations/show.blade.php +++ b/resources/views/annotations/show.blade.php @@ -116,7 +116,7 @@ v-on:measuring="fetchImagesArea" v-on:requires-selected-label="handleRequiresSelectedLabel" v-on:is-invalid-shape="handleInvalidShape" - v-on:restore-lawnmower-image="loadImageWithId" + v-on:restore-lawnmower-image="showImageWithId" ref="canvas" > diff --git a/resources/views/annotations/show/tabs/annotationModes.blade.php b/resources/views/annotations/show/tabs/annotationModes.blade.php index 6798ef6bcb..836f91d8fa 100644 --- a/resources/views/annotations/show/tabs/annotationModes.blade.php +++ b/resources/views/annotations/show/tabs/annotationModes.blade.php @@ -4,7 +4,9 @@ v-on:change="handleAnnotationModeChange" v-on:create-sample="createSampledAnnotation" v-on:lawnmower-state-requested="setLawnmowerState" - :current-lawnmower-state="currentLawnmowerState"> + v-on:volare-state-requested="volare.setState" + :current-lawnmower-state="currentLawnmowerState" + :current-volare-state="volare.state"> @push('scripts') @@ -12,14 +14,13 @@
diff --git a/resources/views/annotations/show/tabs/annotationModes.blade.php b/resources/views/annotations/show/tabs/annotationModes.blade.php index 90f3dad8e1..b1b0df3d59 100644 --- a/resources/views/annotations/show/tabs/annotationModes.blade.php +++ b/resources/views/annotations/show/tabs/annotationModes.blade.php @@ -18,6 +18,7 @@ :state="currentVolareState" @transitionRequested="updateVolareState" > + @can('add-annotation', $image) @endcan @@ -30,8 +31,9 @@ stopButtonTitleText="Stop cycling through image sections 𝗘𝘀𝗰" pauseButtonTitleText="Pause Lawnmower" :state="currentLawnmowerState" - @transitionRequested="updateLawnmowerState" + @transitionRequested="onLawnmowerPlayPauseTransitionRequested" > +
@can('add-annotation', $image) From a95884a643dfaec081f645b9e06f5f2b9b90e777 Mon Sep 17 00:00:00 2001 From: yannik131 Date: Sun, 23 Aug 2026 18:33:37 +0200 Subject: [PATCH 32/34] fixed volare mode check --- resources/assets/js/annotations/annotatorContainer.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/assets/js/annotations/annotatorContainer.vue b/resources/assets/js/annotations/annotatorContainer.vue index d6c7d4620d..3fe598a8ec 100644 --- a/resources/assets/js/annotations/annotatorContainer.vue +++ b/resources/assets/js/annotations/annotatorContainer.vue @@ -416,7 +416,7 @@ export default { confidence: 1, }; - if (this.volare.volareModeIsActive) { + if (this.annotationMode === 'volare') { this.$refs.canvas.blinkAnnotation(annotation); } From f4cad92cdc6c244d33b4c7567b50aa9dff431dcb Mon Sep 17 00:00:00 2001 From: yannik131 Date: Sun, 23 Aug 2026 18:49:54 +0200 Subject: [PATCH 33/34] Removed todo comment --- resources/assets/js/annotations/annotatorContainer.vue | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/resources/assets/js/annotations/annotatorContainer.vue b/resources/assets/js/annotations/annotatorContainer.vue index 3fe598a8ec..5b4b42f7fd 100644 --- a/resources/assets/js/annotations/annotatorContainer.vue +++ b/resources/assets/js/annotations/annotatorContainer.vue @@ -557,7 +557,7 @@ export default { pauseLawnmower(timestamp) { this.$refs.annotationModesTab.pauseLawnmower(timestamp); }, - pauseVolare(timestamp) { + pauseVolare() { this.$refs.annotationModesTab.pauseVolare(timestamp); }, showImageWithId(imageId) { @@ -749,8 +749,6 @@ export default { Events.on('annotations.delete', this.handleDeleteAnnotation); Events.on('annotations.focus', this.focusAnnotation); - // TODO Are these necessary? Should they be moved to the new volare composable? - if (UrlParams.get('annotation')) { let id = parseInt(UrlParams.get('annotation')); Events.once('images.change', () => { From 3bba13c85284511c31acdb9cba855551aafcac13 Mon Sep 17 00:00:00 2001 From: yannik131 Date: Sun, 23 Aug 2026 18:55:56 +0200 Subject: [PATCH 34/34] fixed timestamp typo --- resources/assets/js/annotations/annotatorContainer.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/assets/js/annotations/annotatorContainer.vue b/resources/assets/js/annotations/annotatorContainer.vue index 5b4b42f7fd..ade05d6de9 100644 --- a/resources/assets/js/annotations/annotatorContainer.vue +++ b/resources/assets/js/annotations/annotatorContainer.vue @@ -557,7 +557,7 @@ export default { pauseLawnmower(timestamp) { this.$refs.annotationModesTab.pauseLawnmower(timestamp); }, - pauseVolare() { + pauseVolare(timestamp) { this.$refs.annotationModesTab.pauseVolare(timestamp); }, showImageWithId(imageId) {