From 7bcb4c7adb67483b94fd5ad4a30f9da7ba8d933c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 1 Apr 2025 18:39:07 +0200 Subject: [PATCH 01/18] add more supported update actions for user bounding box updates --- .../annotation/UpdateActions.scala | 8 ++ .../tracings/NamedBoundingBox.scala | 2 +- .../updating/SkeletonUpdateActions.scala | 134 ++++++++++++++++++ .../tracings/volume/VolumeUpdateActions.scala | 134 ++++++++++++++++++ 4 files changed, 277 insertions(+), 1 deletion(-) diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/UpdateActions.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/UpdateActions.scala index f720e240aec..fe0873f863b 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/UpdateActions.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/UpdateActions.scala @@ -54,8 +54,16 @@ object UpdateAction { case "updateTreeEdgesVisibility" => deserialize[UpdateTreeEdgesVisibilitySkeletonAction](jsonValue) case "updateUserBoundingBoxesInSkeletonTracing" => deserialize[UpdateUserBoundingBoxesSkeletonAction](jsonValue) + case "addUserBoundingBoxSkeletonAction" => deserialize[AddUserBoundingBoxSkeletonAction](jsonValue) + case "deleteUserBoundingBoxSkeletonAction" => deserialize[DeleteUserBoundingBoxSkeletonAction](jsonValue) + case "updateUserBoundingBoxBoundsSkeletonAction" => + deserialize[UpdateUserBoundingBoxBoundsSkeletonAction](jsonValue) case "updateUserBoundingBoxVisibilityInSkeletonTracing" => deserialize[UpdateUserBoundingBoxVisibilitySkeletonAction](jsonValue) + case "updateUserBoundingBoxNameSkeletonAction" => + deserialize[UpdateUserBoundingBoxNameSkeletonAction](jsonValue) + case "updateUserBoundingBoxColorSkeletonAction" => + deserialize[UpdateUserBoundingBoxColorSkeletonAction](jsonValue) // Volume case "updateBucket" => deserialize[UpdateBucketVolumeAction](jsonValue) diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/NamedBoundingBox.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/NamedBoundingBox.scala index 526af171b67..cb8753a09fa 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/NamedBoundingBox.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/NamedBoundingBox.scala @@ -3,7 +3,7 @@ package com.scalableminds.webknossos.tracingstore.tracings import play.api.libs.json.{Json, OFormat} import com.scalableminds.util.geometry.BoundingBox import com.scalableminds.util.image.Color -import com.scalableminds.webknossos.datastore.geometry.{NamedBoundingBoxProto => ProtoBoundingBox} +import com.scalableminds.webknossos.datastore.geometry.{BoundingBoxProto, NamedBoundingBoxProto => ProtoBoundingBox} import com.scalableminds.webknossos.datastore.helpers.ProtoGeometryImplicits import com.scalableminds.webknossos.tracingstore.tracings.skeleton.updating.SkeletonUpdateActionHelper diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala index 07757abd896..f3e6f62c0a4 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala @@ -539,6 +539,68 @@ case class UpdateUserBoundingBoxesSkeletonAction(boundingBoxes: List[NamedBoundi this.copy(actionTracingId = newTracingId) } +case class AddUserBoundingBoxSkeletonAction(boundingBox: NamedBoundingBox, + actionTracingId: String, + actionTimestamp: Option[Long] = None, + actionAuthorId: Option[String] = None, + info: Option[String] = None) + extends SkeletonUpdateAction { + override def applyOn(tracing: SkeletonTracing): SkeletonTracing = + tracing.withUserBoundingBoxes(tracing.userBoundingBoxes :+ boundingBox.toProto) + + override def addTimestamp(timestamp: Long): UpdateAction = + this.copy(actionTimestamp = Some(timestamp)) + override def addInfo(info: Option[String]): UpdateAction = this.copy(info = info) + override def addAuthorId(authorId: Option[String]): UpdateAction = + this.copy(actionAuthorId = authorId) + override def withActionTracingId(newTracingId: String): LayerUpdateAction = + this.copy(actionTracingId = newTracingId) +} + +case class DeleteUserBoundingBoxSkeletonAction(boundingBoxId: Int, + actionTracingId: String, + actionTimestamp: Option[Long] = None, + actionAuthorId: Option[String] = None, + info: Option[String] = None) + extends SkeletonUpdateAction { + override def applyOn(tracing: SkeletonTracing): SkeletonTracing = + tracing.withUserBoundingBoxes(tracing.userBoundingBoxes.filter(_.id != boundingBoxId)) + + override def addTimestamp(timestamp: Long): UpdateAction = + this.copy(actionTimestamp = Some(timestamp)) + override def addInfo(info: Option[String]): UpdateAction = this.copy(info = info) + override def addAuthorId(authorId: Option[String]): UpdateAction = + this.copy(actionAuthorId = authorId) + override def withActionTracingId(newTracingId: String): LayerUpdateAction = + this.copy(actionTracingId = newTracingId) +} + +case class UpdateUserBoundingBoxBoundsSkeletonAction(boundingBox: NamedBoundingBox, + actionTracingId: String, + actionTimestamp: Option[Long] = None, + actionAuthorId: Option[String] = None, + info: Option[String] = None) + extends SkeletonUpdateAction { + override def applyOn(tracing: SkeletonTracing): SkeletonTracing = { + def updateUserBoundingBoxes() = + tracing.userBoundingBoxes.map { currentBoundingBox => + if (boundingBox.id == currentBoundingBox.id) + currentBoundingBox.copy(boundingBox = boundingBox.toProto.boundingBox) + else + currentBoundingBox + } + tracing.withUserBoundingBoxes(updateUserBoundingBoxes()) + } + + override def addTimestamp(timestamp: Long): UpdateAction = + this.copy(actionTimestamp = Some(timestamp)) + override def addInfo(info: Option[String]): UpdateAction = this.copy(info = info) + override def addAuthorId(authorId: Option[String]): UpdateAction = + this.copy(actionAuthorId = authorId) + override def withActionTracingId(newTracingId: String): LayerUpdateAction = + this.copy(actionTracingId = newTracingId) +} + case class UpdateUserBoundingBoxVisibilitySkeletonAction(boundingBoxId: Option[Int], isVisible: Boolean, actionTracingId: String, @@ -569,6 +631,58 @@ case class UpdateUserBoundingBoxVisibilitySkeletonAction(boundingBoxId: Option[I override def isViewOnlyChange: Boolean = true } +case class UpdateUserBoundingBoxNameSkeletonAction(boundingBox: NamedBoundingBox, + actionTracingId: String, + actionTimestamp: Option[Long] = None, + actionAuthorId: Option[String] = None, + info: Option[String] = None) + extends SkeletonUpdateAction { + override def applyOn(tracing: SkeletonTracing): SkeletonTracing = { + def updateUserBoundingBoxes() = + tracing.userBoundingBoxes.map { currentBoundingBox => + if (boundingBox.id == currentBoundingBox.id) + currentBoundingBox.copy(name = boundingBox.name) + else + currentBoundingBox + } + tracing.withUserBoundingBoxes(updateUserBoundingBoxes()) + } + + override def addTimestamp(timestamp: Long): UpdateAction = + this.copy(actionTimestamp = Some(timestamp)) + override def addInfo(info: Option[String]): UpdateAction = this.copy(info = info) + override def addAuthorId(authorId: Option[String]): UpdateAction = + this.copy(actionAuthorId = authorId) + override def withActionTracingId(newTracingId: String): LayerUpdateAction = + this.copy(actionTracingId = newTracingId) +} + +case class UpdateUserBoundingBoxColorSkeletonAction(boundingBox: NamedBoundingBox, + actionTracingId: String, + actionTimestamp: Option[Long] = None, + actionAuthorId: Option[String] = None, + info: Option[String] = None) + extends SkeletonUpdateAction { + override def applyOn(tracing: SkeletonTracing): SkeletonTracing = { + def updateUserBoundingBoxes() = + tracing.userBoundingBoxes.map { currentBoundingBox => + if (boundingBox.id == currentBoundingBox.id) + currentBoundingBox.copy(color = boundingBox.toProto.color) + else + currentBoundingBox + } + tracing.withUserBoundingBoxes(updateUserBoundingBoxes()) + } + + override def addTimestamp(timestamp: Long): UpdateAction = + this.copy(actionTimestamp = Some(timestamp)) + override def addInfo(info: Option[String]): UpdateAction = this.copy(info = info) + override def addAuthorId(authorId: Option[String]): UpdateAction = + this.copy(actionAuthorId = authorId) + override def withActionTracingId(newTracingId: String): LayerUpdateAction = + this.copy(actionTracingId = newTracingId) +} + object CreateTreeSkeletonAction { implicit val jsonFormat: OFormat[CreateTreeSkeletonAction] = Json.format[CreateTreeSkeletonAction] } @@ -620,7 +734,27 @@ object UpdateUserBoundingBoxesSkeletonAction { implicit val jsonFormat: OFormat[UpdateUserBoundingBoxesSkeletonAction] = Json.format[UpdateUserBoundingBoxesSkeletonAction] } +object AddUserBoundingBoxSkeletonAction { + implicit val jsonFormat: OFormat[AddUserBoundingBoxSkeletonAction] = + Json.format[AddUserBoundingBoxSkeletonAction] +} +object DeleteUserBoundingBoxSkeletonAction { + implicit val jsonFormat: OFormat[DeleteUserBoundingBoxSkeletonAction] = + Json.format[DeleteUserBoundingBoxSkeletonAction] +} +object UpdateUserBoundingBoxBoundsSkeletonAction { + implicit val jsonFormat: OFormat[UpdateUserBoundingBoxBoundsSkeletonAction] = + Json.format[UpdateUserBoundingBoxBoundsSkeletonAction] +} object UpdateUserBoundingBoxVisibilitySkeletonAction { implicit val jsonFormat: OFormat[UpdateUserBoundingBoxVisibilitySkeletonAction] = Json.format[UpdateUserBoundingBoxVisibilitySkeletonAction] } +object UpdateUserBoundingBoxNameSkeletonAction { + implicit val jsonFormat: OFormat[UpdateUserBoundingBoxNameSkeletonAction] = + Json.format[UpdateUserBoundingBoxNameSkeletonAction] +} +object UpdateUserBoundingBoxColorSkeletonAction { + implicit val jsonFormat: OFormat[UpdateUserBoundingBoxColorSkeletonAction] = + Json.format[UpdateUserBoundingBoxColorSkeletonAction] +} diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala index 9307df81d1c..68156d2d773 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala @@ -106,6 +106,68 @@ case class UpdateUserBoundingBoxesVolumeAction(boundingBoxes: List[NamedBounding tracing.withUserBoundingBoxes(boundingBoxes.map(_.toProto)) } +case class AddUserBoundingBoxSkeletonAction(boundingBox: NamedBoundingBox, + actionTracingId: String, + actionTimestamp: Option[Long] = None, + actionAuthorId: Option[String] = None, + info: Option[String] = None) + extends ApplyableVolumeUpdateAction { + override def applyOn(tracing: VolumeTracing): VolumeTracing = + tracing.withUserBoundingBoxes(tracing.userBoundingBoxes :+ boundingBox.toProto) + + override def addTimestamp(timestamp: Long): UpdateAction = + this.copy(actionTimestamp = Some(timestamp)) + override def addInfo(info: Option[String]): UpdateAction = this.copy(info = info) + override def addAuthorId(authorId: Option[String]): UpdateAction = + this.copy(actionAuthorId = authorId) + override def withActionTracingId(newTracingId: String): LayerUpdateAction = + this.copy(actionTracingId = newTracingId) +} + +case class DeleteUserBoundingBoxSkeletonAction(boundingBoxId: Int, + actionTracingId: String, + actionTimestamp: Option[Long] = None, + actionAuthorId: Option[String] = None, + info: Option[String] = None) + extends ApplyableVolumeUpdateAction { + override def applyOn(tracing: VolumeTracing): VolumeTracing = + tracing.withUserBoundingBoxes(tracing.userBoundingBoxes.filter(_.id != boundingBoxId)) + + override def addTimestamp(timestamp: Long): UpdateAction = + this.copy(actionTimestamp = Some(timestamp)) + override def addInfo(info: Option[String]): UpdateAction = this.copy(info = info) + override def addAuthorId(authorId: Option[String]): UpdateAction = + this.copy(actionAuthorId = authorId) + override def withActionTracingId(newTracingId: String): LayerUpdateAction = + this.copy(actionTracingId = newTracingId) +} + +case class UpdateUserBoundingBoxBoundsSkeletonAction(boundingBox: NamedBoundingBox, + actionTracingId: String, + actionTimestamp: Option[Long] = None, + actionAuthorId: Option[String] = None, + info: Option[String] = None) + extends ApplyableVolumeUpdateAction { + override def applyOn(tracing: VolumeTracing): VolumeTracing = { + def updateUserBoundingBoxes() = + tracing.userBoundingBoxes.map { currentBoundingBox => + if (boundingBox.id == currentBoundingBox.id) + currentBoundingBox.copy(boundingBox = boundingBox.toProto.boundingBox) + else + currentBoundingBox + } + tracing.withUserBoundingBoxes(updateUserBoundingBoxes()) + } + + override def addTimestamp(timestamp: Long): UpdateAction = + this.copy(actionTimestamp = Some(timestamp)) + override def addInfo(info: Option[String]): UpdateAction = this.copy(info = info) + override def addAuthorId(authorId: Option[String]): UpdateAction = + this.copy(actionAuthorId = authorId) + override def withActionTracingId(newTracingId: String): LayerUpdateAction = + this.copy(actionTracingId = newTracingId) +} + case class UpdateUserBoundingBoxVisibilityVolumeAction(boundingBoxId: Option[Int], isVisible: Boolean, actionTracingId: String, @@ -136,6 +198,58 @@ case class UpdateUserBoundingBoxVisibilityVolumeAction(boundingBoxId: Option[Int override def isViewOnlyChange: Boolean = true } +case class UpdateUserBoundingBoxNameSkeletonAction(boundingBox: NamedBoundingBox, + actionTracingId: String, + actionTimestamp: Option[Long] = None, + actionAuthorId: Option[String] = None, + info: Option[String] = None) + extends ApplyableVolumeUpdateAction { + override def applyOn(tracing: VolumeTracing): VolumeTracing = { + def updateUserBoundingBoxes() = + tracing.userBoundingBoxes.map { currentBoundingBox => + if (boundingBox.id == currentBoundingBox.id) + currentBoundingBox.copy(name = boundingBox.name) + else + currentBoundingBox + } + tracing.withUserBoundingBoxes(updateUserBoundingBoxes()) + } + + override def addTimestamp(timestamp: Long): UpdateAction = + this.copy(actionTimestamp = Some(timestamp)) + override def addInfo(info: Option[String]): UpdateAction = this.copy(info = info) + override def addAuthorId(authorId: Option[String]): UpdateAction = + this.copy(actionAuthorId = authorId) + override def withActionTracingId(newTracingId: String): LayerUpdateAction = + this.copy(actionTracingId = newTracingId) +} + +case class UpdateUserBoundingBoxColorSkeletonAction(boundingBox: NamedBoundingBox, + actionTracingId: String, + actionTimestamp: Option[Long] = None, + actionAuthorId: Option[String] = None, + info: Option[String] = None) + extends ApplyableVolumeUpdateAction { + override def applyOn(tracing: VolumeTracing): VolumeTracing = { + def updateUserBoundingBoxes() = + tracing.userBoundingBoxes.map { currentBoundingBox => + if (boundingBox.id == currentBoundingBox.id) + currentBoundingBox.copy(color = boundingBox.toProto.color) + else + currentBoundingBox + } + tracing.withUserBoundingBoxes(updateUserBoundingBoxes()) + } + + override def addTimestamp(timestamp: Long): UpdateAction = + this.copy(actionTimestamp = Some(timestamp)) + override def addInfo(info: Option[String]): UpdateAction = this.copy(info = info) + override def addAuthorId(authorId: Option[String]): UpdateAction = + this.copy(actionAuthorId = authorId) + override def withActionTracingId(newTracingId: String): LayerUpdateAction = + this.copy(actionTracingId = newTracingId) +} + case class RemoveFallbackLayerVolumeAction(actionTracingId: String, actionTimestamp: Option[Long] = None, actionAuthorId: Option[String] = None, @@ -389,10 +503,30 @@ object UpdateUserBoundingBoxesVolumeAction { implicit val jsonFormat: OFormat[UpdateUserBoundingBoxesVolumeAction] = Json.format[UpdateUserBoundingBoxesVolumeAction] } +object AddUserBoundingBoxSkeletonAction { + implicit val jsonFormat: OFormat[AddUserBoundingBoxSkeletonAction] = + Json.format[AddUserBoundingBoxSkeletonAction] +} +object DeleteUserBoundingBoxSkeletonAction { + implicit val jsonFormat: OFormat[DeleteUserBoundingBoxSkeletonAction] = + Json.format[DeleteUserBoundingBoxSkeletonAction] +} +object UpdateUserBoundingBoxBoundsSkeletonAction { + implicit val jsonFormat: OFormat[UpdateUserBoundingBoxBoundsSkeletonAction] = + Json.format[UpdateUserBoundingBoxBoundsSkeletonAction] +} object UpdateUserBoundingBoxVisibilityVolumeAction { implicit val jsonFormat: OFormat[UpdateUserBoundingBoxVisibilityVolumeAction] = Json.format[UpdateUserBoundingBoxVisibilityVolumeAction] } +object UpdateUserBoundingBoxNameSkeletonAction { + implicit val jsonFormat: OFormat[UpdateUserBoundingBoxNameSkeletonAction] = + Json.format[UpdateUserBoundingBoxNameSkeletonAction] +} +object UpdateUserBoundingBoxColorSkeletonAction { + implicit val jsonFormat: OFormat[UpdateUserBoundingBoxColorSkeletonAction] = + Json.format[UpdateUserBoundingBoxColorSkeletonAction] +} object RemoveFallbackLayerVolumeAction { implicit val jsonFormat: OFormat[RemoveFallbackLayerVolumeAction] = Json.format[RemoveFallbackLayerVolumeAction] } From 09de1567955fcbfbdd5d613097ab0ddb527b38c2 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Thu, 3 Apr 2025 18:57:11 +0200 Subject: [PATCH 02/18] [frontend] add new updateActions to add, remove and update individual bounding boxes --- .../oxalis/model/sagas/update_actions.ts | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/frontend/javascripts/oxalis/model/sagas/update_actions.ts b/frontend/javascripts/oxalis/model/sagas/update_actions.ts index 7d6d351466d..d29101a39ee 100644 --- a/frontend/javascripts/oxalis/model/sagas/update_actions.ts +++ b/frontend/javascripts/oxalis/model/sagas/update_actions.ts @@ -377,6 +377,129 @@ export function updateVolumeTracing( }, } as const; } + +export function addUserBoundingBoxSkeletonAction( + boundingBox: UserBoundingBox, + actionTracingId: string, + timestamp: number | null, + authorId: string | null, + info: string | null, +) { + return { + name: "addUserBoundingBoxSkeleton", + value: { + boundingBox: convertUserBoundingBoxesFromFrontendToServer([boundingBox]), + actionTracingId, + actionTimestamp: timestamp, + actionAuthorId: authorId, + info, + }, + } as const; +} + +export function addUserBoundingBoxInVolumeTracingAction( + boundingBox: UserBoundingBox, + actionTracingId: string, + timestamp: number | null, + authorId: string | null, + info: string | null, +) { + return { + name: "addUserBoundingBoxSkeleton", + value: { + boundingBox: convertUserBoundingBoxesFromFrontendToServer([boundingBox]), + actionTracingId, + actionTimestamp: timestamp, + actionAuthorId: authorId, + info, + }, + } as const; +} + +export function deleteUserBoundingBoxInSkeletonTracingAction( + boundingBoxId: number, + actionTracingId: string, + timestamp: number | null, + authorId: string | null, + info: string | null, +) { + return { + name: "deleteUserBoundingBoxSkeletonAction", + value: { + boundingBoxId, + actionTracingId, + actionTimestamp: timestamp, + actionAuthorId: authorId, + info, + }, + } as const; +} + +export function deleteUserBoundingBoxInVolumeTracingAction( + boundingBoxId: number, + actionTracingId: string, + timestamp: number | null, + authorId: string | null, + info: string | null, +) { + return { + name: "deleteUserBoundingBoxVolumeAction", + value: { + boundingBoxId, + actionTracingId, + actionTimestamp: timestamp, + actionAuthorId: authorId, + info, + }, + } as const; +} + +export function updateUserBoundingBoxInSkeletonTracingAction( + boundingBoxId: number, + updatedProps: Partial, + actionTracingId: string, + timestamp: number | null, + authorId: string | null, + info: string | null, +) { + const updatedPropKeys = Object.keys(updatedProps); + return { + name: "updateUserBoundingBoxSkeletonAction", + value: { + boundingBoxId, + actionTracingId, + updatedProps, + updatedPropKeys, + actionTimestamp: timestamp, + actionAuthorId: authorId, + info, + }, + } as const; +} + +export function updateUserBoundingBoxInVolumeTracingAction( + boundingBoxId: number, + updatedProps: Partial, + actionTracingId: string, + timestamp: number | null, + authorId: string | null, + info: string | null, +) { + const updatedPropKeys = Object.keys(updatedProps); + return { + name: "updateUserBoundingBoxVolumeAction", + value: { + boundingBoxId, + actionTracingId, + updatedProps, + updatedPropKeys, + actionTimestamp: timestamp, + actionAuthorId: authorId, + info, + }, + } as const; +} + export function updateUserBoundingBoxesInSkeletonTracing( userBoundingBoxes: Array, actionTracingId: string, From 7a187fd1e1dd24d7dd62a6052dc8a41d3048e195 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Thu, 3 Apr 2025 20:45:15 +0200 Subject: [PATCH 03/18] WIP: adjust saga code to use new update actions and fixing typing in some places --- .../oxalis/model/sagas/save_saga.ts | 7 ++- .../model/sagas/skeletontracing_saga.ts | 54 ++++++++++++++++-- .../oxalis/model/sagas/update_actions.ts | 26 ++++++++- .../oxalis/model/sagas/volumetracing_saga.tsx | 56 +++++++++++++++++-- .../javascripts/oxalis/view/version_entry.tsx | 24 ++++++++ .../test/sagas/compact_toggle_actions.spec.ts | 1 + .../test/sagas/skeletontracing_saga.spec.ts | 1 + 7 files changed, 157 insertions(+), 12 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.ts b/frontend/javascripts/oxalis/model/sagas/save_saga.ts index f0b306608e5..f8918a1f499 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.ts @@ -350,18 +350,19 @@ export function performDiffTracing( flycam: Flycam, prevTdCamera: CameraData, tdCamera: CameraData, + activeUserId: string | null, ): Array { let actions: Array = []; if (prevTracing.type === "skeleton" && tracing.type === "skeleton") { actions = actions.concat( - Array.from(diffSkeletonTracing(prevTracing, tracing, prevFlycam, flycam)), + Array.from(diffSkeletonTracing(prevTracing, tracing, prevFlycam, flycam, activeUserId)), ); } if (prevTracing.type === "volume" && tracing.type === "volume") { actions = actions.concat( - Array.from(diffVolumeTracing(prevTracing, tracing, prevFlycam, flycam)), + Array.from(diffVolumeTracing(prevTracing, tracing, prevFlycam, flycam, activeUserId)), ); } @@ -394,6 +395,7 @@ export function* setupSavingForTracingType( | SkeletonTracing; let prevFlycam = yield* select((state) => state.flycam); let prevTdCamera = yield* select((state) => state.viewModeData.plane.tdCamera); + const activeUserId = yield* select((state) => state.activeUser?.id || null); yield* call(ensureWkReady); while (true) { @@ -434,6 +436,7 @@ export function* setupSavingForTracingType( flycam, prevTdCamera, tdCamera, + activeUserId, ), ), tracing, diff --git a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts index c9013bb8093..da71568d5da 100644 --- a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts @@ -48,19 +48,21 @@ import type { Saga } from "oxalis/model/sagas/effect-generators"; import { select } from "oxalis/model/sagas/effect-generators"; import type { UpdateActionWithoutIsolationRequirement } from "oxalis/model/sagas/update_actions"; import { + addUserBoundingBoxSkeletonAction, createEdge, createNode, createTree, deleteEdge, deleteNode, deleteTree, + deleteUserBoundingBoxInSkeletonTracingAction, updateNode, updateSkeletonTracing, updateTree, updateTreeEdgesVisibility, updateTreeGroups, updateTreeVisibility, - updateUserBoundingBoxesInSkeletonTracing, + updateUserBoundingBoxInSkeletonTracingAction, } from "oxalis/model/sagas/update_actions"; import { api } from "oxalis/singletons"; import type { @@ -612,6 +614,7 @@ export function* diffSkeletonTracing( skeletonTracing: SkeletonTracing, prevFlycam: Flycam, flycam: Flycam, + activeUserId: string | null, ): Generator { if (prevSkeletonTracing !== skeletonTracing) { for (const action of cachedDiffTrees( @@ -638,10 +641,53 @@ export function* diffSkeletonTracing( } if (!_.isEqual(prevSkeletonTracing.userBoundingBoxes, skeletonTracing.userBoundingBoxes)) { - yield updateUserBoundingBoxesInSkeletonTracing( - skeletonTracing.userBoundingBoxes, - skeletonTracing.tracingId, + const { + onlyA: deletedBBoxIds, + onlyB: addedBBoxIds, + both: changedBBoxIds, + } = Utils.diffArrays( + _.map(prevSkeletonTracing.userBoundingBoxes, (bbox) => bbox.id), + _.map(skeletonTracing.userBoundingBoxes, (bbox) => bbox.id), ); + for (const id of deletedBBoxIds) { + yield deleteUserBoundingBoxInSkeletonTracingAction( + id, + skeletonTracing.tracingId, + Date.now(), + activeUserId, + null, + ); + } + for (const id of addedBBoxIds) { + const bbox = skeletonTracing.userBoundingBoxes.find((bbox) => bbox.id === id); + if (bbox) { + yield addUserBoundingBoxSkeletonAction( + bbox, + skeletonTracing.tracingId, + Date.now(), + activeUserId, + null, + ); + } else { + Toast.error(`User bounding box with id ${id} not found in skeleton tracing.`); + } + } + for (const id of changedBBoxIds) { + const bbox = skeletonTracing.userBoundingBoxes.find((bbox) => bbox.id === id); + if (bbox) { + // TODO_charlie only update changed props + yield updateUserBoundingBoxInSkeletonTracingAction( + bbox.id, + bbox, + skeletonTracing.tracingId, + Date.now(), + activeUserId, + null, + ); + } else { + Toast.error(`User bounding box with id ${id} not found in skeleton tracing.`); + } + } } } export default [ diff --git a/frontend/javascripts/oxalis/model/sagas/update_actions.ts b/frontend/javascripts/oxalis/model/sagas/update_actions.ts index d29101a39ee..2a0e70394ff 100644 --- a/frontend/javascripts/oxalis/model/sagas/update_actions.ts +++ b/frontend/javascripts/oxalis/model/sagas/update_actions.ts @@ -38,6 +38,22 @@ export type CreateSegmentUpdateAction = ReturnType; export type DeleteSegmentUpdateAction = ReturnType; export type DeleteSegmentDataUpdateAction = ReturnType; +export type AddUserBoundingBoxSkeletonAction = ReturnType; +export type AddUserBoundingBoxInVolumeTracingAction = ReturnType< + typeof addUserBoundingBoxInVolumeTracingAction +>; +export type DeleteUserBoundingBoxInSkeletonTracingAction = ReturnType< + typeof deleteUserBoundingBoxInSkeletonTracingAction +>; +export type DeleteUserBoundingBoxInVolumeTracingAction = ReturnType< + typeof deleteUserBoundingBoxInVolumeTracingAction +>; +export type UpdateUserBoundingBoxInSkeletonTracingAction = ReturnType< + typeof updateUserBoundingBoxInSkeletonTracingAction +>; +export type UpdateUserBoundingBoxInVolumeTracingAction = ReturnType< + typeof updateUserBoundingBoxInVolumeTracingAction +>; type UpdateUserBoundingBoxesInSkeletonTracingUpdateAction = ReturnType< typeof updateUserBoundingBoxesInSkeletonTracing >; @@ -83,6 +99,12 @@ export type UpdateActionWithoutIsolationRequirement = | DeleteEdgeUpdateAction | UpdateSkeletonTracingUpdateAction | UpdateVolumeTracingUpdateAction + | AddUserBoundingBoxSkeletonAction + | AddUserBoundingBoxInVolumeTracingAction + | DeleteUserBoundingBoxInSkeletonTracingAction + | DeleteUserBoundingBoxInVolumeTracingAction + | UpdateUserBoundingBoxInSkeletonTracingAction + | UpdateUserBoundingBoxInVolumeTracingAction | UpdateUserBoundingBoxesInSkeletonTracingUpdateAction | UpdateUserBoundingBoxesInVolumeTracingUpdateAction | CreateSegmentUpdateAction @@ -386,7 +408,7 @@ export function addUserBoundingBoxSkeletonAction( info: string | null, ) { return { - name: "addUserBoundingBoxSkeleton", + name: "addUserBoundingBoxSkeletonAction", value: { boundingBox: convertUserBoundingBoxesFromFrontendToServer([boundingBox]), actionTracingId, @@ -405,7 +427,7 @@ export function addUserBoundingBoxInVolumeTracingAction( info: string | null, ) { return { - name: "addUserBoundingBoxSkeleton", + name: "addUserBoundingBoxVolumeAction", value: { boundingBox: convertUserBoundingBoxesFromFrontendToServer([boundingBox]), actionTracingId, diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx index 8a379725061..302e8ef1e6e 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx @@ -74,14 +74,16 @@ import { } from "oxalis/model/sagas/saga_helpers"; import { type UpdateActionWithoutIsolationRequirement, + addUserBoundingBoxInVolumeTracingAction, createSegmentVolumeAction, deleteSegmentDataVolumeAction, deleteSegmentVolumeAction, + deleteUserBoundingBoxInVolumeTracingAction, removeFallbackLayer, updateMappingName, updateSegmentGroups, updateSegmentVolumeAction, - updateUserBoundingBoxesInVolumeTracing, + updateUserBoundingBoxInVolumeTracingAction, updateVolumeTracing, } from "oxalis/model/sagas/update_actions"; import type VolumeLayer from "oxalis/model/volumetracing/volumelayer"; @@ -95,6 +97,8 @@ import { floodFill } from "./volume/floodfill_saga"; import { type BooleanBox, createVolumeLayer, labelWithVoxelBuffer2D } from "./volume/helpers"; import maybeInterpolateSegmentationLayer from "./volume/volume_interpolation_saga"; +import * as Utils from "libs/utils"; + const OVERWRITE_EMPTY_WARNING_KEY = "OVERWRITE-EMPTY-WARNING"; export function* watchVolumeTracingAsync(): Saga { @@ -472,6 +476,7 @@ export function* diffVolumeTracing( volumeTracing: VolumeTracing, prevFlycam: Flycam, flycam: Flycam, + activeUserId: string | null, ): Generator { if (updateTracingPredicate(prevVolumeTracing, volumeTracing, prevFlycam, flycam)) { yield updateVolumeTracing( @@ -484,10 +489,53 @@ export function* diffVolumeTracing( } if (!_.isEqual(prevVolumeTracing.userBoundingBoxes, volumeTracing.userBoundingBoxes)) { - yield updateUserBoundingBoxesInVolumeTracing( - volumeTracing.userBoundingBoxes, - volumeTracing.tracingId, + const { + onlyA: deletedBBoxIds, + onlyB: addedBBoxIds, + both: changedBBoxIds, + } = Utils.diffArrays( + _.map(prevVolumeTracing.userBoundingBoxes, (bbox) => bbox.id), + _.map(volumeTracing.userBoundingBoxes, (bbox) => bbox.id), ); + for (const id of deletedBBoxIds) { + yield deleteUserBoundingBoxInVolumeTracingAction( + id, + volumeTracing.tracingId, + Date.now(), + activeUserId, + null, + ); + } + for (const id of addedBBoxIds) { + const bbox = volumeTracing.userBoundingBoxes.find((bbox) => bbox.id === id); + if (bbox) { + yield addUserBoundingBoxInVolumeTracingAction( + bbox, + volumeTracing.tracingId, + Date.now(), + activeUserId, + null, + ); + } else { + Toast.error(`User bounding box with id ${id} not found in volume tracing.`); + } + } + for (const id of changedBBoxIds) { + const bbox = volumeTracing.userBoundingBoxes.find((bbox) => bbox.id === id); + if (bbox) { + // TODO_charlie only update changed props + yield updateUserBoundingBoxInVolumeTracingAction( + bbox.id, + bbox, + volumeTracing.tracingId, + Date.now(), + activeUserId, + null, + ); + } else { + Toast.error(`User bounding box with id ${id} not found in volume tracing.`); + } + } } if (prevVolumeTracing !== volumeTracing) { diff --git a/frontend/javascripts/oxalis/view/version_entry.tsx b/frontend/javascripts/oxalis/view/version_entry.tsx index 7a7b92c0042..e763db4450f 100644 --- a/frontend/javascripts/oxalis/view/version_entry.tsx +++ b/frontend/javascripts/oxalis/view/version_entry.tsx @@ -84,6 +84,30 @@ const descriptionFns: Record< description: "Updated a bounding box.", icon: , }), + addUserBoundingBoxSkeletonAction: (): Description => ({ + description: "Added a bounding box.", + icon: , + }), + addUserBoundingBoxVolumeAction: (): Description => ({ + description: "Added a bounding box.", + icon: , + }), + deleteUserBoundingBoxSkeletonAction: (): Description => ({ + description: "Deleted a bounding box.", + icon: , + }), + deleteUserBoundingBoxVolumeAction: (): Description => ({ + description: "Deleted a bounding box.", + icon: , + }), + updateUserBoundingBoxSkeletonAction: (): Description => ({ + description: "Updated a bounding box.", + icon: , + }), + updateUserBoundingBoxVolumeAction: (): Description => ({ + description: "Updated a bounding box.", + icon: , + }), removeFallbackLayer: (): Description => ({ description: "Removed the segmentation fallback layer.", icon: , diff --git a/frontend/javascripts/test/sagas/compact_toggle_actions.spec.ts b/frontend/javascripts/test/sagas/compact_toggle_actions.spec.ts index f4f17661d20..6614a2e948f 100644 --- a/frontend/javascripts/test/sagas/compact_toggle_actions.spec.ts +++ b/frontend/javascripts/test/sagas/compact_toggle_actions.spec.ts @@ -107,6 +107,7 @@ function testDiffing(prevState: OxalisState, nextState: OxalisState) { enforceSkeletonTracing(nextState.tracing), flycamMock, flycamMock, + nextState.activeUser?.id || null, ), ), ), diff --git a/frontend/javascripts/test/sagas/skeletontracing_saga.spec.ts b/frontend/javascripts/test/sagas/skeletontracing_saga.spec.ts index e55b0919f93..81429127a0b 100644 --- a/frontend/javascripts/test/sagas/skeletontracing_saga.spec.ts +++ b/frontend/javascripts/test/sagas/skeletontracing_saga.spec.ts @@ -63,6 +63,7 @@ function testDiffing( enforceSkeletonTracing(nextTracing), prevFlycam, flycam, + null, ), ), ); From cdf546f69b94d4485b695cac3619dd0515e7d340 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Fri, 4 Apr 2025 11:21:11 +0200 Subject: [PATCH 04/18] [frontend] remove activeUser, timestamp and info from updateActions --- .../oxalis/model/sagas/save_saga.ts | 7 ++-- .../model/sagas/skeletontracing_saga.ts | 20 ++--------- .../oxalis/model/sagas/update_actions.ts | 36 ------------------- .../oxalis/model/sagas/volumetracing_saga.tsx | 26 ++------------ 4 files changed, 7 insertions(+), 82 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.ts b/frontend/javascripts/oxalis/model/sagas/save_saga.ts index f8918a1f499..f0b306608e5 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.ts @@ -350,19 +350,18 @@ export function performDiffTracing( flycam: Flycam, prevTdCamera: CameraData, tdCamera: CameraData, - activeUserId: string | null, ): Array { let actions: Array = []; if (prevTracing.type === "skeleton" && tracing.type === "skeleton") { actions = actions.concat( - Array.from(diffSkeletonTracing(prevTracing, tracing, prevFlycam, flycam, activeUserId)), + Array.from(diffSkeletonTracing(prevTracing, tracing, prevFlycam, flycam)), ); } if (prevTracing.type === "volume" && tracing.type === "volume") { actions = actions.concat( - Array.from(diffVolumeTracing(prevTracing, tracing, prevFlycam, flycam, activeUserId)), + Array.from(diffVolumeTracing(prevTracing, tracing, prevFlycam, flycam)), ); } @@ -395,7 +394,6 @@ export function* setupSavingForTracingType( | SkeletonTracing; let prevFlycam = yield* select((state) => state.flycam); let prevTdCamera = yield* select((state) => state.viewModeData.plane.tdCamera); - const activeUserId = yield* select((state) => state.activeUser?.id || null); yield* call(ensureWkReady); while (true) { @@ -436,7 +434,6 @@ export function* setupSavingForTracingType( flycam, prevTdCamera, tdCamera, - activeUserId, ), ), tracing, diff --git a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts index da71568d5da..1f379f3e784 100644 --- a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts @@ -614,7 +614,6 @@ export function* diffSkeletonTracing( skeletonTracing: SkeletonTracing, prevFlycam: Flycam, flycam: Flycam, - activeUserId: string | null, ): Generator { if (prevSkeletonTracing !== skeletonTracing) { for (const action of cachedDiffTrees( @@ -650,24 +649,12 @@ export function* diffSkeletonTracing( _.map(skeletonTracing.userBoundingBoxes, (bbox) => bbox.id), ); for (const id of deletedBBoxIds) { - yield deleteUserBoundingBoxInSkeletonTracingAction( - id, - skeletonTracing.tracingId, - Date.now(), - activeUserId, - null, - ); + yield deleteUserBoundingBoxInSkeletonTracingAction(id, skeletonTracing.tracingId); } for (const id of addedBBoxIds) { const bbox = skeletonTracing.userBoundingBoxes.find((bbox) => bbox.id === id); if (bbox) { - yield addUserBoundingBoxSkeletonAction( - bbox, - skeletonTracing.tracingId, - Date.now(), - activeUserId, - null, - ); + yield addUserBoundingBoxSkeletonAction(bbox, skeletonTracing.tracingId); } else { Toast.error(`User bounding box with id ${id} not found in skeleton tracing.`); } @@ -680,9 +667,6 @@ export function* diffSkeletonTracing( bbox.id, bbox, skeletonTracing.tracingId, - Date.now(), - activeUserId, - null, ); } else { Toast.error(`User bounding box with id ${id} not found in skeleton tracing.`); diff --git a/frontend/javascripts/oxalis/model/sagas/update_actions.ts b/frontend/javascripts/oxalis/model/sagas/update_actions.ts index 2a0e70394ff..7ebe8107349 100644 --- a/frontend/javascripts/oxalis/model/sagas/update_actions.ts +++ b/frontend/javascripts/oxalis/model/sagas/update_actions.ts @@ -403,18 +403,12 @@ export function updateVolumeTracing( export function addUserBoundingBoxSkeletonAction( boundingBox: UserBoundingBox, actionTracingId: string, - timestamp: number | null, - authorId: string | null, - info: string | null, ) { return { name: "addUserBoundingBoxSkeletonAction", value: { boundingBox: convertUserBoundingBoxesFromFrontendToServer([boundingBox]), actionTracingId, - actionTimestamp: timestamp, - actionAuthorId: authorId, - info, }, } as const; } @@ -422,18 +416,12 @@ export function addUserBoundingBoxSkeletonAction( export function addUserBoundingBoxInVolumeTracingAction( boundingBox: UserBoundingBox, actionTracingId: string, - timestamp: number | null, - authorId: string | null, - info: string | null, ) { return { name: "addUserBoundingBoxVolumeAction", value: { boundingBox: convertUserBoundingBoxesFromFrontendToServer([boundingBox]), actionTracingId, - actionTimestamp: timestamp, - actionAuthorId: authorId, - info, }, } as const; } @@ -441,18 +429,12 @@ export function addUserBoundingBoxInVolumeTracingAction( export function deleteUserBoundingBoxInSkeletonTracingAction( boundingBoxId: number, actionTracingId: string, - timestamp: number | null, - authorId: string | null, - info: string | null, ) { return { name: "deleteUserBoundingBoxSkeletonAction", value: { boundingBoxId, actionTracingId, - actionTimestamp: timestamp, - actionAuthorId: authorId, - info, }, } as const; } @@ -460,18 +442,12 @@ export function deleteUserBoundingBoxInSkeletonTracingAction( export function deleteUserBoundingBoxInVolumeTracingAction( boundingBoxId: number, actionTracingId: string, - timestamp: number | null, - authorId: string | null, - info: string | null, ) { return { name: "deleteUserBoundingBoxVolumeAction", value: { boundingBoxId, actionTracingId, - actionTimestamp: timestamp, - actionAuthorId: authorId, - info, }, } as const; } @@ -480,9 +456,6 @@ export function updateUserBoundingBoxInSkeletonTracingAction( boundingBoxId: number, updatedProps: Partial, actionTracingId: string, - timestamp: number | null, - authorId: string | null, - info: string | null, ) { const updatedPropKeys = Object.keys(updatedProps); return { @@ -492,9 +465,6 @@ export function updateUserBoundingBoxInSkeletonTracingAction( actionTracingId, updatedProps, updatedPropKeys, - actionTimestamp: timestamp, - actionAuthorId: authorId, - info, }, } as const; } @@ -503,9 +473,6 @@ export function updateUserBoundingBoxInVolumeTracingAction( boundingBoxId: number, updatedProps: Partial, actionTracingId: string, - timestamp: number | null, - authorId: string | null, - info: string | null, ) { const updatedPropKeys = Object.keys(updatedProps); return { @@ -515,9 +482,6 @@ export function updateUserBoundingBoxInVolumeTracingAction( actionTracingId, updatedProps, updatedPropKeys, - actionTimestamp: timestamp, - actionAuthorId: authorId, - info, }, } as const; } diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx index 302e8ef1e6e..f5c36f64495 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx @@ -476,7 +476,6 @@ export function* diffVolumeTracing( volumeTracing: VolumeTracing, prevFlycam: Flycam, flycam: Flycam, - activeUserId: string | null, ): Generator { if (updateTracingPredicate(prevVolumeTracing, volumeTracing, prevFlycam, flycam)) { yield updateVolumeTracing( @@ -498,24 +497,12 @@ export function* diffVolumeTracing( _.map(volumeTracing.userBoundingBoxes, (bbox) => bbox.id), ); for (const id of deletedBBoxIds) { - yield deleteUserBoundingBoxInVolumeTracingAction( - id, - volumeTracing.tracingId, - Date.now(), - activeUserId, - null, - ); + yield deleteUserBoundingBoxInVolumeTracingAction(id, volumeTracing.tracingId); } for (const id of addedBBoxIds) { const bbox = volumeTracing.userBoundingBoxes.find((bbox) => bbox.id === id); if (bbox) { - yield addUserBoundingBoxInVolumeTracingAction( - bbox, - volumeTracing.tracingId, - Date.now(), - activeUserId, - null, - ); + yield addUserBoundingBoxInVolumeTracingAction(bbox, volumeTracing.tracingId); } else { Toast.error(`User bounding box with id ${id} not found in volume tracing.`); } @@ -524,14 +511,7 @@ export function* diffVolumeTracing( const bbox = volumeTracing.userBoundingBoxes.find((bbox) => bbox.id === id); if (bbox) { // TODO_charlie only update changed props - yield updateUserBoundingBoxInVolumeTracingAction( - bbox.id, - bbox, - volumeTracing.tracingId, - Date.now(), - activeUserId, - null, - ); + yield updateUserBoundingBoxInVolumeTracingAction(bbox.id, bbox, volumeTracing.tracingId); } else { Toast.error(`User bounding box with id ${id} not found in volume tracing.`); } From bfb8746af6cf224b1702b01285c48714e5fa734e Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Fri, 4 Apr 2025 13:56:19 +0200 Subject: [PATCH 05/18] [frontend] make saga code more DRY and only update changed props --- .../model/sagas/skeletontracing_saga.ts | 101 +++++++++++------- .../oxalis/model/sagas/update_actions.ts | 6 +- .../oxalis/model/sagas/volumetracing_saga.tsx | 44 ++------ 3 files changed, 77 insertions(+), 74 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts index 1f379f3e784..561910de9a7 100644 --- a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts @@ -48,7 +48,8 @@ import type { Saga } from "oxalis/model/sagas/effect-generators"; import { select } from "oxalis/model/sagas/effect-generators"; import type { UpdateActionWithoutIsolationRequirement } from "oxalis/model/sagas/update_actions"; import { - addUserBoundingBoxSkeletonAction, + addUserBoundingBoxInSkeletonTracingAction, + addUserBoundingBoxInVolumeTracingAction, createEdge, createNode, createTree, @@ -56,6 +57,7 @@ import { deleteNode, deleteTree, deleteUserBoundingBoxInSkeletonTracingAction, + deleteUserBoundingBoxInVolumeTracingAction, updateNode, updateSkeletonTracing, updateTree, @@ -63,6 +65,7 @@ import { updateTreeGroups, updateTreeVisibility, updateUserBoundingBoxInSkeletonTracingAction, + updateUserBoundingBoxInVolumeTracingAction, } from "oxalis/model/sagas/update_actions"; import { api } from "oxalis/singletons"; import type { @@ -73,6 +76,7 @@ import type { SkeletonTracing, Tree, TreeMap, + UserBoundingBox, } from "oxalis/store"; import Store from "oxalis/store"; import { @@ -86,7 +90,7 @@ import { takeEvery, throttle, } from "typed-redux-saga"; -import type { ServerSkeletonTracing } from "types/api_flow_types"; +import { AnnotationLayerEnum, type ServerSkeletonTracing } from "types/api_flow_types"; import { ensureWkReady } from "./ready_sagas"; import { takeWithBatchActionSupport } from "./saga_helpers"; @@ -609,6 +613,59 @@ export const cachedDiffTrees = memoizeOne((tracingId: string, prevTrees: TreeMap Array.from(diffTrees(tracingId, prevTrees, trees)), ); +export function* diffBoundingBoxes( + prevBoundingBoxes: UserBoundingBox[], + currentBoundingBoxes: UserBoundingBox[], + tracingId: string, + tracingType: AnnotationLayerEnum, +) { + const { + onlyA: deletedBBoxIds, + onlyB: addedBBoxIds, + both: changedBBoxIds, + } = Utils.diffArrays( + _.map(prevBoundingBoxes, (bbox) => bbox.id), + _.map(currentBoundingBoxes, (bbox) => bbox.id), + ); + const addBBoxAction = + tracingType === AnnotationLayerEnum.Skeleton + ? addUserBoundingBoxInSkeletonTracingAction + : addUserBoundingBoxInVolumeTracingAction; + const deleteBBoxAction = + tracingType === AnnotationLayerEnum.Skeleton + ? deleteUserBoundingBoxInSkeletonTracingAction + : deleteUserBoundingBoxInVolumeTracingAction; + const updateBBoxAction = + tracingType === AnnotationLayerEnum.Skeleton + ? updateUserBoundingBoxInSkeletonTracingAction + : updateUserBoundingBoxInVolumeTracingAction; + const getErrorMessage = (id: number) => + `User bounding box with id ${id} not found in ${tracingType} tracing.`; + for (const id of deletedBBoxIds) { + yield deleteBBoxAction(id, tracingId); + } + for (const id of addedBBoxIds) { + const bbox = currentBoundingBoxes.find((bbox) => bbox.id === id); + if (bbox) { + yield addBBoxAction(bbox, tracingId); + } else { + Toast.error(getErrorMessage(id)); + } + } + for (const id of changedBBoxIds) { + const currentBbox = currentBoundingBoxes.find((bbox) => bbox.id === id); + const prevBbox = prevBoundingBoxes.find((bbox) => bbox.id === id); + if (currentBbox == null || prevBbox == null) { + Toast.error(getErrorMessage(id)); + continue; + } + const diffBBox = Utils.diffObjects(currentBbox, prevBbox); + //TODO_C remove + console.log("diffBBox", diffBBox); + yield updateBBoxAction(currentBbox.id, diffBBox, tracingId); + } +} + export function* diffSkeletonTracing( prevSkeletonTracing: SkeletonTracing, skeletonTracing: SkeletonTracing, @@ -639,40 +696,12 @@ export function* diffSkeletonTracing( ); } - if (!_.isEqual(prevSkeletonTracing.userBoundingBoxes, skeletonTracing.userBoundingBoxes)) { - const { - onlyA: deletedBBoxIds, - onlyB: addedBBoxIds, - both: changedBBoxIds, - } = Utils.diffArrays( - _.map(prevSkeletonTracing.userBoundingBoxes, (bbox) => bbox.id), - _.map(skeletonTracing.userBoundingBoxes, (bbox) => bbox.id), - ); - for (const id of deletedBBoxIds) { - yield deleteUserBoundingBoxInSkeletonTracingAction(id, skeletonTracing.tracingId); - } - for (const id of addedBBoxIds) { - const bbox = skeletonTracing.userBoundingBoxes.find((bbox) => bbox.id === id); - if (bbox) { - yield addUserBoundingBoxSkeletonAction(bbox, skeletonTracing.tracingId); - } else { - Toast.error(`User bounding box with id ${id} not found in skeleton tracing.`); - } - } - for (const id of changedBBoxIds) { - const bbox = skeletonTracing.userBoundingBoxes.find((bbox) => bbox.id === id); - if (bbox) { - // TODO_charlie only update changed props - yield updateUserBoundingBoxInSkeletonTracingAction( - bbox.id, - bbox, - skeletonTracing.tracingId, - ); - } else { - Toast.error(`User bounding box with id ${id} not found in skeleton tracing.`); - } - } - } + diffBoundingBoxes( + skeletonTracing.userBoundingBoxes, + prevSkeletonTracing.userBoundingBoxes, + skeletonTracing.tracingId, + AnnotationLayerEnum.Skeleton, + ); } export default [ watchSkeletonTracingAsync, diff --git a/frontend/javascripts/oxalis/model/sagas/update_actions.ts b/frontend/javascripts/oxalis/model/sagas/update_actions.ts index 7ebe8107349..a46d7f335a8 100644 --- a/frontend/javascripts/oxalis/model/sagas/update_actions.ts +++ b/frontend/javascripts/oxalis/model/sagas/update_actions.ts @@ -38,7 +38,9 @@ export type CreateSegmentUpdateAction = ReturnType; export type DeleteSegmentUpdateAction = ReturnType; export type DeleteSegmentDataUpdateAction = ReturnType; -export type AddUserBoundingBoxSkeletonAction = ReturnType; +export type AddUserBoundingBoxSkeletonAction = ReturnType< + typeof addUserBoundingBoxInSkeletonTracingAction +>; export type AddUserBoundingBoxInVolumeTracingAction = ReturnType< typeof addUserBoundingBoxInVolumeTracingAction >; @@ -400,7 +402,7 @@ export function updateVolumeTracing( } as const; } -export function addUserBoundingBoxSkeletonAction( +export function addUserBoundingBoxInSkeletonTracingAction( boundingBox: UserBoundingBox, actionTracingId: string, ) { diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx index f5c36f64495..531af825025 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx @@ -1,7 +1,6 @@ import { diffDiffableMaps } from "libs/diffable_map"; import { V3 } from "libs/mjs"; import Toast from "libs/toast"; -import _ from "lodash"; import memoizeOne from "memoize-one"; import type { AnnotationTool, @@ -74,16 +73,13 @@ import { } from "oxalis/model/sagas/saga_helpers"; import { type UpdateActionWithoutIsolationRequirement, - addUserBoundingBoxInVolumeTracingAction, createSegmentVolumeAction, deleteSegmentDataVolumeAction, deleteSegmentVolumeAction, - deleteUserBoundingBoxInVolumeTracingAction, removeFallbackLayer, updateMappingName, updateSegmentGroups, updateSegmentVolumeAction, - updateUserBoundingBoxInVolumeTracingAction, updateVolumeTracing, } from "oxalis/model/sagas/update_actions"; import type VolumeLayer from "oxalis/model/volumetracing/volumelayer"; @@ -91,14 +87,14 @@ import { Model, api } from "oxalis/singletons"; import type { Flycam, SegmentMap, VolumeTracing } from "oxalis/store"; import type { ActionPattern } from "redux-saga/effects"; import { actionChannel, call, fork, put, takeEvery, takeLatest } from "typed-redux-saga"; +import { AnnotationLayerEnum } from "types/api_flow_types"; import { pushSaveQueueTransaction } from "../actions/save_actions"; import { ensureWkReady } from "./ready_sagas"; +import { diffBoundingBoxes } from "./skeletontracing_saga"; import { floodFill } from "./volume/floodfill_saga"; import { type BooleanBox, createVolumeLayer, labelWithVoxelBuffer2D } from "./volume/helpers"; import maybeInterpolateSegmentationLayer from "./volume/volume_interpolation_saga"; -import * as Utils from "libs/utils"; - const OVERWRITE_EMPTY_WARNING_KEY = "OVERWRITE-EMPTY-WARNING"; export function* watchVolumeTracingAsync(): Saga { @@ -487,36 +483,12 @@ export function* diffVolumeTracing( ); } - if (!_.isEqual(prevVolumeTracing.userBoundingBoxes, volumeTracing.userBoundingBoxes)) { - const { - onlyA: deletedBBoxIds, - onlyB: addedBBoxIds, - both: changedBBoxIds, - } = Utils.diffArrays( - _.map(prevVolumeTracing.userBoundingBoxes, (bbox) => bbox.id), - _.map(volumeTracing.userBoundingBoxes, (bbox) => bbox.id), - ); - for (const id of deletedBBoxIds) { - yield deleteUserBoundingBoxInVolumeTracingAction(id, volumeTracing.tracingId); - } - for (const id of addedBBoxIds) { - const bbox = volumeTracing.userBoundingBoxes.find((bbox) => bbox.id === id); - if (bbox) { - yield addUserBoundingBoxInVolumeTracingAction(bbox, volumeTracing.tracingId); - } else { - Toast.error(`User bounding box with id ${id} not found in volume tracing.`); - } - } - for (const id of changedBBoxIds) { - const bbox = volumeTracing.userBoundingBoxes.find((bbox) => bbox.id === id); - if (bbox) { - // TODO_charlie only update changed props - yield updateUserBoundingBoxInVolumeTracingAction(bbox.id, bbox, volumeTracing.tracingId); - } else { - Toast.error(`User bounding box with id ${id} not found in volume tracing.`); - } - } - } + diffBoundingBoxes( + prevVolumeTracing.userBoundingBoxes, + volumeTracing.userBoundingBoxes, + volumeTracing.tracingId, + AnnotationLayerEnum.Volume, + ); if (prevVolumeTracing !== volumeTracing) { if (prevVolumeTracing.segments !== volumeTracing.segments) { From 78d8d1850d86c4bfd47f9058c70a47553f12586f Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Fri, 4 Apr 2025 15:07:26 +0200 Subject: [PATCH 06/18] [frontend] correctly call helper function --- .../javascripts/oxalis/model/sagas/skeletontracing_saga.ts | 4 ++-- .../javascripts/oxalis/model/sagas/volumetracing_saga.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts index 561910de9a7..f94075122b6 100644 --- a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts @@ -696,9 +696,9 @@ export function* diffSkeletonTracing( ); } - diffBoundingBoxes( - skeletonTracing.userBoundingBoxes, + yield* diffBoundingBoxes( prevSkeletonTracing.userBoundingBoxes, + skeletonTracing.userBoundingBoxes, skeletonTracing.tracingId, AnnotationLayerEnum.Skeleton, ); diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx index 531af825025..1c02ea9554c 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx @@ -483,7 +483,7 @@ export function* diffVolumeTracing( ); } - diffBoundingBoxes( + yield* diffBoundingBoxes( prevVolumeTracing.userBoundingBoxes, volumeTracing.userBoundingBoxes, volumeTracing.tracingId, From c680cf285877a430c17ee20c8cd8eef452597b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 8 Apr 2025 18:44:44 +0200 Subject: [PATCH 07/18] have single update action for all user bounding box properties --- .../annotation/UpdateActions.scala | 42 +++-- .../tracings/NamedBoundingBox.scala | 19 +- .../updating/SkeletonUpdateActions.scala | 133 +++----------- .../tracings/volume/VolumeUpdateActions.scala | 167 +++++------------- 4 files changed, 116 insertions(+), 245 deletions(-) diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/UpdateActions.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/UpdateActions.scala index fe0873f863b..a790185c066 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/UpdateActions.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/UpdateActions.scala @@ -5,7 +5,7 @@ import com.scalableminds.webknossos.tracingstore.tracings.editablemapping.{ SplitAgglomerateUpdateAction } import com.scalableminds.webknossos.tracingstore.tracings.skeleton.updating._ -import com.scalableminds.webknossos.tracingstore.tracings.volume._ +import com.scalableminds.webknossos.tracingstore.tracings.volume.{UpdateUserBoundingBoxVolumeAction, _} import play.api.libs.json._ trait UpdateAction { @@ -56,22 +56,18 @@ object UpdateAction { deserialize[UpdateUserBoundingBoxesSkeletonAction](jsonValue) case "addUserBoundingBoxSkeletonAction" => deserialize[AddUserBoundingBoxSkeletonAction](jsonValue) case "deleteUserBoundingBoxSkeletonAction" => deserialize[DeleteUserBoundingBoxSkeletonAction](jsonValue) - case "updateUserBoundingBoxBoundsSkeletonAction" => - deserialize[UpdateUserBoundingBoxBoundsSkeletonAction](jsonValue) - case "updateUserBoundingBoxVisibilityInSkeletonTracing" => - deserialize[UpdateUserBoundingBoxVisibilitySkeletonAction](jsonValue) - case "updateUserBoundingBoxNameSkeletonAction" => - deserialize[UpdateUserBoundingBoxNameSkeletonAction](jsonValue) - case "updateUserBoundingBoxColorSkeletonAction" => - deserialize[UpdateUserBoundingBoxColorSkeletonAction](jsonValue) + case "updateUserBoundingBoxSkeletonAction" => + deserialize[UpdateUserBoundingBoxSkeletonAction](jsonValue) // Volume case "updateBucket" => deserialize[UpdateBucketVolumeAction](jsonValue) case "updateVolumeTracing" => deserialize[UpdateTracingVolumeAction](jsonValue) + case "updateUserBoundingBoxVolumeAction" => + deserialize[UpdateUserBoundingBoxVolumeAction](jsonValue) + case "addUserBoundingBoxVolumeAction" => deserialize[AddUserBoundingBoxVolumeAction](jsonValue) + case "deleteUserBoundingBoxVolumeAction" => deserialize[DeleteUserBoundingBoxVolumeAction](jsonValue) case "updateUserBoundingBoxesInVolumeTracing" => deserialize[UpdateUserBoundingBoxesVolumeAction](jsonValue) - case "updateUserBoundingBoxVisibilityInVolumeTracing" => - deserialize[UpdateUserBoundingBoxVisibilityVolumeAction](jsonValue) case "removeFallbackLayer" => deserialize[RemoveFallbackLayerVolumeAction](jsonValue) case "importVolumeTracing" => deserialize[ImportVolumeDataVolumeAction](jsonValue) case "createSegment" => deserialize[CreateSegmentVolumeAction](jsonValue) @@ -148,9 +144,15 @@ object UpdateAction { case s: UpdateUserBoundingBoxesSkeletonAction => Json.obj("name" -> "updateUserBoundingBoxesInSkeletonTracing", "value" -> Json.toJson(s)(UpdateUserBoundingBoxesSkeletonAction.jsonFormat)) - case s: UpdateUserBoundingBoxVisibilitySkeletonAction => - Json.obj("name" -> "updateUserBoundingBoxVisibilityInSkeletonTracing", - "value" -> Json.toJson(s)(UpdateUserBoundingBoxVisibilitySkeletonAction.jsonFormat)) + case s: UpdateUserBoundingBoxSkeletonAction => + Json.obj("name" -> "updateUserBoundingBoxSkeletonAction", + "value" -> Json.toJson(s)(UpdateUserBoundingBoxSkeletonAction.jsonFormat)) + case s: AddUserBoundingBoxSkeletonAction => + Json.obj("name" -> "addUserBoundingBoxSkeletonAction", + "value" -> Json.toJson(s)(AddUserBoundingBoxSkeletonAction.jsonFormat)) + case s: DeleteUserBoundingBoxSkeletonAction => + Json.obj("name" -> "deleteUserBoundingBoxSkeletonAction", + "value" -> Json.toJson(s)(DeleteUserBoundingBoxSkeletonAction.jsonFormat)) // Volume case s: UpdateBucketVolumeAction => @@ -160,9 +162,15 @@ object UpdateAction { case s: UpdateUserBoundingBoxesVolumeAction => Json.obj("name" -> "updateUserBoundingBoxesInVolumeTracing", "value" -> Json.toJson(s)(UpdateUserBoundingBoxesVolumeAction.jsonFormat)) - case s: UpdateUserBoundingBoxVisibilityVolumeAction => - Json.obj("name" -> "updateUserBoundingBoxVisibilityInVolumeTracing", - "value" -> Json.toJson(s)(UpdateUserBoundingBoxVisibilityVolumeAction.jsonFormat)) + case s: UpdateUserBoundingBoxVolumeAction => + Json.obj("name" -> "updateUserBoundingBoxVolumeAction", + "value" -> Json.toJson(s)(UpdateUserBoundingBoxVolumeAction.jsonFormat)) + case s: AddUserBoundingBoxVolumeAction => + Json.obj("name" -> "addUserBoundingBoxVolumeAction", + "value" -> Json.toJson(s)(AddUserBoundingBoxVolumeAction.jsonFormat)) + case s: DeleteUserBoundingBoxVolumeAction => + Json.obj("name" -> "deleteUserBoundingBoxVolumeAction", + "value" -> Json.toJson(s)(DeleteUserBoundingBoxVolumeAction.jsonFormat)) case s: RemoveFallbackLayerVolumeAction => Json.obj("name" -> "removeFallbackLayer", "value" -> Json.toJson(s)(RemoveFallbackLayerVolumeAction.jsonFormat)) case s: ImportVolumeDataVolumeAction => diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/NamedBoundingBox.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/NamedBoundingBox.scala index cb8753a09fa..79a114d6ffc 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/NamedBoundingBox.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/NamedBoundingBox.scala @@ -3,7 +3,11 @@ package com.scalableminds.webknossos.tracingstore.tracings import play.api.libs.json.{Json, OFormat} import com.scalableminds.util.geometry.BoundingBox import com.scalableminds.util.image.Color -import com.scalableminds.webknossos.datastore.geometry.{BoundingBoxProto, NamedBoundingBoxProto => ProtoBoundingBox} +import com.scalableminds.webknossos.datastore.geometry.{ + BoundingBoxProto, + ColorProto, + NamedBoundingBoxProto => ProtoBoundingBox +} import com.scalableminds.webknossos.datastore.helpers.ProtoGeometryImplicits import com.scalableminds.webknossos.tracingstore.tracings.skeleton.updating.SkeletonUpdateActionHelper @@ -18,3 +22,16 @@ case class NamedBoundingBox(id: Int, } object NamedBoundingBox { implicit val jsonFormat: OFormat[NamedBoundingBox] = Json.format[NamedBoundingBox] } + +case class NamedBoundingBoxUpdate(id: Option[Int], + name: Option[String], + isVisible: Option[Boolean], + color: Option[Color], + boundingBox: Option[BoundingBox], +) extends ProtoGeometryImplicits { + def colorOptProto: Option[ColorProto] = colorOptToProto(color) + def boundingBoxProto: Option[BoundingBoxProto] = boundingBoxOptToProto(boundingBox) +} +object NamedBoundingBoxUpdate { + implicit val jsonFormat: OFormat[NamedBoundingBoxUpdate] = Json.format[NamedBoundingBoxUpdate] +} diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala index f3e6f62c0a4..39b2313ae85 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala @@ -575,100 +575,35 @@ case class DeleteUserBoundingBoxSkeletonAction(boundingBoxId: Int, this.copy(actionTracingId = newTracingId) } -case class UpdateUserBoundingBoxBoundsSkeletonAction(boundingBox: NamedBoundingBox, - actionTracingId: String, - actionTimestamp: Option[Long] = None, - actionAuthorId: Option[String] = None, - info: Option[String] = None) - extends SkeletonUpdateAction { - override def applyOn(tracing: SkeletonTracing): SkeletonTracing = { - def updateUserBoundingBoxes() = - tracing.userBoundingBoxes.map { currentBoundingBox => - if (boundingBox.id == currentBoundingBox.id) - currentBoundingBox.copy(boundingBox = boundingBox.toProto.boundingBox) - else - currentBoundingBox - } - tracing.withUserBoundingBoxes(updateUserBoundingBoxes()) - } - - override def addTimestamp(timestamp: Long): UpdateAction = - this.copy(actionTimestamp = Some(timestamp)) - override def addInfo(info: Option[String]): UpdateAction = this.copy(info = info) - override def addAuthorId(authorId: Option[String]): UpdateAction = - this.copy(actionAuthorId = authorId) - override def withActionTracingId(newTracingId: String): LayerUpdateAction = - this.copy(actionTracingId = newTracingId) -} - -case class UpdateUserBoundingBoxVisibilitySkeletonAction(boundingBoxId: Option[Int], - isVisible: Boolean, - actionTracingId: String, - actionTimestamp: Option[Long] = None, - actionAuthorId: Option[String] = None, - info: Option[String] = None) - extends SkeletonUpdateAction { - override def applyOn(tracing: SkeletonTracing): SkeletonTracing = { - def updateUserBoundingBoxes() = - tracing.userBoundingBoxes.map { boundingBox => - if (boundingBoxId.forall(_ == boundingBox.id)) - boundingBox.copy(isVisible = Some(isVisible)) - else - boundingBox - } - - tracing.withUserBoundingBoxes(updateUserBoundingBoxes()) - } - - override def addTimestamp(timestamp: Long): UpdateAction = - this.copy(actionTimestamp = Some(timestamp)) - override def addInfo(info: Option[String]): UpdateAction = this.copy(info = info) - override def addAuthorId(authorId: Option[String]): UpdateAction = - this.copy(actionAuthorId = authorId) - override def withActionTracingId(newTracingId: String): LayerUpdateAction = - this.copy(actionTracingId = newTracingId) - - override def isViewOnlyChange: Boolean = true -} - -case class UpdateUserBoundingBoxNameSkeletonAction(boundingBox: NamedBoundingBox, - actionTracingId: String, - actionTimestamp: Option[Long] = None, - actionAuthorId: Option[String] = None, - info: Option[String] = None) - extends SkeletonUpdateAction { - override def applyOn(tracing: SkeletonTracing): SkeletonTracing = { - def updateUserBoundingBoxes() = - tracing.userBoundingBoxes.map { currentBoundingBox => - if (boundingBox.id == currentBoundingBox.id) - currentBoundingBox.copy(name = boundingBox.name) - else - currentBoundingBox - } - tracing.withUserBoundingBoxes(updateUserBoundingBoxes()) - } - - override def addTimestamp(timestamp: Long): UpdateAction = - this.copy(actionTimestamp = Some(timestamp)) - override def addInfo(info: Option[String]): UpdateAction = this.copy(info = info) - override def addAuthorId(authorId: Option[String]): UpdateAction = - this.copy(actionAuthorId = authorId) - override def withActionTracingId(newTracingId: String): LayerUpdateAction = - this.copy(actionTracingId = newTracingId) -} - -case class UpdateUserBoundingBoxColorSkeletonAction(boundingBox: NamedBoundingBox, - actionTracingId: String, - actionTimestamp: Option[Long] = None, - actionAuthorId: Option[String] = None, - info: Option[String] = None) +case class UpdateUserBoundingBoxSkeletonAction(boundingBoxId: Int, + updatedPropKeys: List[String], + updatedProps: NamedBoundingBoxUpdate, + actionTracingId: String, + actionTimestamp: Option[Long] = None, + actionAuthorId: Option[String] = None, + info: Option[String] = None) extends SkeletonUpdateAction { override def applyOn(tracing: SkeletonTracing): SkeletonTracing = { def updateUserBoundingBoxes() = tracing.userBoundingBoxes.map { currentBoundingBox => - if (boundingBox.id == currentBoundingBox.id) - currentBoundingBox.copy(color = boundingBox.toProto.color) - else + if (boundingBoxId == currentBoundingBox.id) { + currentBoundingBox.copy( + id = updatedProps.id.getOrElse(currentBoundingBox.id), + name = + if (updatedPropKeys.contains("name") && updatedProps.name.isDefined) updatedProps.name + else currentBoundingBox.name, + isVisible = + if (updatedPropKeys.contains("isVisible") && updatedProps.isVisible.isDefined) updatedProps.isVisible + else currentBoundingBox.isVisible, + color = + if (updatedPropKeys.contains("color") && updatedProps.color.isDefined) updatedProps.colorOptProto + else currentBoundingBox.color, + boundingBox = + if (updatedPropKeys.contains("boundingBox")) + updatedProps.boundingBoxProto.getOrElse(currentBoundingBox.boundingBox) + else currentBoundingBox.boundingBox + ) + } else currentBoundingBox } tracing.withUserBoundingBoxes(updateUserBoundingBoxes()) @@ -742,19 +677,7 @@ object DeleteUserBoundingBoxSkeletonAction { implicit val jsonFormat: OFormat[DeleteUserBoundingBoxSkeletonAction] = Json.format[DeleteUserBoundingBoxSkeletonAction] } -object UpdateUserBoundingBoxBoundsSkeletonAction { - implicit val jsonFormat: OFormat[UpdateUserBoundingBoxBoundsSkeletonAction] = - Json.format[UpdateUserBoundingBoxBoundsSkeletonAction] -} -object UpdateUserBoundingBoxVisibilitySkeletonAction { - implicit val jsonFormat: OFormat[UpdateUserBoundingBoxVisibilitySkeletonAction] = - Json.format[UpdateUserBoundingBoxVisibilitySkeletonAction] -} -object UpdateUserBoundingBoxNameSkeletonAction { - implicit val jsonFormat: OFormat[UpdateUserBoundingBoxNameSkeletonAction] = - Json.format[UpdateUserBoundingBoxNameSkeletonAction] -} -object UpdateUserBoundingBoxColorSkeletonAction { - implicit val jsonFormat: OFormat[UpdateUserBoundingBoxColorSkeletonAction] = - Json.format[UpdateUserBoundingBoxColorSkeletonAction] +object UpdateUserBoundingBoxSkeletonAction { + implicit val jsonFormat: OFormat[UpdateUserBoundingBoxSkeletonAction] = + Json.format[UpdateUserBoundingBoxSkeletonAction] } diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala index f1d07ca87c4..fe51021407f 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala @@ -6,7 +6,7 @@ import com.scalableminds.webknossos.datastore.geometry.NamedBoundingBoxProto import com.scalableminds.webknossos.datastore.helpers.ProtoGeometryImplicits import com.scalableminds.webknossos.datastore.models.{AdditionalCoordinate, BucketPosition} import com.scalableminds.webknossos.tracingstore.annotation.{LayerUpdateAction, UpdateAction} -import com.scalableminds.webknossos.tracingstore.tracings.{MetadataEntry, NamedBoundingBox} +import com.scalableminds.webknossos.tracingstore.tracings.{MetadataEntry, NamedBoundingBox, NamedBoundingBoxUpdate} import play.api.libs.json._ trait VolumeUpdateActionHelper { @@ -117,11 +117,11 @@ case class UpdateUserBoundingBoxesVolumeAction(boundingBoxes: List[NamedBounding tracing.withUserBoundingBoxes(boundingBoxes.map(_.toProto)) } -case class AddUserBoundingBoxSkeletonAction(boundingBox: NamedBoundingBox, - actionTracingId: String, - actionTimestamp: Option[Long] = None, - actionAuthorId: Option[String] = None, - info: Option[String] = None) +case class AddUserBoundingBoxVolumeAction(boundingBox: NamedBoundingBox, + actionTracingId: String, + actionTimestamp: Option[Long] = None, + actionAuthorId: Option[String] = None, + info: Option[String] = None) extends ApplyableVolumeUpdateAction { override def applyOn(tracing: VolumeTracing): VolumeTracing = tracing.withUserBoundingBoxes(tracing.userBoundingBoxes :+ boundingBox.toProto) @@ -135,11 +135,11 @@ case class AddUserBoundingBoxSkeletonAction(boundingBox: NamedBoundingBox, this.copy(actionTracingId = newTracingId) } -case class DeleteUserBoundingBoxSkeletonAction(boundingBoxId: Int, - actionTracingId: String, - actionTimestamp: Option[Long] = None, - actionAuthorId: Option[String] = None, - info: Option[String] = None) +case class DeleteUserBoundingBoxVolumeAction(boundingBoxId: Int, + actionTracingId: String, + actionTimestamp: Option[Long] = None, + actionAuthorId: Option[String] = None, + info: Option[String] = None) extends ApplyableVolumeUpdateAction { override def applyOn(tracing: VolumeTracing): VolumeTracing = tracing.withUserBoundingBoxes(tracing.userBoundingBoxes.filter(_.id != boundingBoxId)) @@ -153,100 +153,35 @@ case class DeleteUserBoundingBoxSkeletonAction(boundingBoxId: Int, this.copy(actionTracingId = newTracingId) } -case class UpdateUserBoundingBoxBoundsSkeletonAction(boundingBox: NamedBoundingBox, - actionTracingId: String, - actionTimestamp: Option[Long] = None, - actionAuthorId: Option[String] = None, - info: Option[String] = None) +case class UpdateUserBoundingBoxVolumeAction(boundingBoxId: Int, + updatedPropKeys: List[String], + updatedProps: NamedBoundingBoxUpdate, + actionTracingId: String, + actionTimestamp: Option[Long] = None, + actionAuthorId: Option[String] = None, + info: Option[String] = None) extends ApplyableVolumeUpdateAction { override def applyOn(tracing: VolumeTracing): VolumeTracing = { def updateUserBoundingBoxes() = tracing.userBoundingBoxes.map { currentBoundingBox => - if (boundingBox.id == currentBoundingBox.id) - currentBoundingBox.copy(boundingBox = boundingBox.toProto.boundingBox) - else - currentBoundingBox - } - tracing.withUserBoundingBoxes(updateUserBoundingBoxes()) - } - - override def addTimestamp(timestamp: Long): UpdateAction = - this.copy(actionTimestamp = Some(timestamp)) - override def addInfo(info: Option[String]): UpdateAction = this.copy(info = info) - override def addAuthorId(authorId: Option[String]): UpdateAction = - this.copy(actionAuthorId = authorId) - override def withActionTracingId(newTracingId: String): LayerUpdateAction = - this.copy(actionTracingId = newTracingId) -} - -case class UpdateUserBoundingBoxVisibilityVolumeAction(boundingBoxId: Option[Int], - isVisible: Boolean, - actionTracingId: String, - actionTimestamp: Option[Long] = None, - actionAuthorId: Option[String] = None, - info: Option[String] = None) - extends ApplyableVolumeUpdateAction { - override def addTimestamp(timestamp: Long): VolumeUpdateAction = this.copy(actionTimestamp = Some(timestamp)) - override def addAuthorId(authorId: Option[String]): VolumeUpdateAction = - this.copy(actionAuthorId = authorId) - override def addInfo(info: Option[String]): UpdateAction = this.copy(info = info) - override def withActionTracingId(newTracingId: String): LayerUpdateAction = - this.copy(actionTracingId = newTracingId) - - override def applyOn(tracing: VolumeTracing): VolumeTracing = { - - def updateUserBoundingBoxes(): Seq[NamedBoundingBoxProto] = - tracing.userBoundingBoxes.map { boundingBox => - if (boundingBoxId.forall(_ == boundingBox.id)) - boundingBox.copy(isVisible = Some(isVisible)) - else - boundingBox - } - - tracing.withUserBoundingBoxes(updateUserBoundingBoxes()) - } - - override def isViewOnlyChange: Boolean = true -} - -case class UpdateUserBoundingBoxNameSkeletonAction(boundingBox: NamedBoundingBox, - actionTracingId: String, - actionTimestamp: Option[Long] = None, - actionAuthorId: Option[String] = None, - info: Option[String] = None) - extends ApplyableVolumeUpdateAction { - override def applyOn(tracing: VolumeTracing): VolumeTracing = { - def updateUserBoundingBoxes() = - tracing.userBoundingBoxes.map { currentBoundingBox => - if (boundingBox.id == currentBoundingBox.id) - currentBoundingBox.copy(name = boundingBox.name) - else - currentBoundingBox - } - tracing.withUserBoundingBoxes(updateUserBoundingBoxes()) - } - - override def addTimestamp(timestamp: Long): UpdateAction = - this.copy(actionTimestamp = Some(timestamp)) - override def addInfo(info: Option[String]): UpdateAction = this.copy(info = info) - override def addAuthorId(authorId: Option[String]): UpdateAction = - this.copy(actionAuthorId = authorId) - override def withActionTracingId(newTracingId: String): LayerUpdateAction = - this.copy(actionTracingId = newTracingId) -} - -case class UpdateUserBoundingBoxColorSkeletonAction(boundingBox: NamedBoundingBox, - actionTracingId: String, - actionTimestamp: Option[Long] = None, - actionAuthorId: Option[String] = None, - info: Option[String] = None) - extends ApplyableVolumeUpdateAction { - override def applyOn(tracing: VolumeTracing): VolumeTracing = { - def updateUserBoundingBoxes() = - tracing.userBoundingBoxes.map { currentBoundingBox => - if (boundingBox.id == currentBoundingBox.id) - currentBoundingBox.copy(color = boundingBox.toProto.color) - else + if (boundingBoxId == currentBoundingBox.id) { + currentBoundingBox.copy( + id = updatedProps.id.getOrElse(currentBoundingBox.id), + name = + if (updatedPropKeys.contains("name") && updatedProps.name.isDefined) updatedProps.name + else currentBoundingBox.name, + isVisible = + if (updatedPropKeys.contains("isVisible") && updatedProps.isVisible.isDefined) updatedProps.isVisible + else currentBoundingBox.isVisible, + color = + if (updatedPropKeys.contains("color") && updatedProps.color.isDefined) updatedProps.colorOptProto + else currentBoundingBox.color, + boundingBox = + if (updatedPropKeys.contains("boundingBox")) + updatedProps.boundingBoxProto.getOrElse(currentBoundingBox.boundingBox) + else currentBoundingBox.boundingBox + ) + } else currentBoundingBox } tracing.withUserBoundingBoxes(updateUserBoundingBoxes()) @@ -514,29 +449,17 @@ object UpdateUserBoundingBoxesVolumeAction { implicit val jsonFormat: OFormat[UpdateUserBoundingBoxesVolumeAction] = Json.format[UpdateUserBoundingBoxesVolumeAction] } -object AddUserBoundingBoxSkeletonAction { - implicit val jsonFormat: OFormat[AddUserBoundingBoxSkeletonAction] = - Json.format[AddUserBoundingBoxSkeletonAction] -} -object DeleteUserBoundingBoxSkeletonAction { - implicit val jsonFormat: OFormat[DeleteUserBoundingBoxSkeletonAction] = - Json.format[DeleteUserBoundingBoxSkeletonAction] -} -object UpdateUserBoundingBoxBoundsSkeletonAction { - implicit val jsonFormat: OFormat[UpdateUserBoundingBoxBoundsSkeletonAction] = - Json.format[UpdateUserBoundingBoxBoundsSkeletonAction] -} -object UpdateUserBoundingBoxVisibilityVolumeAction { - implicit val jsonFormat: OFormat[UpdateUserBoundingBoxVisibilityVolumeAction] = - Json.format[UpdateUserBoundingBoxVisibilityVolumeAction] +object AddUserBoundingBoxVolumeAction { + implicit val jsonFormat: OFormat[AddUserBoundingBoxVolumeAction] = + Json.format[AddUserBoundingBoxVolumeAction] } -object UpdateUserBoundingBoxNameSkeletonAction { - implicit val jsonFormat: OFormat[UpdateUserBoundingBoxNameSkeletonAction] = - Json.format[UpdateUserBoundingBoxNameSkeletonAction] +object DeleteUserBoundingBoxVolumeAction { + implicit val jsonFormat: OFormat[DeleteUserBoundingBoxVolumeAction] = + Json.format[DeleteUserBoundingBoxVolumeAction] } -object UpdateUserBoundingBoxColorSkeletonAction { - implicit val jsonFormat: OFormat[UpdateUserBoundingBoxColorSkeletonAction] = - Json.format[UpdateUserBoundingBoxColorSkeletonAction] +object UpdateUserBoundingBoxVolumeAction { + implicit val jsonFormat: OFormat[UpdateUserBoundingBoxVolumeAction] = + Json.format[UpdateUserBoundingBoxVolumeAction] } object RemoveFallbackLayerVolumeAction { implicit val jsonFormat: OFormat[RemoveFallbackLayerVolumeAction] = Json.format[RemoveFallbackLayerVolumeAction] From 55fdf45f9a1b07754ad0f99ab4a0dadf62e44660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 8 Apr 2025 18:45:47 +0200 Subject: [PATCH 08/18] Don't make frontend return a single elemnt list of a user bbox for new add update actions --- .../oxalis/model/reducers/reducer_helpers.ts | 10 ++++------ .../javascripts/oxalis/model/sagas/update_actions.ts | 8 ++++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/frontend/javascripts/oxalis/model/reducers/reducer_helpers.ts b/frontend/javascripts/oxalis/model/reducers/reducer_helpers.ts index 7fd0a5c090f..27c05373c8e 100644 --- a/frontend/javascripts/oxalis/model/reducers/reducer_helpers.ts +++ b/frontend/javascripts/oxalis/model/reducers/reducer_helpers.ts @@ -57,12 +57,10 @@ export function convertUserBoundingBoxesFromServerToFrontend( }); } export function convertUserBoundingBoxesFromFrontendToServer( - boundingBoxes: Array, -): Array { - return boundingBoxes.map((bb) => { - const { boundingBox, ...rest } = bb; - return { ...rest, boundingBox: Utils.computeBoundingBoxObjectFromBoundingBox(boundingBox) }; - }); + boundingBox: UserBoundingBox, +): UserBoundingBoxToServer { + const { boundingBox: bb, ...rest } = boundingBox; + return { ...rest, boundingBox: Utils.computeBoundingBoxObjectFromBoundingBox(bb) }; } export function convertFrontendBoundingBoxToServer( boundingBox: BoundingBoxType, diff --git a/frontend/javascripts/oxalis/model/sagas/update_actions.ts b/frontend/javascripts/oxalis/model/sagas/update_actions.ts index bcc8f9cfd07..6982b942149 100644 --- a/frontend/javascripts/oxalis/model/sagas/update_actions.ts +++ b/frontend/javascripts/oxalis/model/sagas/update_actions.ts @@ -409,7 +409,7 @@ export function addUserBoundingBoxInSkeletonTracingAction( return { name: "addUserBoundingBoxSkeletonAction", value: { - boundingBox: convertUserBoundingBoxesFromFrontendToServer([boundingBox]), + boundingBox: convertUserBoundingBoxesFromFrontendToServer(boundingBox), actionTracingId, }, } as const; @@ -422,7 +422,7 @@ export function addUserBoundingBoxInVolumeTracingAction( return { name: "addUserBoundingBoxVolumeAction", value: { - boundingBox: convertUserBoundingBoxesFromFrontendToServer([boundingBox]), + boundingBox: convertUserBoundingBoxesFromFrontendToServer(boundingBox), actionTracingId, }, } as const; @@ -496,7 +496,7 @@ export function updateUserBoundingBoxesInSkeletonTracing( name: "updateUserBoundingBoxesInSkeletonTracing", value: { actionTracingId, - boundingBoxes: convertUserBoundingBoxesFromFrontendToServer(userBoundingBoxes), + boundingBoxes: userBoundingBoxes.map(convertUserBoundingBoxesFromFrontendToServer), }, } as const; } @@ -508,7 +508,7 @@ export function updateUserBoundingBoxesInVolumeTracing( name: "updateUserBoundingBoxesInVolumeTracing", value: { actionTracingId, - boundingBoxes: convertUserBoundingBoxesFromFrontendToServer(userBoundingBoxes), + boundingBoxes: userBoundingBoxes.map(convertUserBoundingBoxesFromFrontendToServer), }, } as const; } From d8dcfdd4c4c788f39775440b34d9c4a9bdd26ad3 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Wed, 9 Apr 2025 14:48:45 +0200 Subject: [PATCH 09/18] [frontend] dont send empty update objects --- .../javascripts/oxalis/model/sagas/skeletontracing_saga.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts index 9e6c76059fe..8438c25a74d 100644 --- a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts @@ -622,7 +622,7 @@ export function* diffBoundingBoxes( const { onlyA: deletedBBoxIds, onlyB: addedBBoxIds, - both: changedBBoxIds, + both: maybeChangedBBoxIds, } = Utils.diffArrays( _.map(prevBoundingBoxes, (bbox) => bbox.id), _.map(currentBoundingBoxes, (bbox) => bbox.id), @@ -652,7 +652,7 @@ export function* diffBoundingBoxes( Toast.error(getErrorMessage(id)); } } - for (const id of changedBBoxIds) { + for (const id of maybeChangedBBoxIds) { const currentBbox = currentBoundingBoxes.find((bbox) => bbox.id === id); const prevBbox = prevBoundingBoxes.find((bbox) => bbox.id === id); if (currentBbox == null || prevBbox == null) { @@ -660,6 +660,7 @@ export function* diffBoundingBoxes( continue; } const diffBBox = Utils.diffObjects(currentBbox, prevBbox); + if (_.isEmpty(diffBBox)) continue; //TODO_C remove console.log("diffBBox", diffBBox); yield updateBBoxAction(currentBbox.id, diffBBox, tracingId); From b8d0a71e9a7f63fb0499d39d9a4f6774ba6a717b Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Wed, 9 Apr 2025 16:39:11 +0200 Subject: [PATCH 10/18] [frontend] WIP: send right bbox object to server --- .../oxalis/model/reducers/reducer_helpers.ts | 1 + .../model/sagas/skeletontracing_saga.ts | 7 +++++-- .../oxalis/model/sagas/update_actions.ts | 21 +++++++++++++++---- .../test/sagas/compact_toggle_actions.spec.ts | 1 - .../test/sagas/skeletontracing_saga.spec.ts | 1 - 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/frontend/javascripts/oxalis/model/reducers/reducer_helpers.ts b/frontend/javascripts/oxalis/model/reducers/reducer_helpers.ts index 27c05373c8e..c649282138d 100644 --- a/frontend/javascripts/oxalis/model/reducers/reducer_helpers.ts +++ b/frontend/javascripts/oxalis/model/reducers/reducer_helpers.ts @@ -56,6 +56,7 @@ export function convertUserBoundingBoxesFromServerToFrontend( }; }); } + export function convertUserBoundingBoxesFromFrontendToServer( boundingBox: UserBoundingBox, ): UserBoundingBoxToServer { diff --git a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts index 8438c25a74d..a60372e0caa 100644 --- a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts @@ -661,8 +661,11 @@ export function* diffBoundingBoxes( } const diffBBox = Utils.diffObjects(currentBbox, prevBbox); if (_.isEmpty(diffBBox)) continue; - //TODO_C remove - console.log("diffBBox", diffBBox); + const changedKeys = Object.keys(diffBBox); + if (changedKeys.includes("boundingBox")) { + diffBBox.boundingBox.min = currentBbox.boundingBox.min; + diffBBox.boundingBox.max = currentBbox.boundingBox.max; + } yield updateBBoxAction(currentBbox.id, diffBBox, tracingId); } } diff --git a/frontend/javascripts/oxalis/model/sagas/update_actions.ts b/frontend/javascripts/oxalis/model/sagas/update_actions.ts index 6982b942149..6e3a3fb2857 100644 --- a/frontend/javascripts/oxalis/model/sagas/update_actions.ts +++ b/frontend/javascripts/oxalis/model/sagas/update_actions.ts @@ -1,4 +1,5 @@ -import type { Vector3 } from "oxalis/constants"; +import * as Utils from "libs/utils"; +import { EMPTY_OBJECT, type Vector3 } from "oxalis/constants"; import type { SendBucketInfo } from "oxalis/model/bucket_data_handling/wkstore_adapter"; import { convertUserBoundingBoxesFromFrontendToServer } from "oxalis/model/reducers/reducer_helpers"; import type { @@ -459,13 +460,19 @@ export function updateUserBoundingBoxInSkeletonTracingAction( updatedProps: Partial, actionTracingId: string, ) { + let serverBBox = EMPTY_OBJECT; + const { boundingBox, ...rest } = updatedProps; const updatedPropKeys = Object.keys(updatedProps); + if (updatedProps.boundingBox?.min != null && updatedProps.boundingBox?.max != null) { + const bb = updatedProps.boundingBox; + serverBBox = { boundingBox: Utils.computeBoundingBoxObjectFromBoundingBox(bb) }; + } return { - name: "updateUserBoundingBoxSkeletonAction", + name: "updateUserBoundingBoxVolumeAction", value: { boundingBoxId, actionTracingId, - updatedProps, + updatedProps: { ...serverBBox, ...rest }, updatedPropKeys, }, } as const; @@ -476,13 +483,19 @@ export function updateUserBoundingBoxInVolumeTracingAction( updatedProps: Partial, actionTracingId: string, ) { + let serverBBox = EMPTY_OBJECT; + const { boundingBox, ...rest } = updatedProps; const updatedPropKeys = Object.keys(updatedProps); + if (updatedProps.boundingBox?.min != null && updatedProps.boundingBox?.max != null) { + const bb = updatedProps.boundingBox; + serverBBox = { boundingBox: Utils.computeBoundingBoxObjectFromBoundingBox(bb) }; + } return { name: "updateUserBoundingBoxVolumeAction", value: { boundingBoxId, actionTracingId, - updatedProps, + updatedProps: { ...serverBBox, ...rest }, updatedPropKeys, }, } as const; diff --git a/frontend/javascripts/test/sagas/compact_toggle_actions.spec.ts b/frontend/javascripts/test/sagas/compact_toggle_actions.spec.ts index a6d9424cf2c..c2ec4b140d4 100644 --- a/frontend/javascripts/test/sagas/compact_toggle_actions.spec.ts +++ b/frontend/javascripts/test/sagas/compact_toggle_actions.spec.ts @@ -107,7 +107,6 @@ function testDiffing(prevState: OxalisState, nextState: OxalisState) { enforceSkeletonTracing(nextState.annotation), flycamMock, flycamMock, - nextState.activeUser?.id || null, ), ), ), diff --git a/frontend/javascripts/test/sagas/skeletontracing_saga.spec.ts b/frontend/javascripts/test/sagas/skeletontracing_saga.spec.ts index 08ff771dc3b..35fb080c355 100644 --- a/frontend/javascripts/test/sagas/skeletontracing_saga.spec.ts +++ b/frontend/javascripts/test/sagas/skeletontracing_saga.spec.ts @@ -63,7 +63,6 @@ function testDiffing( enforceSkeletonTracing(nextAnnotation), prevFlycam, flycam, - null, ), ), ); From 21e13e2779b28340784c7f52b972ede91f53cccb Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Wed, 9 Apr 2025 18:19:01 +0200 Subject: [PATCH 11/18] [frontend] clean up code --- .../oxalis/model/sagas/update_actions.ts | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/update_actions.ts b/frontend/javascripts/oxalis/model/sagas/update_actions.ts index 6e3a3fb2857..e6a8b8c0b17 100644 --- a/frontend/javascripts/oxalis/model/sagas/update_actions.ts +++ b/frontend/javascripts/oxalis/model/sagas/update_actions.ts @@ -455,7 +455,8 @@ export function deleteUserBoundingBoxInVolumeTracingAction( } as const; } -export function updateUserBoundingBoxInSkeletonTracingAction( +function getUpdateUserBoundingBoxAction( + actionName: "updateUserBoundingBoxVolumeAction" | "updateUserBoundingBoxSkeletonAction", boundingBoxId: number, updatedProps: Partial, actionTracingId: string, @@ -463,12 +464,11 @@ export function updateUserBoundingBoxInSkeletonTracingAction( let serverBBox = EMPTY_OBJECT; const { boundingBox, ...rest } = updatedProps; const updatedPropKeys = Object.keys(updatedProps); - if (updatedProps.boundingBox?.min != null && updatedProps.boundingBox?.max != null) { - const bb = updatedProps.boundingBox; - serverBBox = { boundingBox: Utils.computeBoundingBoxObjectFromBoundingBox(bb) }; + if (boundingBox != null) { + serverBBox = { boundingBox: Utils.computeBoundingBoxObjectFromBoundingBox(boundingBox) }; } return { - name: "updateUserBoundingBoxVolumeAction", + name: actionName, value: { boundingBoxId, actionTracingId, @@ -483,22 +483,25 @@ export function updateUserBoundingBoxInVolumeTracingAction( updatedProps: Partial, actionTracingId: string, ) { - let serverBBox = EMPTY_OBJECT; - const { boundingBox, ...rest } = updatedProps; - const updatedPropKeys = Object.keys(updatedProps); - if (updatedProps.boundingBox?.min != null && updatedProps.boundingBox?.max != null) { - const bb = updatedProps.boundingBox; - serverBBox = { boundingBox: Utils.computeBoundingBoxObjectFromBoundingBox(bb) }; - } - return { - name: "updateUserBoundingBoxVolumeAction", - value: { - boundingBoxId, - actionTracingId, - updatedProps: { ...serverBBox, ...rest }, - updatedPropKeys, - }, - } as const; + return getUpdateUserBoundingBoxAction( + "updateUserBoundingBoxVolumeAction", + boundingBoxId, + updatedProps, + actionTracingId, + ); +} + +export function updateUserBoundingBoxInSkeletonTracingAction( + boundingBoxId: number, + updatedProps: Partial, + actionTracingId: string, +) { + return getUpdateUserBoundingBoxAction( + "updateUserBoundingBoxSkeletonAction", + boundingBoxId, + updatedProps, + actionTracingId, + ); } export function updateUserBoundingBoxesInSkeletonTracing( From 374fe71fb8a4011c56c8b3b5c063a395061b75b5 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Thu, 10 Apr 2025 11:55:49 +0200 Subject: [PATCH 12/18] [frontend] remove unneccesary update actions sent to backend --- .../helpers/compaction/compact_save_queue.ts | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/frontend/javascripts/oxalis/model/helpers/compaction/compact_save_queue.ts b/frontend/javascripts/oxalis/model/helpers/compaction/compact_save_queue.ts index 7504a03eded..493c33f53db 100644 --- a/frontend/javascripts/oxalis/model/helpers/compaction/compact_save_queue.ts +++ b/frontend/javascripts/oxalis/model/helpers/compaction/compact_save_queue.ts @@ -70,6 +70,39 @@ function removeSubsequentUpdateNodeActions(updateActionsBatches: Array) { + const obsoleteUpdateActions = []; + + // Actions are obsolete, if they are for the same bounding box and for the same prop. + // The given action is always compared to the next next one, as usually an + // updateUserBoundingBoxSkeletonAction and updateUserBoundingBoxVolumeAction + // is sent at the same time. + for (let i = 0; i < updateActionsBatches.length - 2; i++) { + const actions1 = updateActionsBatches[i].actions; + const actions2 = updateActionsBatches[i + 2].actions; + + if ( + actions1.length === 1 && + actions2.length === 1 && + (actions1[0].name === "updateUserBoundingBoxSkeletonAction" || + actions1[0].name === "updateUserBoundingBoxVolumeAction") && + actions1[0].name === actions2[0].name && + actions1[0].value.boundingBoxId === actions2[0].value.boundingBoxId && + _.isEqual(actions1[0].value.updatedPropKeys, actions2[0].value.updatedPropKeys) + ) { + obsoleteUpdateActions.push(updateActionsBatches[i]); + console.log( + actions1[0].value.boundingBoxId, + actions1[0].value.updatedProps, + actions2[0].value.boundingBoxId, + actions2[0].value.updatedProps, + ); // TODO_c remove + } + } + + return _.without(updateActionsBatches, ...obsoleteUpdateActions); +} + function removeSubsequentUpdateSegmentActions(updateActionsBatches: Array) { const obsoleteUpdateActions = []; @@ -100,10 +133,12 @@ export default function compactSaveQueue( (updateActionsBatch) => updateActionsBatch.actions.length > 0, ); - return removeSubsequentUpdateSegmentActions( - removeSubsequentUpdateTreeActions( - removeSubsequentUpdateNodeActions( - removeAllButLastUpdateTdCameraAction(removeAllButLastUpdateTracingAction(result)), + return removeSubsequentUpdateBBoxActions( + removeSubsequentUpdateSegmentActions( + removeSubsequentUpdateTreeActions( + removeSubsequentUpdateNodeActions( + removeAllButLastUpdateTdCameraAction(removeAllButLastUpdateTracingAction(result)), + ), ), ), ); From c6a9b0fae80b17d276aa5a59f8871cdecc0eb887 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Thu, 10 Apr 2025 13:21:27 +0200 Subject: [PATCH 13/18] [frontend] remove old actions --- .../oxalis/model/sagas/update_actions.ts | 32 ------------------- .../javascripts/oxalis/view/version_entry.tsx | 9 ------ 2 files changed, 41 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/update_actions.ts b/frontend/javascripts/oxalis/model/sagas/update_actions.ts index e6a8b8c0b17..5443c83207c 100644 --- a/frontend/javascripts/oxalis/model/sagas/update_actions.ts +++ b/frontend/javascripts/oxalis/model/sagas/update_actions.ts @@ -57,12 +57,6 @@ export type UpdateUserBoundingBoxInSkeletonTracingAction = ReturnType< export type UpdateUserBoundingBoxInVolumeTracingAction = ReturnType< typeof updateUserBoundingBoxInVolumeTracingAction >; -type UpdateUserBoundingBoxesInSkeletonTracingUpdateAction = ReturnType< - typeof updateUserBoundingBoxesInSkeletonTracing ->; -type UpdateUserBoundingBoxesInVolumeTracingUpdateAction = ReturnType< - typeof updateUserBoundingBoxesInVolumeTracing ->; export type UpdateBucketUpdateAction = ReturnType; export type UpdateSegmentGroupsUpdateAction = ReturnType; @@ -108,8 +102,6 @@ export type UpdateActionWithoutIsolationRequirement = | DeleteUserBoundingBoxInVolumeTracingAction | UpdateUserBoundingBoxInSkeletonTracingAction | UpdateUserBoundingBoxInVolumeTracingAction - | UpdateUserBoundingBoxesInSkeletonTracingUpdateAction - | UpdateUserBoundingBoxesInVolumeTracingUpdateAction | CreateSegmentUpdateAction | UpdateSegmentUpdateAction | DeleteSegmentUpdateAction @@ -504,30 +496,6 @@ export function updateUserBoundingBoxInSkeletonTracingAction( ); } -export function updateUserBoundingBoxesInSkeletonTracing( - userBoundingBoxes: Array, - actionTracingId: string, -) { - return { - name: "updateUserBoundingBoxesInSkeletonTracing", - value: { - actionTracingId, - boundingBoxes: userBoundingBoxes.map(convertUserBoundingBoxesFromFrontendToServer), - }, - } as const; -} -export function updateUserBoundingBoxesInVolumeTracing( - userBoundingBoxes: Array, - actionTracingId: string, -) { - return { - name: "updateUserBoundingBoxesInVolumeTracing", - value: { - actionTracingId, - boundingBoxes: userBoundingBoxes.map(convertUserBoundingBoxesFromFrontendToServer), - }, - } as const; -} export function createSegmentVolumeAction( id: number, anchorPosition: Vector3 | null | undefined, diff --git a/frontend/javascripts/oxalis/view/version_entry.tsx b/frontend/javascripts/oxalis/view/version_entry.tsx index 072f9e52f13..4d0f2ac8521 100644 --- a/frontend/javascripts/oxalis/view/version_entry.tsx +++ b/frontend/javascripts/oxalis/view/version_entry.tsx @@ -2,7 +2,6 @@ import { ArrowsAltOutlined, BackwardOutlined, CodeSandboxOutlined, - CodepenOutlined, DeleteOutlined, EditOutlined, EyeOutlined, @@ -76,14 +75,6 @@ const descriptionFns: Record< description: "Created the annotation.", icon: , }), - updateUserBoundingBoxesInSkeletonTracing: (): Description => ({ - description: "Updated a bounding box.", - icon: , - }), - updateUserBoundingBoxesInVolumeTracing: (): Description => ({ - description: "Updated a bounding box.", - icon: , - }), addUserBoundingBoxSkeletonAction: (): Description => ({ description: "Added a bounding box.", icon: , From 0ae286c5095d87bac7a3dc1411da409961e3ba80 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Wed, 23 Apr 2025 14:19:07 +0200 Subject: [PATCH 14/18] [frontend] use updateUserBoundingBoxVisibilityActions --- .../model/sagas/skeletontracing_saga.ts | 10 +++++ .../oxalis/model/sagas/update_actions.ts | 43 ++++++++++++++++++- .../javascripts/oxalis/view/version_entry.tsx | 8 ++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts index a60372e0caa..2e96e23d342 100644 --- a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts @@ -66,6 +66,8 @@ import { updateTreeVisibility, updateUserBoundingBoxInSkeletonTracingAction, updateUserBoundingBoxInVolumeTracingAction, + updateUserBoundingBoxVisibilityInSkeletonTracingAction, + updateUserBoundingBoxVisibilityInVolumeTracingAction, } from "oxalis/model/sagas/update_actions"; import { api } from "oxalis/singletons"; import type { @@ -639,6 +641,10 @@ export function* diffBoundingBoxes( tracingType === AnnotationLayerEnum.Skeleton ? updateUserBoundingBoxInSkeletonTracingAction : updateUserBoundingBoxInVolumeTracingAction; + const updateBBoxVisibilityAction = + tracingType === AnnotationLayerEnum.Skeleton + ? updateUserBoundingBoxVisibilityInSkeletonTracingAction + : updateUserBoundingBoxVisibilityInVolumeTracingAction; const getErrorMessage = (id: number) => `User bounding box with id ${id} not found in ${tracingType} tracing.`; for (const id of deletedBBoxIds) { @@ -662,6 +668,10 @@ export function* diffBoundingBoxes( const diffBBox = Utils.diffObjects(currentBbox, prevBbox); if (_.isEmpty(diffBBox)) continue; const changedKeys = Object.keys(diffBBox); + if (changedKeys.includes("isVisible")) { + yield updateBBoxVisibilityAction(currentBbox.id, currentBbox.isVisible, tracingId); + continue; + } if (changedKeys.includes("boundingBox")) { diffBBox.boundingBox.min = currentBbox.boundingBox.min; diffBBox.boundingBox.max = currentBbox.boundingBox.max; diff --git a/frontend/javascripts/oxalis/model/sagas/update_actions.ts b/frontend/javascripts/oxalis/model/sagas/update_actions.ts index 5443c83207c..35eaccfaf2c 100644 --- a/frontend/javascripts/oxalis/model/sagas/update_actions.ts +++ b/frontend/javascripts/oxalis/model/sagas/update_actions.ts @@ -21,6 +21,8 @@ export type NodeWithTreeId = { treeId: number; } & Node; +type PartialBoundingBoxWithoutVisibility = Partial>; + export type UpdateTreeUpdateAction = ReturnType | ReturnType; export type DeleteTreeUpdateAction = ReturnType; export type MoveTreeComponentUpdateAction = ReturnType; @@ -57,6 +59,12 @@ export type UpdateUserBoundingBoxInSkeletonTracingAction = ReturnType< export type UpdateUserBoundingBoxInVolumeTracingAction = ReturnType< typeof updateUserBoundingBoxInVolumeTracingAction >; +export type UpdateUserBoundingBoxVisibilityInSkeletonTracingAction = ReturnType< + typeof updateUserBoundingBoxVisibilityInSkeletonTracingAction +>; +export type UpdateUserBoundingBoxVisibilityInVolumeTracingAction = ReturnType< + typeof updateUserBoundingBoxVisibilityInVolumeTracingAction +>; export type UpdateBucketUpdateAction = ReturnType; export type UpdateSegmentGroupsUpdateAction = ReturnType; @@ -102,6 +110,8 @@ export type UpdateActionWithoutIsolationRequirement = | DeleteUserBoundingBoxInVolumeTracingAction | UpdateUserBoundingBoxInSkeletonTracingAction | UpdateUserBoundingBoxInVolumeTracingAction + | UpdateUserBoundingBoxVisibilityInSkeletonTracingAction + | UpdateUserBoundingBoxVisibilityInVolumeTracingAction | CreateSegmentUpdateAction | UpdateSegmentUpdateAction | DeleteSegmentUpdateAction @@ -450,7 +460,7 @@ export function deleteUserBoundingBoxInVolumeTracingAction( function getUpdateUserBoundingBoxAction( actionName: "updateUserBoundingBoxVolumeAction" | "updateUserBoundingBoxSkeletonAction", boundingBoxId: number, - updatedProps: Partial, + updatedProps: PartialBoundingBoxWithoutVisibility, actionTracingId: string, ) { let serverBBox = EMPTY_OBJECT; @@ -472,7 +482,7 @@ function getUpdateUserBoundingBoxAction( export function updateUserBoundingBoxInVolumeTracingAction( boundingBoxId: number, - updatedProps: Partial, + updatedProps: PartialBoundingBoxWithoutVisibility, actionTracingId: string, ) { return getUpdateUserBoundingBoxAction( @@ -496,6 +506,35 @@ export function updateUserBoundingBoxInSkeletonTracingAction( ); } +export function updateUserBoundingBoxVisibilityInSkeletonTracingAction( + boundingBoxId: number, + isVisible: boolean, + actionTracingId: string, +) { + return { + name: "updateUserBoundingBoxVisibilitySkeletonAction", + value: { + boundingBoxId, + actionTracingId, + isVisible, + }, + } as const; +} +export function updateUserBoundingBoxVisibilityInVolumeTracingAction( + boundingBoxId: number, + isVisible: boolean, + actionTracingId: string, +) { + return { + name: "updateUserBoundingBoxVisibilityVolumeAction", + value: { + boundingBoxId, + actionTracingId, + isVisible, + }, + } as const; +} + export function createSegmentVolumeAction( id: number, anchorPosition: Vector3 | null | undefined, diff --git a/frontend/javascripts/oxalis/view/version_entry.tsx b/frontend/javascripts/oxalis/view/version_entry.tsx index 4d0f2ac8521..8ee34bdf72e 100644 --- a/frontend/javascripts/oxalis/view/version_entry.tsx +++ b/frontend/javascripts/oxalis/view/version_entry.tsx @@ -99,6 +99,14 @@ const descriptionFns: Record< description: "Updated a bounding box.", icon: , }), + updateUserBoundingBoxVisibilitySkeletonAction: (): Description => ({ + description: "Toggled the visibility of a bounding box.", + icon: , + }), + updateUserBoundingBoxVisibilityVolumeAction: (): Description => ({ + description: "Toggled the visibility of a bounding box.", + icon: , + }), removeFallbackLayer: (): Description => ({ description: "Removed the segmentation fallback layer.", icon: , From 675ae3571f7c7570c24f4bb3322c1670690f1909 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Wed, 23 Apr 2025 14:28:39 +0200 Subject: [PATCH 15/18] [frontend] adjust new update actions name fields --- .../helpers/compaction/compact_save_queue.ts | 4 ++-- .../oxalis/model/sagas/update_actions.ts | 18 ++++++++-------- .../javascripts/oxalis/view/version_entry.tsx | 21 +++++++++---------- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/frontend/javascripts/oxalis/model/helpers/compaction/compact_save_queue.ts b/frontend/javascripts/oxalis/model/helpers/compaction/compact_save_queue.ts index 493c33f53db..17552bf6f7f 100644 --- a/frontend/javascripts/oxalis/model/helpers/compaction/compact_save_queue.ts +++ b/frontend/javascripts/oxalis/model/helpers/compaction/compact_save_queue.ts @@ -84,8 +84,8 @@ function removeSubsequentUpdateBBoxActions(updateActionsBatches: Array, }), - addUserBoundingBoxSkeletonAction: (): Description => ({ + addUserBoundingBoxSkeleton: (): Description => ({ description: "Added a bounding box.", icon: , }), - addUserBoundingBoxVolumeAction: (): Description => ({ + addUserBoundingBoxVolume: (): Description => ({ description: "Added a bounding box.", icon: , }), - deleteUserBoundingBoxSkeletonAction: (): Description => ({ + deleteUserBoundingBoxSkeleton: (): Description => ({ description: "Deleted a bounding box.", icon: , }), - deleteUserBoundingBoxVolumeAction: (): Description => ({ + deleteUserBoundingBoxVolume: (): Description => ({ description: "Deleted a bounding box.", icon: , }), - updateUserBoundingBoxSkeletonAction: (): Description => ({ + updateUserBoundingBoxSkeleton: (): Description => ({ description: "Updated a bounding box.", icon: , }), - updateUserBoundingBoxVolumeAction: (): Description => ({ + updateUserBoundingBoxVolume: (): Description => ({ description: "Updated a bounding box.", icon: , }), - updateUserBoundingBoxVisibilitySkeletonAction: (): Description => ({ + updateUserBoundingBoxVisibilitySkeleton: (): Description => ({ description: "Toggled the visibility of a bounding box.", icon: , }), - updateUserBoundingBoxVisibilityVolumeAction: (): Description => ({ + updateUserBoundingBoxVisibilityVolume: (): Description => ({ description: "Toggled the visibility of a bounding box.", icon: , }), @@ -215,9 +215,8 @@ const descriptionFns: Record< icon: Hide Tree Edges Icon, }), updateTreeGroupVisibility: (action: UpdateTreeGroupVisibilityUpdateAction): Description => ({ - description: `Updated the visibility of the group with id ${ - action.value.treeGroupId != null ? action.value.treeGroupId : MISSING_GROUP_ID - }.`, + description: `Updated the visibility of the group with id ${action.value.treeGroupId != null ? action.value.treeGroupId : MISSING_GROUP_ID + }.`, icon: , }), createEdge: (action: CreateEdgeUpdateAction): Description => ({ From 4d33b5c609fa65a76adef14e05ce2fe5bb82ac61 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Fri, 25 Apr 2025 18:07:45 +0200 Subject: [PATCH 16/18] [frontend] rename actions to ...In[Volume|Skeleton]TracingAction --- .../helpers/compaction/compact_save_queue.ts | 6 ++--- .../oxalis/model/sagas/update_actions.ts | 22 +++++++++---------- .../javascripts/oxalis/view/version_entry.tsx | 21 +++++++++--------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/frontend/javascripts/oxalis/model/helpers/compaction/compact_save_queue.ts b/frontend/javascripts/oxalis/model/helpers/compaction/compact_save_queue.ts index 17552bf6f7f..dd7b2d30a1b 100644 --- a/frontend/javascripts/oxalis/model/helpers/compaction/compact_save_queue.ts +++ b/frontend/javascripts/oxalis/model/helpers/compaction/compact_save_queue.ts @@ -75,7 +75,7 @@ function removeSubsequentUpdateBBoxActions(updateActionsBatches: Array; export type DeleteSegmentUpdateAction = ReturnType; export type DeleteSegmentDataUpdateAction = ReturnType; -export type AddUserBoundingBoxSkeletonAction = ReturnType< +export type AddUserBoundingBoxInSkeletonTracingAction = ReturnType< typeof addUserBoundingBoxInSkeletonTracingAction >; export type AddUserBoundingBoxInVolumeTracingAction = ReturnType< @@ -104,7 +104,7 @@ export type UpdateActionWithoutIsolationRequirement = | DeleteEdgeUpdateAction | UpdateSkeletonTracingUpdateAction | UpdateVolumeTracingUpdateAction - | AddUserBoundingBoxSkeletonAction + | AddUserBoundingBoxInSkeletonTracingAction | AddUserBoundingBoxInVolumeTracingAction | DeleteUserBoundingBoxInSkeletonTracingAction | DeleteUserBoundingBoxInVolumeTracingAction @@ -410,7 +410,7 @@ export function addUserBoundingBoxInSkeletonTracingAction( actionTracingId: string, ) { return { - name: "addUserBoundingBoxSkeleton", + name: "addUserBoundingBoxInSkeletonTracing", value: { boundingBox: convertUserBoundingBoxesFromFrontendToServer(boundingBox), actionTracingId, @@ -423,7 +423,7 @@ export function addUserBoundingBoxInVolumeTracingAction( actionTracingId: string, ) { return { - name: "addUserBoundingBoxVolume", + name: "addUserBoundingBoxInVolumeTracing", value: { boundingBox: convertUserBoundingBoxesFromFrontendToServer(boundingBox), actionTracingId, @@ -436,7 +436,7 @@ export function deleteUserBoundingBoxInSkeletonTracingAction( actionTracingId: string, ) { return { - name: "deleteUserBoundingBoxSkeleton", + name: "deleteUserBoundingBoxInSkeletonTracing", value: { boundingBoxId, actionTracingId, @@ -449,7 +449,7 @@ export function deleteUserBoundingBoxInVolumeTracingAction( actionTracingId: string, ) { return { - name: "deleteUserBoundingBoxVolume", + name: "deleteUserBoundingBoxInVolumeTracing", value: { boundingBoxId, actionTracingId, @@ -458,7 +458,7 @@ export function deleteUserBoundingBoxInVolumeTracingAction( } function getUpdateUserBoundingBoxAction( - actionName: "updateUserBoundingBoxVolume" | "updateUserBoundingBoxSkeleton", + actionName: "updateUserBoundingBoxInVolumeTracing" | "updateUserBoundingBoxInSkeletonTracing", boundingBoxId: number, updatedProps: PartialBoundingBoxWithoutVisibility, actionTracingId: string, @@ -486,7 +486,7 @@ export function updateUserBoundingBoxInVolumeTracingAction( actionTracingId: string, ) { return getUpdateUserBoundingBoxAction( - "updateUserBoundingBoxVolume", + "updateUserBoundingBoxInVolumeTracing", boundingBoxId, updatedProps, actionTracingId, @@ -499,7 +499,7 @@ export function updateUserBoundingBoxInSkeletonTracingAction( actionTracingId: string, ) { return getUpdateUserBoundingBoxAction( - "updateUserBoundingBoxSkeleton", + "updateUserBoundingBoxInSkeletonTracing", boundingBoxId, updatedProps, actionTracingId, @@ -512,7 +512,7 @@ export function updateUserBoundingBoxVisibilityInSkeletonTracingAction( actionTracingId: string, ) { return { - name: "updateUserBoundingBoxVisibilitySkeleton", + name: "updateUserBoundingBoxVisibilityInSkeletonTracing", value: { boundingBoxId, actionTracingId, @@ -526,7 +526,7 @@ export function updateUserBoundingBoxVisibilityInVolumeTracingAction( actionTracingId: string, ) { return { - name: "updateUserBoundingBoxVisibilityVolume", + name: "updateUserBoundingBoxVisibilityInVolumeTracing", value: { boundingBoxId, actionTracingId, diff --git a/frontend/javascripts/oxalis/view/version_entry.tsx b/frontend/javascripts/oxalis/view/version_entry.tsx index 5b163b7ee2c..3a2f167a2c4 100644 --- a/frontend/javascripts/oxalis/view/version_entry.tsx +++ b/frontend/javascripts/oxalis/view/version_entry.tsx @@ -75,35 +75,35 @@ const descriptionFns: Record< description: "Created the annotation.", icon: , }), - addUserBoundingBoxSkeleton: (): Description => ({ + addUserBoundingBoxInSkeletonTracing: (): Description => ({ description: "Added a bounding box.", icon: , }), - addUserBoundingBoxVolume: (): Description => ({ + addUserBoundingBoxInVolumeTracing: (): Description => ({ description: "Added a bounding box.", icon: , }), - deleteUserBoundingBoxSkeleton: (): Description => ({ + deleteUserBoundingBoxInSkeletonTracing: (): Description => ({ description: "Deleted a bounding box.", icon: , }), - deleteUserBoundingBoxVolume: (): Description => ({ + deleteUserBoundingBoxInVolumeTracing: (): Description => ({ description: "Deleted a bounding box.", icon: , }), - updateUserBoundingBoxSkeleton: (): Description => ({ + updateUserBoundingBoxInSkeletonTracing: (): Description => ({ description: "Updated a bounding box.", icon: , }), - updateUserBoundingBoxVolume: (): Description => ({ + updateUserBoundingBoxInVolumeTracing: (): Description => ({ description: "Updated a bounding box.", icon: , }), - updateUserBoundingBoxVisibilitySkeleton: (): Description => ({ + updateUserBoundingBoxVisibilityInSkeletonTracing: (): Description => ({ description: "Toggled the visibility of a bounding box.", icon: , }), - updateUserBoundingBoxVisibilityVolume: (): Description => ({ + updateUserBoundingBoxVisibilityInVolumeTracing: (): Description => ({ description: "Toggled the visibility of a bounding box.", icon: , }), @@ -215,8 +215,9 @@ const descriptionFns: Record< icon: Hide Tree Edges Icon, }), updateTreeGroupVisibility: (action: UpdateTreeGroupVisibilityUpdateAction): Description => ({ - description: `Updated the visibility of the group with id ${action.value.treeGroupId != null ? action.value.treeGroupId : MISSING_GROUP_ID - }.`, + description: `Updated the visibility of the group with id ${ + action.value.treeGroupId != null ? action.value.treeGroupId : MISSING_GROUP_ID + }.`, icon: , }), createEdge: (action: CreateEdgeUpdateAction): Description => ({ From 3ed226fe50b1a67d62d9d39b50b97b0938bdbf5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 7 May 2025 15:40:51 +0200 Subject: [PATCH 17/18] fix merge --- frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx index 8823b29953f..a97f08f752a 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx @@ -77,7 +77,7 @@ import { Model, api } from "oxalis/singletons"; import type { Flycam, SegmentMap, VolumeTracing } from "oxalis/store"; import type { ActionPattern } from "redux-saga/effects"; import { actionChannel, call, fork, put, takeEvery, takeLatest } from "typed-redux-saga"; -import { AnnotationLayerEnum } from "types/api_flow_types"; +import { AnnotationLayerEnum } from "types/api_types"; import { pushSaveQueueTransaction } from "../actions/save_actions"; import { ensureWkReady } from "./ready_sagas"; import { diffBoundingBoxes } from "./skeletontracing_saga"; From ae851c39bfd74487fbf3351e2e45af6d101f801d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 7 May 2025 15:45:13 +0200 Subject: [PATCH 18/18] re-add old UpdateUserBoundingBoxVisibility action & fix naming of action names --- .../annotation/UpdateActions.scala | 18 +++++---- .../updating/SkeletonUpdateActions.scala | 37 +++++++++++++++++-- .../tracings/volume/VolumeUpdateActions.scala | 36 ++++++++++++++++-- 3 files changed, 78 insertions(+), 13 deletions(-) diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/UpdateActions.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/UpdateActions.scala index a790185c066..99b0cf651d2 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/UpdateActions.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/UpdateActions.scala @@ -54,20 +54,24 @@ object UpdateAction { case "updateTreeEdgesVisibility" => deserialize[UpdateTreeEdgesVisibilitySkeletonAction](jsonValue) case "updateUserBoundingBoxesInSkeletonTracing" => deserialize[UpdateUserBoundingBoxesSkeletonAction](jsonValue) - case "addUserBoundingBoxSkeletonAction" => deserialize[AddUserBoundingBoxSkeletonAction](jsonValue) - case "deleteUserBoundingBoxSkeletonAction" => deserialize[DeleteUserBoundingBoxSkeletonAction](jsonValue) - case "updateUserBoundingBoxSkeletonAction" => + case "addUserBoundingBoxInSkeletonTracing" => deserialize[AddUserBoundingBoxSkeletonAction](jsonValue) + case "deleteUserBoundingBoxInSkeletonTracing" => deserialize[DeleteUserBoundingBoxSkeletonAction](jsonValue) + case "updateUserBoundingBoxInSkeletonTracing" => deserialize[UpdateUserBoundingBoxSkeletonAction](jsonValue) + case "updateUserBoundingBoxVisibilityInSkeletonTracing" => + deserialize[UpdateUserBoundingBoxVisibilitySkeletonAction](jsonValue) // Volume case "updateBucket" => deserialize[UpdateBucketVolumeAction](jsonValue) case "updateVolumeTracing" => deserialize[UpdateTracingVolumeAction](jsonValue) - case "updateUserBoundingBoxVolumeAction" => - deserialize[UpdateUserBoundingBoxVolumeAction](jsonValue) - case "addUserBoundingBoxVolumeAction" => deserialize[AddUserBoundingBoxVolumeAction](jsonValue) - case "deleteUserBoundingBoxVolumeAction" => deserialize[DeleteUserBoundingBoxVolumeAction](jsonValue) case "updateUserBoundingBoxesInVolumeTracing" => deserialize[UpdateUserBoundingBoxesVolumeAction](jsonValue) + case "addUserBoundingBoxInVolumeTracing" => deserialize[AddUserBoundingBoxVolumeAction](jsonValue) + case "deleteUserBoundingBoxInVolumeTracing" => deserialize[DeleteUserBoundingBoxVolumeAction](jsonValue) + case "updateUserBoundingBoxInVolumeTracing" => + deserialize[UpdateUserBoundingBoxVolumeAction](jsonValue) + case "updateUserBoundingBoxVisibilityInVolumeTracing" => + deserialize[UpdateUserBoundingBoxVisibilityVolumeAction](jsonValue) case "removeFallbackLayer" => deserialize[RemoveFallbackLayerVolumeAction](jsonValue) case "importVolumeTracing" => deserialize[ImportVolumeDataVolumeAction](jsonValue) case "createSegment" => deserialize[CreateSegmentVolumeAction](jsonValue) diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala index 39b2313ae85..339859c2d4a 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala @@ -592,9 +592,6 @@ case class UpdateUserBoundingBoxSkeletonAction(boundingBoxId: Int, name = if (updatedPropKeys.contains("name") && updatedProps.name.isDefined) updatedProps.name else currentBoundingBox.name, - isVisible = - if (updatedPropKeys.contains("isVisible") && updatedProps.isVisible.isDefined) updatedProps.isVisible - else currentBoundingBox.isVisible, color = if (updatedPropKeys.contains("color") && updatedProps.color.isDefined) updatedProps.colorOptProto else currentBoundingBox.color, @@ -618,6 +615,36 @@ case class UpdateUserBoundingBoxSkeletonAction(boundingBoxId: Int, this.copy(actionTracingId = newTracingId) } +case class UpdateUserBoundingBoxVisibilitySkeletonAction(boundingBoxId: Option[Int], + isVisible: Boolean, + actionTracingId: String, + actionTimestamp: Option[Long] = None, + actionAuthorId: Option[String] = None, + info: Option[String] = None) + extends SkeletonUpdateAction { + override def applyOn(tracing: SkeletonTracing): SkeletonTracing = { + def updateUserBoundingBoxes() = + tracing.userBoundingBoxes.map { boundingBox => + if (boundingBoxId.forall(_ == boundingBox.id)) + boundingBox.copy(isVisible = Some(isVisible)) + else + boundingBox + } + + tracing.withUserBoundingBoxes(updateUserBoundingBoxes()) + } + + override def addTimestamp(timestamp: Long): UpdateAction = + this.copy(actionTimestamp = Some(timestamp)) + override def addInfo(info: Option[String]): UpdateAction = this.copy(info = info) + override def addAuthorId(authorId: Option[String]): UpdateAction = + this.copy(actionAuthorId = authorId) + override def withActionTracingId(newTracingId: String): LayerUpdateAction = + this.copy(actionTracingId = newTracingId) + + override def isViewOnlyChange: Boolean = true +} + object CreateTreeSkeletonAction { implicit val jsonFormat: OFormat[CreateTreeSkeletonAction] = Json.format[CreateTreeSkeletonAction] } @@ -681,3 +708,7 @@ object UpdateUserBoundingBoxSkeletonAction { implicit val jsonFormat: OFormat[UpdateUserBoundingBoxSkeletonAction] = Json.format[UpdateUserBoundingBoxSkeletonAction] } +object UpdateUserBoundingBoxVisibilitySkeletonAction { + implicit val jsonFormat: OFormat[UpdateUserBoundingBoxVisibilitySkeletonAction] = + Json.format[UpdateUserBoundingBoxVisibilitySkeletonAction] +} diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala index fe51021407f..33d3c3b6f5a 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala @@ -170,9 +170,6 @@ case class UpdateUserBoundingBoxVolumeAction(boundingBoxId: Int, name = if (updatedPropKeys.contains("name") && updatedProps.name.isDefined) updatedProps.name else currentBoundingBox.name, - isVisible = - if (updatedPropKeys.contains("isVisible") && updatedProps.isVisible.isDefined) updatedProps.isVisible - else currentBoundingBox.isVisible, color = if (updatedPropKeys.contains("color") && updatedProps.color.isDefined) updatedProps.colorOptProto else currentBoundingBox.color, @@ -196,6 +193,36 @@ case class UpdateUserBoundingBoxVolumeAction(boundingBoxId: Int, this.copy(actionTracingId = newTracingId) } +case class UpdateUserBoundingBoxVisibilityVolumeAction(boundingBoxId: Option[Int], + isVisible: Boolean, + actionTracingId: String, + actionTimestamp: Option[Long] = None, + actionAuthorId: Option[String] = None, + info: Option[String] = None) + extends ApplyableVolumeUpdateAction { + override def addTimestamp(timestamp: Long): VolumeUpdateAction = this.copy(actionTimestamp = Some(timestamp)) + override def addAuthorId(authorId: Option[String]): VolumeUpdateAction = + this.copy(actionAuthorId = authorId) + override def addInfo(info: Option[String]): UpdateAction = this.copy(info = info) + override def withActionTracingId(newTracingId: String): LayerUpdateAction = + this.copy(actionTracingId = newTracingId) + + override def applyOn(tracing: VolumeTracing): VolumeTracing = { + + def updateUserBoundingBoxes(): Seq[NamedBoundingBoxProto] = + tracing.userBoundingBoxes.map { boundingBox => + if (boundingBoxId.forall(_ == boundingBox.id)) + boundingBox.copy(isVisible = Some(isVisible)) + else + boundingBox + } + + tracing.withUserBoundingBoxes(updateUserBoundingBoxes()) + } + + override def isViewOnlyChange: Boolean = true +} + case class RemoveFallbackLayerVolumeAction(actionTracingId: String, actionTimestamp: Option[Long] = None, actionAuthorId: Option[String] = None, @@ -461,6 +488,9 @@ object UpdateUserBoundingBoxVolumeAction { implicit val jsonFormat: OFormat[UpdateUserBoundingBoxVolumeAction] = Json.format[UpdateUserBoundingBoxVolumeAction] } +object UpdateUserBoundingBoxVisibilityVolumeAction { + implicit val jsonFormat: OFormat[UpdateUserBoundingBoxVisibilityVolumeAction] = Json.format[UpdateUserBoundingBoxVisibilityVolumeAction] +} object RemoveFallbackLayerVolumeAction { implicit val jsonFormat: OFormat[RemoveFallbackLayerVolumeAction] = Json.format[RemoveFallbackLayerVolumeAction] }