Skip to content

Per-User State in Annotations: Camera, Visibility Toggles #8542

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 95 commits into
base: master
Choose a base branch
from

Conversation

fm3
Copy link
Member

@fm3 fm3 commented Apr 17, 2025

In shared annotations with multiple authors, some changes are now stored per user. This means that other users won’t see all those changes if their own diverge. This includes the current position and zoom, visibilities of trees, bounding boxes, and segments (as specified with the checkboxes in the lists), as well as which groups are expanded in the lists. The annotation owner’s user state is used as a fallback for users who haven’t explicitly changed these values themselves.

User-Specific state for annotations and tracings
  • Annotation
    • Camera (editPos/rot/zoomLevel)
  • Skeleton
    • activeNodeId
    • Tree visibilities
    • TreeGroup expanded states
    • boundingBox visibilities
  • Volume
    • activeSegmentId
    • SegmentGroup expanded states
    • boundingBox visibilities
    • segment visibilities
Update actions that now mutate (only) the user state
  • UpdateCameraAnnotationAction
  • UpdateSegmentGroupsExpandedStateVolumeAction
  • UpdateSegmentVisibilityVolumeAction
  • UpdateTreeGroupVisibilitySkeletonAction
  • UpdateTreeGroupsExpandedStateSkeletonAction
  • UpdateTreeVisibilitySkeletonAction
  • UpdateUserBoundingBoxVisibilitySkeletonAction
  • UpdateUserBoundingBoxVisibilityVolumeAction
  • UpdateActiveNodeSkeletonAction
  • UpdateActiveSegmentIdVolumeAction

Steps to test:

Note: since new update actions are introduced here, annotations that are edited on this branch cannot be loaded after switching back to master. Refresh your db to be sure everything is clean

  • Edit annotation with multiple users, user-state should be saved and used after reload
  • Annotation owner’s state should be used as a fallback (per element)
  • Layer Deletion/Addition (bbox visibilities should be moved to user state of new precedence layer)
  • NML download should “render” the visibilities etc so that it matches what the requesting user also sees in the browser
  • Merging annotations should preserve user states (despite id remapping of segments/trees etc)
  • Duplicating annotations should preserve the user state for the requesting user (who will be owner of the new annotation).

TODOs:

Backend
  • User state in annotation, skeleton, volume proto
  • Define new update actions
    • Annotation
    • Skeleton
    • Volume
  • Implement applying new update actions
  • When layer precedence changes, move relevant user states (bbox visibilities)
  • Better Naming (now we have class UpdateUserStateSkeletonAction and trait UserStateVolumeUpdateAction…)
  • Render user state for NML writing
    • Camera
    • Skeleton specifics
    • Volume specifics
  • Update with the new UpdateSegmentGroupVisibilityVolumeAction and UpdateSegmentVisibilityVolumeAction
  • discuss what NML parser should do (create a user state? or use old properties?)
  • discuss what duplicate should do (the owner changes, should user state be mutated?)
  • check other tracingstore routes for potential interactions with user state
  • Merge User State when merging Annotations (beware of remapped ids)
    • merge skeleton user states
    • merge volume user states
    • adapt user states to new owner if needed
    • add unit tests for render logic
    • add unit tests for merge logic
  • Refresh snapshots (blocked by Replace docker compose script in yarn #8541)
  • Cleanup
  • Make sure that everything still behaves as expected after Make user bounding box updates more efficient #8492 is merged
Frontend
  • Read relevant properties from user state, with fallback hierarchy
  • Create new update actions
  • Don’t update everything immediately after load
  • fix tests

Issues:


  • Updated changelog
  • Removed dev-only changes like prints and application.conf edits
  • Considered common edge cases
  • Needs datastore update after deployment

Copy link
Contributor

coderabbitai bot commented Apr 17, 2025

Warning

Rate limit exceeded

@philippotto has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 10 minutes and 0 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 261ef4a and 02aa7c9.

📒 Files selected for processing (4)
  • frontend/javascripts/libs/utils.ts (2 hunks)
  • frontend/javascripts/viewer/model/reducers/reducer_helpers.ts (7 hunks)
  • frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts (6 hunks)
  • frontend/javascripts/viewer/view/right-border-tabs/connectome_tab/connectome_view.tsx (3 hunks)
📝 Walkthrough

Walkthrough

This change introduces user-specific state management for annotations, skeleton tracings, and volume tracings. It adds userStates fields to relevant data structures, updates backend and frontend logic to handle per-user view and interaction states, and extends duplication, merging, and update actions to support personalized annotation experiences without affecting other users.

Changes

Files/Groups Change Summary
Backend: Scala controllers, services, models, proto files, and utils Added/updated userStates fields, extended duplication/merge/update logic to handle user context, updated APIs.
Frontend: TypeScript model, reducers, sagas, actions, helpers, utils, and types Integrated user state into annotation/tracing models, reducers, and diffing; added new update actions and helpers.
Proto files (Annotation.proto, SkeletonTracing.proto, VolumeTracing.proto) Added new message types for user states and extended main messages with repeated userStates fields.
Test files (backend and frontend) Added/updated tests to validate user state handling, state merging, and annotation/tracing initialization.
Configuration/routes files Extended API endpoints to accept user and owner IDs for duplication/merging.
Miscellaneous (e.g., changelogs, fixtures, logging, comments) Updated documentation, fixtures, and log messages for clarity and alignment with new user state logic.

Assessment against linked issues

Objective Addressed Explanation
Store user-dependent annotation state separately (e.g., camera, visibility, expansion, etc.) (#8531)
Ensure per-user changes do not affect other users in shared annotations (#8531)
Update duplication, merging, and update actions to handle user-specific state (#8531)
Add/extend APIs and data structures to support user state persistence and retrieval (#8531)

Possibly related PRs

Suggested reviewers

  • MichaelBuessemeyer
  • fm3

Poem

🐇
In the warren of code, where annotations grow,
Each bunny now keeps their own state in tow.
Bounding boxes, trees, and camera zoom,
Personalized views in every room!
No more hopping on each other's toes,
User states blossom—see how it shows!
The garden of tracings, for every hare, now uniquely flows.

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@fm3 fm3 changed the title WIP: Per-User State in Annotations: Camera, Visibility Toggles Per-User State in Annotations: Camera, Visibility Toggles May 26, 2025
@fm3 fm3 marked this pull request as ready for review May 26, 2025 12:38
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 22

🔭 Outside diff range comments (4)
webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingController.scala (1)

376-385: 💡 Verification agent

🧩 Analysis chain

LGTM! Properly extends duplicate method with user context.

The addition of ownerId and requestingUserId parameters aligns with the PR's per-user state management objectives. The parameters are cleanly passed through to the service layer.

Consider verifying that these user IDs are validated either in this controller or in the annotationService.duplicateVolumeTracing method to ensure they are non-empty and valid.

Also applies to: 397-406


🏁 Script executed:

#!/bin/bash
# Description: Check if user ID validation exists in the service layer

# Search for validation of ownerId and requestingUserId in duplicateVolumeTracing implementations
ast-grep --pattern 'def duplicateVolumeTracing($$$) {
  $$$
}'

# Also check for any validation patterns
rg -A 10 "duplicateVolumeTracing.*ownerId.*requestingUserId" --type scala

Length of output: 140


🏁 Script executed:

#!/bin/bash
set -e

echo "1) Locate all definitions of duplicateVolumeTracing in the service layer"
rg -n "def duplicateVolumeTracing" -t scala || true

echo
echo "2) Show 5 lines of context around each duplicateVolumeTracing definition"
rg -n -C5 "def duplicateVolumeTracing" -t scala || true

echo
echo "3) Find all references to annotationService to identify where duplicateVolumeTracing is invoked"
rg -n "annotationService" -t scala || true

Length of output: 28364


🏁 Script executed:

#!/bin/bash
# Show the full duplicateVolumeTracing implementation in TSAnnotationService
sed -n '880,950p' webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/TSAnnotationService.scala

Length of output: 3338


🏁 Script executed:

#!/bin/bash
# Show the duplicate action implementation in VolumeTracingController
sed -n '350,420p' webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingController.scala

Length of output: 3649


Add validation for ownerId and requestingUserId

Currently neither the controller nor the service layer checks that these IDs are non-empty or well-formed before proceeding with duplication. Please add explicit validation to fail fast on invalid input.

• In VolumeTracingController.scala (around the duplicate(...) action):

// before calling annotationService.duplicateVolumeTracing(...)
_ <- Fox.fromBool(ownerId.nonEmpty)           ?~> "ownerId must be non-empty"
_ <- Fox.fromBool(requestingUserId.nonEmpty)  ?~> "requestingUserId must be non-empty"

• In TSAnnotationService.scala (inside duplicateVolumeTracing):

for {
  _ <- Fox.fromBool(ownerId.nonEmpty)          ?~> "ownerId must be non-empty"
  _ <- Fox.fromBool(requestingUserId.nonEmpty) ?~> "requestingUserId must be non-empty"
  sourceTracing <- findVolume(…)
  …
} yield newTracingId

Ensuring these checks at the boundary or in the service guarantees that empty or malformed IDs cannot slip through.

app/models/annotation/nml/NmlWriter.scala (1)

50-64: 🛠️ Refactor suggestion

⚠️ Potential issue

Breaking API change requires documentation.

The addition of the required annotationProto parameter and the change from userOpt: Option[User] to annotationOwner: User constitutes a breaking change to the public API. Consider:

  1. Adding ScalaDoc documentation for the new parameters
  2. Documenting the migration path for existing callers
  3. Ensuring all callers have been updated

Add documentation:

+/**
+ * Converts annotation layers to NML stream format
+ * @param name The output file name
+ * @param annotationProto The annotation protocol buffer containing user states
+ * @param annotationOwner The owner of the annotation (required)
+ * @param requestingUser The user requesting the export (optional, defaults to owner's view)
+ */
 def toNmlStream(name: String,
                 annotationProto: AnnotationProto,
app/models/task/TaskCreationService.scala (2)

149-160: ⚠️ Potential issue

Missing requestingUserId on fresh skeleton creations

In the None branch you call
tracingStoreClient.saveSkeletonTracing(skeleton, newSkeletonId)
without forwarding ownerId / requestingUserId, so newly-created bases will not record the per-user state you introduced. Pass both IDs for consistency.

- tracingStoreClient.saveSkeletonTracing(skeleton, newSkeletonId)
+ tracingStoreClient.saveSkeletonTracing(
+   skeleton,
+   newSkeletonId,
+   ownerId        = baseAnnotation._user,
+   requestingUserId = requestingUserId
+ )

168-204: 🛠️ Refactor suggestion

Analogous omission for volume tracing creation

The createVolumeTracingBase path omits the new requestingUserId when persisting:

tracingStoreClient.saveVolumeTracing(newAnnotationId,
                                     newVolumeTracingId,
                                     _,
                                     magRestrictions = magRestrictions,
                                     dataSource = dataSource)

Add the parameter so that freshly created volume tracings carry the correct initial user state.

🧹 Nitpick comments (19)
webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/AnnotationUpdateActions.scala (1)

3-3: Import the geometry types instead of fully-qualifying

You already import AdditionalCoordinate; importing Vec3Int / Vec3Double keeps the signature concise:

import com.scalableminds.util.geometry.{Vec3Int, Vec3Double}

case class UpdateCameraAnnotationAction(
  editPosition: Vec3Int,
  editRotation: Vec3Double,
  ...
)

Purely cosmetic, but improves readability across the file.

Also applies to: 124-128

webknossos-datastore/proto/VolumeTracing.proto (1)

21-30: Consider documenting the parallel array relationship.

The VolumeUserStateProto message uses parallel arrays (e.g., segmentGroupIds with segmentGroupExpandedStates). While this is a valid pattern, it requires careful handling to maintain synchronization between related arrays.

Consider adding a comment to document this relationship:

message VolumeUserStateProto {
  required string userId = 1;
  optional int64 activeSegmentId = 2;
  // The following pairs of fields are parallel arrays - indices must match:
  // segmentGroupIds[i] corresponds to segmentGroupExpandedStates[i]
  repeated int32 segmentGroupIds = 3;
  repeated bool segmentGroupExpandedStates = 4;
  // boundingBoxIds[i] corresponds to boundingBoxVisibilities[i]
  repeated int32 boundingBoxIds = 5;
  repeated bool boundingBoxVisibilities = 6;
  // segmentIds[i] corresponds to segmentVisibilities[i]
  repeated int64 segmentIds = 7;
  repeated bool segmentVisibilities = 8;
}
webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/AnnotationWithTracings.scala (1)

133-159: Consider simplifying the updateCamera implementation.

The implementation correctly handles both update and insert cases. However, it could be made more concise by extracting the common user state creation logic:

def updateCamera(a: UpdateCameraAnnotationAction): AnnotationWithTracings =
  a.actionAuthorId match {
    case None => this
    case Some(actionUserId) =>
-     val userStateAlreadyPresent = annotation.userStates.exists(state => actionUserId == state.userId)
-     if (userStateAlreadyPresent) {
-       this.copy(annotation = annotation.copy(userStates = annotation.userStates.map {
-         case userState if actionUserId == userState.userId =>
-           userState.copy(
-             userId = actionUserId,
-             editPosition = a.editPosition,
-             editRotation = a.editRotation,
-             zoomLevel = a.zoomLevel,
-             editPositionAdditionalCoordinates = AdditionalCoordinate.toProto(a.editPositionAdditionalCoordinates),
-           )
-         case userState => userState
-       }))
-     } else
-       this.copy(
-         annotation = annotation.copy(userStates = annotation.userStates :+ AnnotationUserStateProto(
-           userId = actionUserId,
-           editPosition = a.editPosition,
-           editRotation = a.editRotation,
-           zoomLevel = a.zoomLevel,
-           editPositionAdditionalCoordinates = AdditionalCoordinate.toProto(a.editPositionAdditionalCoordinates),
-         )))
+     val newUserState = AnnotationUserStateProto(
+       userId = actionUserId,
+       editPosition = a.editPosition,
+       editRotation = a.editRotation,
+       zoomLevel = a.zoomLevel,
+       editPositionAdditionalCoordinates = AdditionalCoordinate.toProto(a.editPositionAdditionalCoordinates),
+     )
+     
+     val updatedUserStates = if (annotation.userStates.exists(_.userId == actionUserId)) {
+       annotation.userStates.map {
+         case userState if userState.userId == actionUserId => 
+           userState.copy(
+             editPosition = a.editPosition,
+             editRotation = a.editRotation,
+             zoomLevel = a.zoomLevel,
+             editPositionAdditionalCoordinates = AdditionalCoordinate.toProto(a.editPositionAdditionalCoordinates),
+           )
+         case userState => userState
+       }
+     } else {
+       annotation.userStates :+ newUserState
+     }
+     
+     this.copy(annotation = annotation.copy(userStates = updatedUserStates))
  }

However, the current implementation is functionally correct and readable.

frontend/javascripts/test/sagas/save_saga.spec.ts (1)

359-359: Update comment to reflect the new action type.

The comment still references "updateTracing update actions" but the code now uses updateActiveNode.

-    // two of the updateTracing update actions are removed by compactSaveQueue
+    // two of the updateActiveNode update actions are removed by compactSaveQueue
webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala (1)

449-456: Consider using immutable operations instead of mutable maps.

While the current implementation works, using immutable operations would be more idiomatic and safer in Scala.

Example refactor for one of the methods:

-    val expandedStateMapMutable: mutable.Map[Int, Boolean] =
-      existingUserState.treeGroupIds.zip(existingUserState.treeGroupExpandedStates).to(collection.mutable.Map)
-    groupIds.foreach(expandedStateMapMutable(_) = areExpanded)
-    val (treeGroupIds, expandedStates) = expandedStateMapMutable.unzip
+    val expandedStateMap = existingUserState.treeGroupIds.zip(existingUserState.treeGroupExpandedStates).toMap
+    val updatedMap = expandedStateMap ++ groupIds.map(_ -> areExpanded)
+    val (treeGroupIds, expandedStates) = updatedMap.unzip

Also applies to: 534-541, 586-593, 669-676

frontend/javascripts/viewer/model/sagas/update_actions.ts (1)

344-367: Consider adding @deprecated JSDoc annotation.

While the comment explains this is for legacy reasons, consider adding a proper @deprecated JSDoc annotation for better IDE support and documentation.

 // This action only exists for legacy reasons. Old annotations may have this
 // action in the action log. Don't use it.
+/**
+ * @deprecated This action only exists for legacy reasons. Use updateActiveNode and updateCameraAnnotation instead.
+ */
 function LEGACY_updateSkeletonTracing(
frontend/javascripts/viewer/model_initialization.ts (1)

655-720: Excellent backward compatibility with clear priority hierarchy.

The implementation correctly prioritizes user state while maintaining backward compatibility for annotations created before the feature deployment. The comment clearly explains the fallback behavior.

Consider extracting the repeated pattern of checking userState and someTracing into a helper function to reduce code duplication:

function getStateValue<T>(
  userStateValue: T | undefined,
  someTracingValue: T | undefined,
  userState: APIAnnotationUserState | null | undefined,
  someTracing: ServerTracing | undefined
): T | undefined {
  if (userState != null) {
    return userStateValue;
  } else if (someTracing != null) {
    return someTracingValue;
  }
  return undefined;
}
app/models/annotation/nml/NmlWriter.scala (1)

115-124: Fix indentation for better readability.

The method parameters should be consistently indented.

-          parameters <- extractTracingParameters(
-            skeletonLayers,
-            volumeLayers,
-            annotation: Option[Annotation],
-            organizationId,
-            wkUrl,
-            datasetName,
-            datasetId,
-            voxelSize
-          )
+          parameters <- extractTracingParameters(
+            skeletonLayers,
+            volumeLayers,
+            annotation,
+            organizationId,
+            wkUrl,
+            datasetName,
+            datasetId,
+            voxelSize
+          )
frontend/javascripts/viewer/view/version_entry.tsx (2)

372-377: Consider using a more descriptive icon.

The function implementation is correct, but the icon choice could be improved.

   updateLargestSegmentId: (action: UpdateLargestSegmentIdVolumeAction): Description => {
     return {
       description: `Set largest segment id to ${action.value.largestSegmentId}`,
-      icon: <EditOutlined />, // todop: better icon?
+      icon: <NumberOutlined />,
     };
   },

378-393: Improve icon choices for expand/collapse actions.

Consider using more intuitive icons for expand/collapse operations.

   updateSegmentGroupsExpandedState: (
     action: UpdateSegmentGroupsExpandedStateUpdateAction,
   ): Description => {
     return {
       description: `${action.value.areExpanded ? "Expanded" : "Collapsed"} some segment groups.`,
-      icon: <EditOutlined />, // todop: better icon?
+      icon: action.value.areExpanded ? <ExpandOutlined /> : <CompressOutlined />,
     };
   },
   updateTreeGroupsExpandedState: (
     action: UpdateSegmentGroupsExpandedStateUpdateAction,
   ): Description => {
     return {
       description: `${action.value.areExpanded ? "Expanded" : "Collapsed"} some tree groups.`,
-      icon: <EditOutlined />, // todop: better icon?
+      icon: action.value.areExpanded ? <ExpandOutlined /> : <CompressOutlined />,
     };
   },

Note: You may need to import ExpandOutlined and CompressOutlined from @ant-design/icons.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/TreeUtils.scala (1)

80-90: Update comment to accurately describe the mapping behavior.

The comment mentions "densifying" but the implementation creates sequential IDs starting from offset + 1, which is more than just densification.

-  // We're densifying the tree ids to avoid sparse ids growing too fast
+  // Create a sequential mapping of tree IDs starting from offset + 1 to avoid sparse IDs
   private def calculateTreeMapping(trees: Seq[Tree], offset: Int): Map[Int, Int] =
webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/TSAnnotationController.scala (1)

192-280: Consider breaking down this complex method for better maintainability.

The mergedFromIds method handles many responsibilities including fetching, adapting, merging, and saving annotations. While the implementation appears correct, consider extracting some of the logic into smaller helper methods for better readability and testability.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala (2)

11-12: Remove unnecessary blank line.

 import scala.collection.mutable
-

439-450: Consider adding a comment explaining the mutable map usage.

While the use of mutable maps here is efficient for updating expanded states, it would benefit from a brief comment explaining the approach.

   override def applyOnUserState(tracing: VolumeTracing,
                                 actionUserId: String,
                                 existingUserStateOpt: Option[VolumeUserStateProto]): VolumeUserStateProto =
     existingUserStateOpt.map { existingUserState =>
+      // Use mutable map for efficient in-place updates of expanded states
       val expandedStateMapMutable: mutable.Map[Int, Boolean] =
         existingUserState.segmentGroupIds.zip(existingUserState.segmentGroupExpandedStates).to(collection.mutable.Map)
       groupIds.foreach(expandedStateMapMutable(_) = areExpanded)
webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/AnnotationUserStateUtils.scala (1)

233-259: Consider performance implications for large-scale merging.

The merge operations create multiple intermediate collections and maps. For annotations with thousands of segments or trees, consider using more efficient data structures or streaming approaches to reduce memory allocation.

For better performance with large datasets, consider:

  1. Using mutable collections during the merge process and converting to immutable at the end
  2. Pre-allocating collection sizes when known
  3. Using specialized primitive collections for ID mappings

Also applies to: 299-327

frontend/javascripts/viewer/model/sagas/save_saga.ts (2)

356-356: Unnecessary Array.from() wrapper

The diffSkeletonTracing and diffVolumeTracing functions already return arrays, so wrapping them with Array.from() is redundant.

-    actions = actions.concat(Array.from(diffSkeletonTracing(prevTracing, tracing)));
+    actions = actions.concat(diffSkeletonTracing(prevTracing, tracing));
-    actions = actions.concat(Array.from(diffVolumeTracing(prevTracing, tracing)));
+    actions = actions.concat(diffVolumeTracing(prevTracing, tracing));

Also applies to: 360-360


405-409: Consider narrowing the action listener scope

The saga listens to SkeletonTracingSaveRelevantActions which includes many skeleton-specific actions that don't affect camera state. This could cause unnecessary diff calculations on every skeleton tracing action.

Consider creating a more specific set of actions that actually affect camera/annotation state to improve performance.

app/models/annotation/AnnotationService.scala (1)

536-553: Consider a more explicit approach for the dummy annotationProto.

While the dummy AnnotationProto works for compound downloads where user state is not needed, this pattern could be made more explicit to improve code clarity.

Consider introducing a companion object method to make the intent clearer:

-            // user state is not used in compound download, so the annotationProto can be a dummy one.
-            annotationProto = AnnotationProto("", 0L, Seq.empty, 0L)
+            // user state is not used in compound download, so the annotationProto can be a dummy one.
+            annotationProto = AnnotationProto.emptyForCompoundDownload

And add to the AnnotationProto companion object:

object AnnotationProto {
  def emptyForCompoundDownload: AnnotationProto = AnnotationProto("", 0L, Seq.empty, 0L)
}
frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx (1)

11-11: Prefer tree-shakable lodash import

import _ from "lodash"; pulls the whole library. Import only the used helper (e.g. import isEqual from "lodash/isEqual";) or switch to lodash-es to keep bundle size small.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a535d3b and 9f72f5b.

⛔ Files ignored due to path filters (1)
  • frontend/javascripts/test/backend-snapshot-tests/__snapshots__/annotations.e2e.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (74)
  • CHANGELOG.unreleased.md (1 hunks)
  • app/controllers/AnnotationController.scala (1 hunks)
  • app/controllers/AnnotationIOController.scala (6 hunks)
  • app/controllers/TaskController.scala (1 hunks)
  • app/models/annotation/AnnotationLayerPrecedence.scala (6 hunks)
  • app/models/annotation/AnnotationMerger.scala (2 hunks)
  • app/models/annotation/AnnotationService.scala (3 hunks)
  • app/models/annotation/WKRemoteTracingStoreClient.scala (6 hunks)
  • app/models/annotation/nml/NmlWriter.scala (7 hunks)
  • app/models/task/TaskCreationService.scala (5 hunks)
  • frontend/javascripts/admin/rest_api.ts (1 hunks)
  • frontend/javascripts/libs/utils.ts (2 hunks)
  • frontend/javascripts/test/backend-snapshot-tests/annotations.e2e.ts (3 hunks)
  • frontend/javascripts/test/fixtures/skeletontracing_server_objects.ts (2 hunks)
  • frontend/javascripts/test/fixtures/tasktracing_server_objects.ts (2 hunks)
  • frontend/javascripts/test/fixtures/volumetracing_server_objects.ts (2 hunks)
  • frontend/javascripts/test/helpers/saveHelpers.ts (1 hunks)
  • frontend/javascripts/test/sagas/compact_toggle_actions.spec.ts (2 hunks)
  • frontend/javascripts/test/sagas/saga_integration.spec.ts (3 hunks)
  • frontend/javascripts/test/sagas/save_saga.spec.ts (3 hunks)
  • frontend/javascripts/test/sagas/skeletontracing_saga.spec.ts (26 hunks)
  • frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts (4 hunks)
  • frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_1.spec.ts (4 hunks)
  • frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_2.spec.ts (4 hunks)
  • frontend/javascripts/types/api_types.ts (3 hunks)
  • frontend/javascripts/viewer/controller/combinations/tool_controls.ts (0 hunks)
  • frontend/javascripts/viewer/model/accessors/annotation_accessor.ts (2 hunks)
  • frontend/javascripts/viewer/model/accessors/tracing_accessor.ts (2 hunks)
  • frontend/javascripts/viewer/model/actions/skeletontracing_actions.tsx (3 hunks)
  • frontend/javascripts/viewer/model/helpers/chunk.ts (1 hunks)
  • frontend/javascripts/viewer/model/helpers/compaction/compact_save_queue.ts (1 hunks)
  • frontend/javascripts/viewer/model/helpers/diff_helpers.ts (1 hunks)
  • frontend/javascripts/viewer/model/reducers/reducer_helpers.ts (9 hunks)
  • frontend/javascripts/viewer/model/reducers/save_reducer.ts (1 hunks)
  • frontend/javascripts/viewer/model/reducers/skeletontracing_reducer.ts (5 hunks)
  • frontend/javascripts/viewer/model/reducers/skeletontracing_reducer_helpers.ts (1 hunks)
  • frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts (6 hunks)
  • frontend/javascripts/viewer/model/sagas/save_saga.ts (7 hunks)
  • frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts (2 hunks)
  • frontend/javascripts/viewer/model/sagas/split_boundary_mesh_saga.ts (1 hunks)
  • frontend/javascripts/viewer/model/sagas/undo_saga.ts (2 hunks)
  • frontend/javascripts/viewer/model/sagas/update_actions.ts (12 hunks)
  • frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx (3 hunks)
  • frontend/javascripts/viewer/model/types/tree_types.ts (1 hunks)
  • frontend/javascripts/viewer/model_initialization.ts (9 hunks)
  • frontend/javascripts/viewer/store.ts (1 hunks)
  • frontend/javascripts/viewer/view/action-bar/tracing_actions_view.tsx (2 hunks)
  • frontend/javascripts/viewer/view/jobs/train_ai_model.tsx (2 hunks)
  • frontend/javascripts/viewer/view/version_entry.tsx (3 hunks)
  • test/backend/AnnotationUserStateTestSuite.scala (1 hunks)
  • test/backend/NMLUnitTestSuite.scala (4 hunks)
  • webknossos-datastore/app/com/scalableminds/webknossos/datastore/helpers/SkeletonElementDefaults.scala (2 hunks)
  • webknossos-datastore/proto/Annotation.proto (2 hunks)
  • webknossos-datastore/proto/SkeletonTracing.proto (2 hunks)
  • webknossos-datastore/proto/VolumeTracing.proto (2 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/AnnotationUpdateActions.scala (3 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/AnnotationWithTracings.scala (3 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/TSAnnotationService.scala (10 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/UpdateActions.scala (8 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/SkeletonTracingController.scala (2 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/TSAnnotationController.scala (7 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingController.scala (2 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/AnnotationUserStateUtils.scala (1 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/BoundingBoxMerger.scala (2 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/GroupUtils.scala (2 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingMergeService.scala (2 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/SkeletonTracingService.scala (4 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/TreeUtils.scala (2 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActionHelper.scala (0 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala (7 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingDefaults.scala (2 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala (6 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala (8 hunks)
  • webknossos-tracingstore/conf/com.scalableminds.webknossos.tracingstore.routes (3 hunks)
💤 Files with no reviewable changes (2)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActionHelper.scala
  • frontend/javascripts/viewer/controller/combinations/tool_controls.ts
🧰 Additional context used
🪛 Buf (1.54.0)
webknossos-datastore/proto/Annotation.proto

2-2: import "geometry.proto": file does not exist

(COMPILE)

🔇 Additional comments (122)
test/backend/NMLUnitTestSuite.scala (2)

5-6: LGTM! Imports align with the new user state management feature.

The added imports support the new User and AnnotationProto parameters required for the per-user state management functionality.

Also applies to: 15-15, 20-20, 22-23


44-71: Changes correctly adapt the test to the new user state management API.

The modifications appropriately update the test to include the required AnnotationProto and User context. The dummy values used are suitable for unit testing the NML serialization/parsing logic.

Note that requestingUser is set to None, which appears to test the fallback behavior where the annotation owner's state is used.

Please ensure that comprehensive tests for the actual per-user state functionality (camera positions, visibility toggles, expanded/collapsed states) are included in other test files, as this unit test only verifies NML parsing with minimal dummy values.

test/backend/AnnotationUserStateTestSuite.scala (4)

1-8: LGTM!

The package declaration, imports, and class declaration follow standard conventions for a Play framework test suite.


10-29: LGTM!

The test data setup provides good coverage with different user states for testing various scenarios.


31-41: LGTM!

The test correctly verifies the rendering of skeleton user state with fallback behavior for sparse user states.


63-102: LGTM!

The volume user states merging test is well-structured and correctly verifies ID mapping and group transformation logic.

frontend/javascripts/viewer/model/helpers/chunk.ts (1)

2-2: LGTM! Formatting improvements.

The whitespace additions improve code readability without affecting functionality.

Also applies to: 6-6

frontend/javascripts/viewer/model/types/tree_types.ts (1)

80-82: Well-documented backward compatibility support.

The addition of the optional isExpanded property with clear documentation about its purpose for legacy groups is excellent. This ensures smooth migration to the new user-specific state management system while maintaining compatibility with existing data.

frontend/javascripts/admin/rest_api.ts (1)

745-745: Enhanced debugging information.

Adding the tracing type to the console log improves debugging capabilities for protobuf parsing, which is valuable for troubleshooting the new user state functionality.

frontend/javascripts/test/fixtures/volumetracing_server_objects.ts (2)

74-74: Proper test fixture update for user states.

Adding the userStates: [] property to the ServerVolumeTracing test fixture correctly aligns with the new user state management functionality introduced in this PR.


139-139: Consistent fixture updates for annotation proto.

The addition of userStates: [] to the APITracingStoreAnnotation fixture maintains consistency with the user state management changes and ensures test compatibility.

CHANGELOG.unreleased.md (1)

21-21: Well-written changelog entry!

The description clearly explains the new per-user state management feature, including what properties are stored per user and the fallback behavior. This will help users understand the new functionality.

frontend/javascripts/libs/utils.ts (2)

1115-1139: Well-implemented set difference utility!

The diffSets function efficiently compares two sets by iterating through each set once and categorizing elements correctly. The implementation follows good performance practices and the return structure is clear and useful.


1338-1346: Excellent recursive mapping utility!

The mapGroupsDeep function correctly implements depth-first traversal by recursively processing children before applying the mapping function to the parent. This is exactly what's needed for transforming hierarchical group structures while preserving the tree structure.

frontend/javascripts/viewer/store.ts (1)

114-116: Good use of type aliases for semantic clarity!

These type aliases improve code readability by providing segment-specific names for the existing tree group types. Since segment groups and tree groups share the same structure, this approach maintains type safety while making the code more semantically meaningful.

frontend/javascripts/test/fixtures/skeletontracing_server_objects.ts (2)

165-165: Appropriate test fixture update!

Adding the userStates property as an empty array to the test fixture ensures compatibility with the new schema while maintaining focus on testing the core functionality rather than user state specifics.


233-233: Consistent test fixture update!

The addition of userStates as an empty array maintains consistency with the updated APITracingStoreAnnotation type and ensures the test fixture remains valid.

frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_1.spec.ts (1)

16-16: LGTM! Improved test setup and cleanup.

The changes enhance test reliability:

  • Using resetStoreAction() provides a more comprehensive store reset
  • Adding tearDownPullQueues() ensures proper cleanup of resources

Also applies to: 44-44, 63-63

frontend/javascripts/viewer/model/helpers/compaction/compact_save_queue.ts (1)

7-22: Consistent with per-user state management approach.

The changes from generic tracing actions to specific user-state actions (updateActiveNode, updateActiveSegmentId, updateCamera) align well with the PR's objective of tracking individual user modifications separately.

frontend/javascripts/test/helpers/saveHelpers.ts (1)

24-33: Test helper updated consistently with save queue changes.

The filter now correctly excludes the new user-specific action types, maintaining consistency with the changes in compact_save_queue.ts.

webknossos-datastore/proto/Annotation.proto (2)

18-18: LGTM! Well-structured user state implementation.

The AnnotationUserStateProto message and its integration into AnnotationProto are properly designed. The field types appropriately capture user-specific view state including position, rotation, zoom level, and additional coordinates.

Also applies to: 27-33


2-2:

✅ Verification successful

Verify that geometry.proto exists in the build environment.

The static analysis tool indicates that geometry.proto may not exist. Please ensure this file is present in your protobuf compilation path.

Run the following script to verify the existence of the geometry.proto file:


🏁 Script executed:

#!/bin/bash
# Description: Check if geometry.proto exists in the repository

# Search for geometry.proto file
fd -t f "geometry.proto"

# Also check for references to the types that should be defined in geometry.proto
rg -A 2 -B 2 "Vec3IntProto|Vec3DoubleProto|AdditionalCoordinateProto" --glob "*.proto"

Length of output: 7466


Confirmed: geometry.proto import is valid

  • geometry.proto exists at webknossos-datastore/proto/geometry.proto
  • Defines Vec3IntProto, Vec3DoubleProto, AdditionalCoordinateProto, etc.
  • The import in Annotation.proto correctly references the file in the same directory

No further changes required.

🧰 Tools
🪛 Buf (1.54.0)

2-2: import "geometry.proto": file does not exist

(COMPILE)

frontend/javascripts/viewer/view/jobs/train_ai_model.tsx (1)

722-722: LGTM! Function signatures updated for user state support.

The additional parameters (null/undefined) maintain backward compatibility while enabling the new user state management feature.

Also applies to: 734-734

frontend/javascripts/test/sagas/compact_toggle_actions.spec.ts (1)

2-2: LGTM! Test simplification improves clarity.

The removal of the unused Flycam mock and its parameters from diffVolumeTracing calls streamlines the tests appropriately.

Also applies to: 184-184

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingDefaults.scala (1)

3-3: LGTM! Clean implementation of empty user state factory method.

The emptyUserState method provides a clear and consistent way to create default user state objects. All fields are properly initialized with empty/None values.

Also applies to: 18-27

frontend/javascripts/viewer/model/reducers/save_reducer.ts (1)

33-36: Good defensive programming! Prevents empty transactions.

The early return with warning message helps catch potential issues where empty save transactions might be dispatched, avoiding unnecessary state updates and queue modifications.

webknossos-datastore/app/com/scalableminds/webknossos/datastore/helpers/SkeletonElementDefaults.scala (1)

30-40: LGTM! Clean implementation of the empty user state factory method.

The emptyUserState method follows the established pattern in this file and provides a clean way to initialize user-specific skeleton state with sensible defaults.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/SkeletonTracingController.scala (1)

119-143: LGTM! Appropriate addition of ownership parameters for duplication.

The addition of ownerId and requestingUserId parameters aligns well with the PR's objective of managing per-user state. These parameters are correctly propagated to the service layer.

frontend/javascripts/viewer/model/accessors/tracing_accessor.ts (1)

2-2:

✅ Verification successful

Verify that getSomeServerTracing is no longer used in the codebase.

The removal of the ServerTracing import and getSomeServerTracing function appears to be part of the refactoring. Please ensure this function is not referenced elsewhere.

Also applies to: 31-31


🏁 Script executed:

#!/bin/bash
# Description: Search for any remaining references to getSomeServerTracing function

# Test: Search for the function usage. Expect: No occurrences found.
rg -A 2 "getSomeServerTracing" --type ts --type tsx --type js --type jsx

Length of output: 100


🏁 Script executed:

#!/bin/bash
# Description: Re-run search for any remaining references to getSomeServerTracing across the entire repo,
# without restricting by file type to avoid the “unrecognized file type” error.
rg -n "getSomeServerTracing" || grep -R -n "getSomeServerTracing" .

Length of output: 64


All references to getSomeServerTracing have been removed
A repository‐wide search returned no occurrences of getSomeServerTracing, so it’s safe to drop the import and function.

frontend/javascripts/test/sagas/skeletontracing_saga.spec.ts (2)

35-44: Nice clean-up – fewer test parameters

Refactoring testDiffing to accept only the annotation pair simplifies every call site and keeps focus on the diff logic. 👍


100-107: Confirm type update for userStates in ServerSkeletonTracing

serverSkeletonTracing now includes userStates: [], but the imported ServerSkeletonTracing type must expose this field (and its nested shape) or the test will fail to compile once strict checks are enabled.

Please verify that types/api_types.ts (or equivalent) was updated; otherwise add the property there.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/BoundingBoxMerger.scala (3)

8-11: Well-structured enum for tracking bounding box sources.

The BoundingBoxSource enum provides clear categorization of bounding box origins during the merge process, which is essential for correct ID mapping.


15-15: Good use of type alias and enhanced return type.

The UserBboxIdMap type alias improves code readability, and returning the ID maps alongside the merged bounding boxes enables proper tracking of ID transformations during the merge process.

Also applies to: 31-31


33-54: Robust implementation of bounding box merging with ID tracking.

The implementation correctly:

  • Handles deprecated single bounding boxes by converting them to named versions
  • Ensures uniqueness while preserving source information
  • Generates consistent new IDs using index-based assignment
  • Creates accurate ID mappings for both sources

The use of distinctBy with copy(id = 0) ensures proper deduplication based on content rather than ID.

webknossos-datastore/proto/VolumeTracing.proto (1)

72-72: Proper addition of user states field.

The userStates field is correctly added as a repeated field with a non-conflicting field number, enabling per-user state management for volume tracings.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/AnnotationWithTracings.scala (1)

3-7: Appropriate imports and trait extension for user state functionality.

The added imports and ProtoGeometryImplicits trait extension provide the necessary types and implicit conversions for handling user-specific camera state.

Also applies to: 12-13, 32-33

frontend/javascripts/test/sagas/save_saga.spec.ts (2)

74-87: LGTM! The granular update actions improve state management.

The refactoring from monolithic updateSkeletonTracing to specific update actions (updateActiveNode, updateActiveSegmentId, updateCameraAnnotation) aligns well with the PR's objective of per-user state management. This allows for more precise control over which state updates are compacted and preserved.


295-302: Consistent refactoring to use specific update actions.

The change from updateSkeletonTracing to updateActiveNode maintains the test's original purpose while aligning with the new granular update action pattern.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingMergeService.scala (2)

40-45: Good practice: Using Seq instead of List for method parameters.

The change from List to Seq improves API flexibility and follows Scala best practices. Seq is more general and allows callers to pass various collection types without conversion.


101-102: Consistent parameter type update.

The private method signature is correctly updated to match the public method's parameter type change.

frontend/javascripts/viewer/model/reducers/skeletontracing_reducer_helpers.ts (2)

879-909: Well-implemented user state transformation support.

The optional applyUserStateToTreeFn parameter elegantly enables per-user tree state customization while maintaining backward compatibility. The default identity function ensures existing code continues to work unchanged.


915-923: Consistent parameter propagation to immutable version.

The immutable sibling function correctly accepts and passes through the applyUserStateToTreeFn parameter, maintaining API consistency.

frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts (2)

55-59: LGTM! Appropriate initialization of new user state fields.

The new fields are initialized with sensible defaults that align with the PR's user state management feature.


73-73: LGTM! Correct usage of null parameters for user state.

The null parameters correctly represent the absence of existing user state and user ID in the test context.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/UpdateActions.scala (3)

25-25: LGTM! Well-designed trait for user state updates.

The UserStateUpdateAction trait appropriately extends UpdateAction to support user-specific state modifications.


42-62: LGTM! Comprehensive deserialization support for new user state actions.

The pattern matching correctly handles all new user state update actions with proper deserialization.

Also applies to: 79-85, 99-99


140-146: LGTM! Symmetric serialization implementation.

The serialization logic properly mirrors the deserialization, ensuring round-trip compatibility for all new user state actions.

Also applies to: 168-173, 194-196, 234-235

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala (2)

26-45: LGTM! Well-structured trait for user-specific skeleton updates.

The UserStateSkeletonUpdateAction trait properly combines skeleton and user state update functionality. The applyOn method correctly handles both creation of new user states and updates to existing ones.


731-756: LGTM! Proper JSON format definitions for new actions.

All new action classes have appropriate JSON format implicits defined.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/SkeletonTracingService.scala (4)

22-22: LGTM!

The addition of the AnnotationUserStateUtils trait is appropriate for handling user-specific state management functionality.


57-84: Good implementation of user state handling for duplication.

The method correctly creates a user state for the requesting user using renderSkeletonUserStateIntoUserState, which should handle the fallback logic to the owner's state as described in the PR objectives. The integration into the duplicated tracing is clean and follows the existing pattern.


99-136: Comprehensive and correct implementation of user state merging.

The merge logic properly handles all necessary mappings (node, tree, group, and bounding box) before merging user states. This ensures that user states are correctly updated with remapped IDs during the merge process, maintaining consistency across the merged annotation.


139-139: Helpful clarification comment.

Good addition of context explaining why user state mapping is not needed in remapTooLargeTreeIds. This helps future maintainers understand the scope of this method.

frontend/javascripts/viewer/model/helpers/diff_helpers.ts (4)

6-12: Clean recursive implementation for stripping UI state.

The function correctly creates shallow copies while preserving the tree structure. Good use of recursion for handling nested groups.


14-30: Efficient recursive state gathering.

Good use of a mutable accumulator pattern for performance. The function correctly categorizes all groups by their expansion state while traversing the tree structure.


32-52: Well-structured diff function with good performance characteristics.

Excellent optimization with reference equality check. The separation of content changes from state changes is a clean design that allows consumers to handle different types of changes appropriately.


54-94: Consistent implementation for bounding box diffing.

Good use of object destructuring to strip properties. The implementation maintains consistency with the group diffing pattern while appropriately handling the simpler, non-nested structure of bounding boxes.

webknossos-tracingstore/conf/com.scalableminds.webknossos.tracingstore.routes (1)

13-15: Consistent addition of user identification parameters.

The route changes correctly add ownerId and requestingUserId parameters to support per-user state management. These align with the controller method signatures and the PR objectives.

Note that the mergedFromIds route (line 15) only adds requestingUserId without ownerId. Please verify this is intentional - perhaps merged annotations determine ownership differently?

Also applies to: 26-26, 74-74

frontend/javascripts/viewer/model/sagas/update_actions.ts (12)

24-29: Good refactoring approach with backward compatibility.

The renaming of update actions to include the LEGACY_ prefix and introduction of granular actions is a clean approach that maintains backward compatibility while moving towards a more modular architecture.


45-50: LGTM! Consistent naming for user-specific visibility actions.

The new action types for user bounding box visibility follow the established naming pattern and support the per-user state feature.


369-380: Clean implementation of granular update action.

The updateActiveNode function follows the single responsibility principle by only updating the active node state.


423-425: LGTM! Proper null handling for segment ID.

The function correctly handles both number and null values for largestSegmentId.


450-483: Consistent implementation for user bounding box visibility.

Both skeleton and volume tracing visibility update functions follow the same pattern and are properly typed.


595-624: Efficient bulk update for group expansion states.

The functions efficiently handle multiple group IDs in a single action, which is good for performance when toggling multiple groups.


665-680: Well-structured camera state update action.

The updateCameraAnnotation function properly encapsulates all camera-related state updates in a single action.


24-29: LGTM! Good separation of concerns with legacy actions.

The renaming of comprehensive update actions to LEGACY_ prefix and introduction of granular actions (UpdateActiveNodeUpdateAction, UpdateActiveSegmentIdUpdateAction, etc.) aligns well with the PR objective of supporting per-user state management. This approach maintains backward compatibility while enabling more targeted state updates.


344-380: Well-structured refactoring for per-user state management.

The separation of the monolithic updateSkeletonTracing into focused actions like updateActiveNode is excellent. This granular approach supports the PR's goal of managing user-specific states independently, allowing different users to have their own active nodes without affecting camera positions or other properties.


450-483: Consistent implementation for bounding box visibility controls.

The new functions for updating bounding box visibility in both skeleton and volume tracings follow a consistent pattern and enable per-user visibility states as intended by the PR objectives.


595-623: Efficient bulk update approach for group expansion states.

The implementation allows updating multiple group expansion states in a single action, which is efficient for UI operations. This supports the per-user state requirement where each user can have their own view of expanded/collapsed groups.


665-680: Clean separation of camera state from tracing state.

The dedicated updateCameraAnnotation function properly isolates camera-related updates (position, rotation, zoom) from tracing data, supporting the per-user view state management objective.

frontend/javascripts/viewer/model/reducers/reducer_helpers.ts (4)

49-68: Robust implementation with proper fallback handling.

The function correctly handles user-specific visibility states with a logical fallback chain: user state → bounding box visibility → default true.


70-88: Clean conversion functions with proper structure.

Both conversion functions correctly transform between frontend and server bounding box formats.


239-258: Well-designed higher-order function for tree visibility.

The function correctly returns a transformation function that applies user-specific visibility states to trees, maintaining consistency with the group expansion implementation.


212-237:

✅ Verification successful

Verify the availability of mapGroupsDeep utility function.

The implementation correctly handles user-specific group expansion states for both skeleton and volume tracings. The use of a lookup object is efficient.

Verify that Utils.mapGroupsDeep is properly implemented:


🏁 Script executed:

#!/bin/bash
# Description: Verify the implementation of mapGroupsDeep utility function

# Search for mapGroupsDeep implementation
ast-grep --pattern 'export function mapGroupsDeep'

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Search for any reference or definition of mapGroupsDeep in the codebase
rg 'mapGroupsDeep' -n

Length of output: 366


mapGroupsDeep utility function confirmed

  • Located in frontend/javascripts/libs/utils.ts (around line 1338), providing the expected recursive mapping behavior.
  • No further action needed—implementation is present and correct.
frontend/javascripts/viewer/model_initialization.ts (2)

145-145: Proper initialization of user state with null safety.

The user state is correctly obtained when annotation proto is available, with appropriate null checking.

Also applies to: 159-162


242-244: Correct placement of zoom level initialization.

Setting the zoom level from user state during data layer initialization ensures the view is properly configured before rendering.

app/models/annotation/nml/NmlWriter.scala (3)

44-47: LGTM!

The addition of the AnnotationUserStateUtils trait to the NmlWriter class is appropriate for implementing user-specific state rendering functionality.


70-74: Good implementation of user state rendering.

The implementation correctly applies user-specific states with a fallback to the annotation owner's state, which aligns with the PR objectives.


431-454: Good improvement to metadata consistency.

The change from optional to required annotationOwner parameter ensures that the username metadata is always present and accurate, improving NML file consistency.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/TreeUtils.scala (1)

71-90: Good refactoring to improve code clarity.

The separation of mapping calculation (calculateTreeMappings) from mapping application (mergeTrees) improves code clarity and makes the ID remapping logic more explicit and testable.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/TSAnnotationService.scala (3)

838-882: LGTM!

The ownerId and requestingUserId parameters are correctly propagated to both volume and skeleton tracing duplication methods based on the layer type.


884-925: LGTM!

The volume tracing duplication correctly passes the ownerId and requestingUserId parameters to the adaptation method, maintaining the proper context for user-specific state management.


942-965: LGTM!

The skeleton tracing duplication correctly propagates the ownerId and requestingUserId parameters, maintaining consistency with the volume tracing implementation.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/TSAnnotationController.scala (2)

158-173: LGTM!

The helper method correctly filters volume layers, fetches the corresponding tracings, and adapts them with user-specific states using the appropriate rendering method.


116-140: LGTM!

The duplicate method correctly propagates the new ownerId and requestingUserId parameters to the annotation service.

frontend/javascripts/types/api_types.ts (2)

606-613: LGTM!

The APIAnnotationUserState type properly captures user-specific view state including position, rotation, zoom level, and support for additional coordinates.


908-914: LGTM!

The fallback properties in ServerTracingBase are well-documented with clear comments indicating they should only be used when the userStates array is empty, ensuring proper backward compatibility.

app/models/annotation/WKRemoteTracingStoreClient.scala (3)

103-107: LGTM! Clean implementation of annotation proto retrieval.

The new method follows the established pattern for RPC calls with proper token authentication and optional version support.


110-127: Good consistency in adding user context parameters.

The addition of ownerId and requestingUserId parameters across duplication methods is implemented consistently, with all methods following the same pattern of adding these as query parameters.

Also applies to: 130-147, 150-172


175-188:

✅ Verification successful

Verify the order of ownerIds matches annotationIds.

The method now accepts a list of ownerIds that should correspond to the annotationIds list. Please ensure that the calling code maintains the correct order alignment between these two lists to avoid mismatched ownership assignments.

Run the following script to verify the usage of this method:


🏁 Script executed:

#!/bin/bash
# Description: Check how mergeAnnotationsByIds is called to ensure ownerIds and annotationIds are properly aligned

# Search for calls to mergeAnnotationsByIds
rg -A 10 "mergeAnnotationsByIds\(" --type scala

Length of output: 2391


mergeAnnotationsByIds invocation order verified

Both annotationIds and ownerIds are derived from the same annotations list in AnnotationMerger.scala, preserving their alignment:

  • File: app/models/annotation/AnnotationMerger.scala
    Call site:
    tracingStoreClient.mergeAnnotationsByIds(
      annotations.map(_.id),
      annotations.map(_._user),
      newAnnotationId,
      toTemporaryStore,
      requestingUserId
    )

No further changes are needed here.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala (2)

40-59: Well-designed trait for user-specific update actions.

The UserStateVolumeUpdateAction trait correctly handles the creation and update of user-specific states, with proper handling of the case where no author ID is present (view-only changes).


425-459:

✅ Verification successful

Verify action naming consistency.

The AI summary mentions both UpdateSegmentGroupsExpandedStateVolumeAction and UpdateSegmentGroupExpandedStateVolumeAction, but only the former (plural "Groups") is defined in the code. Please verify this is intentional and update any references to use the consistent plural form.


🏁 Script executed:

#!/bin/bash
# Description: Check for any references to the singular form of the action name

# Search for references to the potentially incorrect singular form
rg "UpdateSegmentGroupExpandedStateVolumeAction" --type scala

# Also check for the correct plural form to compare usage
rg "UpdateSegmentGroupsExpandedStateVolumeAction" --type scala

Length of output: 1395


Naming consistency verified – only the plural form is used throughout
All references to this action use UpdateSegmentGroupsExpandedStateVolumeAction; there are no occurrences of the singular form. No changes needed.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/AnnotationUserStateUtils.scala (2)

14-23: Clean implementation of user state rendering.

The method correctly applies user-specific states to annotation layers with proper fallback logic.


25-30: Correct priority order for user state selection.

The fallback from requesting user to owner provides the expected behavior for shared annotations.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/GroupUtils.scala (2)

20-67: Good encapsulation improvements!

Making these helper methods private properly hides the internal implementation details while maintaining the public API through the type-specific methods below.


63-67:

✅ Verification successful

Verify the removal of the default parameter doesn't break existing code.

The removal of the default parameter ids: Seq[Int] = Seq[Int]() from getAllGroupIds could potentially break existing code that relied on calling this method without the second parameter.

Run the following script to check if any code calls getAllGroupIds without the second parameter:


🏁 Script executed:

#!/bin/bash
# Description: Search for getAllGroupIds calls that might be affected by the removal of the default parameter

# Test: Search for getAllGroupIds calls. Expect: All calls should have two parameters.
rg -A 3 'getAllGroupIds\s*\(' --type scala

Length of output: 1389


No invocations rely on the removed default parameter
A repo-wide search for getAllGroupIds only found its definition and the internal recursive call—both explicitly pass two arguments. As this helper is private and has no external usages, removing the default ids parameter introduces no breakage.

app/controllers/TaskController.scala (1)

67-71:

✅ Verification successful

LGTM! User ID properly passed for user-specific state management.

The addition of request.identity._id aligns with the PR's objective of implementing per-user state management for annotations.

Run the following script to verify the method signature update is consistent across the codebase:


🏁 Script executed:

#!/bin/bash
# Description: Verify that createTracingsFromBaseAnnotations method signature is updated consistently

# Test: Search for the method definition. Expect: The method should accept the user ID parameter.
ast-grep --pattern 'def createTracingsFromBaseAnnotations($$$)'

Length of output: 65


Let’s locate the method definition to confirm its parameter list:


🏁 Script executed:

#!/bin/bash
set -e
# Search for the method declaration to verify it includes the user ID parameter
rg -n "def createTracingsFromBaseAnnotations" .

Length of output: 146


Inspecting the full method signature to confirm the added userId parameter:


🏁 Script executed:

#!/bin/bash
set -e
# Show method signature with surrounding lines
rg -n -A 5 -B 2 "def createTracingsFromBaseAnnotations" app/models/task/TaskCreationService.scala

Length of output: 577


LGTM – Service signature updated correctly.

The call in TaskController.scala now passes request.identity._id, which matches the requestingUserId: ObjectId parameter in the method definition found in app/models/task/TaskCreationService.scala (lines 67–72). This aligns with the PR’s per-user state management goal.

frontend/javascripts/viewer/model/sagas/split_boundary_mesh_saga.ts (1)

111-111:

✅ Verification successful

Verify the action type rename is complete across the codebase.

The change from "SET_TRACING" to "SET_SKELETON_TRACING" looks good, but we should ensure this rename is consistent throughout the codebase.

Run the following script to verify the action type rename is complete:


🏁 Script executed:

#!/bin/bash
# Description: Verify that SET_TRACING is no longer used and SET_SKELETON_TRACING is properly defined

# Test: Search for any remaining usage of SET_TRACING action. Expect: No occurrences.
rg '"SET_TRACING"' --type ts --type tsx --type js --type jsx

# Test: Verify SET_SKELETON_TRACING is defined and used. Expect: At least one definition and usage.
rg '"SET_SKELETON_TRACING"' --type ts --type tsx --type js --type jsx -A 2

Length of output: 195


🏁 Script executed:

#!/bin/bash
# Retry search using file globs since tsx wasn't recognized as a type

# Ensure no remaining usages of SET_TRACING
rg '"SET_TRACING"' --glob '*.ts' --glob '*.tsx' --glob '*.js' --glob '*.jsx'

# Verify definition and usage of SET_SKELETON_TRACING with context
rg '"SET_SKELETON_TRACING"' --glob '*.ts' --glob '*.tsx' --glob '*.js' --glob '*.jsx' -C 2

Length of output: 2067


Action rename verified: "SET_TRACING" removed, "SET_SKELETON_TRACING" properly defined and used.

  • No occurrences of "SET_TRACING" remain in .ts/.tsx/.js/.jsx files.
  • "SET_SKELETON_TRACING" is defined in viewer/model/actions/skeletontracing_actions.tsx.
  • It’s referenced in split_boundary_mesh_saga.ts, save_saga.ts, and skeletontracing_reducer.ts.

All usages have been updated accordingly.

frontend/javascripts/viewer/model/sagas/undo_saga.ts (1)

24-25:

✅ Verification successful

Action rename looks consistent – double-check for stragglers

The import and dispatch were updated to setSkeletonTracingAction.
Please run a quick grep to ensure no remaining setTracingAction(...) usages linger in the codebase to avoid dead code and broken builds.

Also applies to: 589-590


🏁 Script executed:

#!/bin/bash
rg --no-heading -n "setTracingAction(" || echo "No leftover occurrences."

Length of output: 184


🏁 Script executed:

#!/bin/bash
# Search for any remaining references to `setTracingAction`
rg --no-heading -n 'setTracingAction' || echo "No leftover occurrences of setTracingAction."

Length of output: 136


All setTracingAction references replaced
No leftover occurrences of setTracingAction found in the codebase—rename to setSkeletonTracingAction is complete.

frontend/javascripts/viewer/view/action-bar/tracing_actions_view.tsx (1)

58-59: Import/dispatch rename OK

setSkeletonTracingAction replaces the old name and the payload structure stays untouched — looks good.

Also applies to: 568-573

frontend/javascripts/test/backend-snapshot-tests/annotations.e2e.ts (1)

187-219: LGTM! Comprehensive test coverage for user state functionality.

The test properly verifies that user-specific states are correctly stored and retrieved:

  • Active node ID in skeleton user states
  • Camera position, rotation, and zoom level in annotation user states
frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration_2.spec.ts (1)

16-16: Good test cleanup improvements.

The changes enhance test isolation by:

  • Using resetStoreAction() for more comprehensive store reset
  • Adding tearDownPullQueues() to properly clean up async operations

These changes are consistent with the improvements made to volumetracing_saga_integration_1.spec.ts.

Also applies to: 41-41, 60-60

frontend/javascripts/viewer/model/accessors/annotation_accessor.ts (1)

96-119: Well-implemented user state retrieval with proper fallback logic.

The getUserStateForTracing function correctly implements the fallback hierarchy as described in the PR objectives:

  1. First attempts to find the active user's state
  2. Falls back to the owner's state if not found
  3. Returns undefined if neither exists

The generic implementation allows reuse across different user state types.

frontend/javascripts/test/sagas/saga_integration.spec.ts (2)

101-101: Good use of numeric separators.

Using underscores in numeric literals improves readability.


115-117: Verify the expected chunking behavior change.

The test now expects 2 chunks instead of 1 after deleting a node. This appears to be an intentional change, possibly due to additional user state data or modified chunking logic. Please confirm this behavioral change is expected and consider adding a comment explaining why the save queue now produces 2 chunks.

frontend/javascripts/test/fixtures/tasktracing_server_objects.ts (1)

54-56: Test fixture updates align with per-user state management.

The addition of userStates arrays and other property changes correctly reflect the new data model for storing individual user preferences. The empty arrays are appropriate for test fixtures representing initial states.

Also applies to: 196-196

app/models/annotation/AnnotationMerger.scala (1)

39-39: User ID propagation for annotation merging looks good.

The addition of requestingUserId and user ID mapping properly supports per-user state management during annotation merging. The parameters are consistently propagated through the call chain, enabling the tracing store to handle user-specific states correctly.

Also applies to: 49-54, 72-72, 78-81

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala (3)

527-539: LGTM! Method signature correctly extended for user state support.

The addition of ownerId and requestingUserId parameters aligns with the PR objective of implementing per-user state management for annotations.


545-545: User state handling is correctly implemented.

The user state is properly generated using renderVolumeUserStateIntoUserState and included in the duplicated tracing, ensuring that user-specific view states are preserved during duplication.

Also applies to: 556-557


738-781: Comprehensive user state merging implementation.

The merging logic correctly handles:

  • Group mapping calculation and segment group merging
  • Segment ID remapping from mergedVolumeStats.labelMaps
  • User bounding box combination with ID mapping
  • User state merging with proper remapping of groups, segments, and bounding boxes

The implementation ensures that user-specific states are properly preserved and remapped during annotation merging.

app/controllers/AnnotationIOController.scala (1)

416-416: Consistent parameter naming improvement.

The rename from issuingUser to requestingUser better reflects the semantic meaning - this represents the user requesting the download operation, making the code more intuitive.

Also applies to: 428-429, 446-447, 475-476, 491-492, 529-530, 535-536

frontend/javascripts/viewer/model/reducers/skeletontracing_reducer.ts (2)

60-70: User state integration is well-implemented.

The initialization correctly:

  • Retrieves user-specific state based on the active user and annotation owner
  • Creates a transformation function to apply user state to trees
  • Applies user state to tree groups and bounding boxes
  • Sets the active node from user state when available

This ensures that each user's view preferences are properly restored when loading the annotation.

Also applies to: 110-111


467-475: Simplified state update improves code clarity.

The direct assignment $set: action.tracing is cleaner than the previous nested update approach and achieves the same result more efficiently.

frontend/javascripts/viewer/model/actions/skeletontracing_actions.tsx (1)

62-62: LGTM! Clean refactoring for better naming clarity.

The renaming from SetTracingAction to SetSkeletonTracingAction makes the intent more explicit and better differentiates skeleton tracing from other tracing types.

Also applies to: 128-128, 530-534

frontend/javascripts/viewer/model/sagas/save_saga.ts (1)

366-390: Well-structured annotation diff logic

The performDiffAnnotation function correctly handles camera state changes and only generates update actions when actual changes are detected.

frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts (1)

268-307: Well-implemented user state integration

The changes properly integrate user-specific state management for volume tracings, including:

  • User-specific bounding boxes
  • Segment visibility overrides
  • Active segment selection
  • Proper fallback handling through getUserStateForTracing

The implementation aligns well with the PR objectives.

Also applies to: 427-431

app/models/annotation/AnnotationService.scala (1)

363-367: LGTM! Proper user state tracking for task assignments.

The addition of annotationBase._user and user._id parameters correctly implements the user state management feature, allowing the system to track both the original annotation owner and the new assignee.

app/models/annotation/AnnotationLayerPrecedence.scala (1)

31-31: Good addition for user-specific bounding box visibility tracking.

The new field properly captures per-user bounding box visibility states as a map from user ID to bounding box IDs and their visibility states.

frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts (2)

604-625: Excellent refactoring for granular tree group state management.

The new approach correctly separates content changes from expansion state changes, which is essential for the per-user state management feature. This ensures that one user's expansion/collapse actions don't affect other users' views.


630-648: Well-structured handling of user bounding box states.

The implementation properly distinguishes between content changes and visibility state changes, allowing for per-user visibility preferences while maintaining shared bounding box definitions.

frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx (1)

469-471: Identity-only short-circuit may miss in-place mutations

if (prevVolumeTracing === volumeTracing) return; assumes immutability.
If any nested field is mutated in place, the reference remains equal and no further diffing occurs, silently dropping required updates. Please ensure VolumeTracing objects are treated as strictly immutable (e.g. enforced by Redux reducers) or add a cheap hash / version check here.

app/models/task/TaskCreationService.scala (1)

72-79: Extra parameter breaks callers

createTracingsFromBaseAnnotations(...) now requires requestingUserId.
Please update all call sites (e.g. TaskController, tests) or compilation will fail.

Comment on lines 284 to 286
const segmentVisibilityMap: Record<number, boolean> = userState
? Object.fromEntries(_.zip(userState.segmentIds, userState.segmentVisibilities))
: {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Validate array lengths before creating segmentVisibilityMap

The _.zip function will create tuples with undefined values if userState.segmentIds and userState.segmentVisibilities have different lengths. This could lead to unexpected behavior in the visibility map.

-  const segmentVisibilityMap: Record<number, boolean> = userState
-    ? Object.fromEntries(_.zip(userState.segmentIds, userState.segmentVisibilities))
-    : {};
+  const segmentVisibilityMap: Record<number, boolean> = {};
+  if (userState) {
+    if (userState.segmentIds.length !== userState.segmentVisibilities.length) {
+      console.warn("Segment IDs and visibilities arrays have different lengths");
+    }
+    userState.segmentIds.forEach((id, index) => {
+      if (index < userState.segmentVisibilities.length) {
+        segmentVisibilityMap[id] = userState.segmentVisibilities[index];
+      }
+    });
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const segmentVisibilityMap: Record<number, boolean> = userState
? Object.fromEntries(_.zip(userState.segmentIds, userState.segmentVisibilities))
: {};
const segmentVisibilityMap: Record<number, boolean> = {};
if (userState) {
if (userState.segmentIds.length !== userState.segmentVisibilities.length) {
console.warn("Segment IDs and visibilities arrays have different lengths");
}
userState.segmentIds.forEach((id, index) => {
if (index < userState.segmentVisibilities.length) {
segmentVisibilityMap[id] = userState.segmentVisibilities[index];
}
});
}
🤖 Prompt for AI Agents
In frontend/javascripts/viewer/model/reducers/volumetracing_reducer.ts between
lines 284 and 286, the code creates segmentVisibilityMap using _.zip on
userState.segmentIds and userState.segmentVisibilities without validating their
lengths. To fix this, add a check to ensure both arrays have the same length
before calling _.zip, or handle mismatched lengths appropriately to prevent
unexpected undefined values in the resulting map.

Comment on lines +77 to +127
private def adaptSkeletonUserStates(
userStates: Seq[SkeletonUserStateProto],
oldPrecedenceLayerProperties: RedundantTracingProperties): Seq[SkeletonUserStateProto] = {
val adaptedExistingUserStates = userStates.map { userState =>
val userId = userState.userId
oldPrecedenceLayerProperties.userStateBoundingBoxVisibilities.get(userId) match {
case None => userState
case Some((precedenceBboxIds, precedenceBboxVisibilities)) =>
userState.copy(boundingBoxIds = precedenceBboxIds, boundingBoxVisibilities = precedenceBboxVisibilities)
}
}
// We also have to create new user states for the users the old precedence layer has, but the new precedence layer is missing.
val newUserPrecedenceProperties = oldPrecedenceLayerProperties.userStateBoundingBoxVisibilities.filter(tuple =>
!userStates.exists(_.userId == tuple._1))
val newUserStates = newUserPrecedenceProperties.map {
case (userId: String, (boundingBoxIds: Seq[Int], boundingBoxVisibilities: Seq[Boolean])) =>
SkeletonTracingDefaults
.emptyUserState(userId)
.copy(
boundingBoxIds = boundingBoxIds,
boundingBoxVisibilities = boundingBoxVisibilities
)
}
adaptedExistingUserStates ++ newUserStates
}

private def adaptVolumeUserStates(
userStates: Seq[VolumeUserStateProto],
oldPrecedenceLayerProperties: RedundantTracingProperties): Seq[VolumeUserStateProto] = {
val adaptedExistingUserStates = userStates.map { userState =>
val userId = userState.userId
oldPrecedenceLayerProperties.userStateBoundingBoxVisibilities.get(userId) match {
case None => userState
case Some((precedenceBboxIds, precedenceBboxVisibilities)) =>
userState.copy(boundingBoxIds = precedenceBboxIds, boundingBoxVisibilities = precedenceBboxVisibilities)
}
}
// We also have to create new user states for the users the old precedence layer has, but the new precedence layer is missing.
val newUserPrecedenceProperties = oldPrecedenceLayerProperties.userStateBoundingBoxVisibilities.filter(tuple =>
!userStates.exists(_.userId == tuple._1))
val newUserStates = newUserPrecedenceProperties.map {
case (userId: String, (boundingBoxIds: Seq[Int], boundingBoxVisibilities: Seq[Boolean])) =>
VolumeTracingDefaults
.emptyUserState(userId)
.copy(
boundingBoxIds = boundingBoxIds,
boundingBoxVisibilities = boundingBoxVisibilities
)
}
adaptedExistingUserStates ++ newUserStates
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider refactoring to eliminate code duplication.

The adaptSkeletonUserStates and adaptVolumeUserStates functions are nearly identical, differing only in their proto types. This duplication could lead to maintenance issues.

Consider extracting the common logic into a generic helper function:

private def adaptUserStatesGeneric[T](
    userStates: Seq[T],
    oldPrecedenceLayerProperties: RedundantTracingProperties,
    getUserId: T => String,
    updateBoundingBoxProperties: (T, Seq[Int], Seq[Boolean]) => T,
    createDefaultUserState: String => T
): Seq[T] = {
  val adaptedExistingUserStates = userStates.map { userState =>
    val userId = getUserId(userState)
    oldPrecedenceLayerProperties.userStateBoundingBoxVisibilities.get(userId) match {
      case None => userState
      case Some((precedenceBboxIds, precedenceBboxVisibilities)) =>
        updateBoundingBoxProperties(userState, precedenceBboxIds, precedenceBboxVisibilities)
    }
  }
  
  val newUserPrecedenceProperties = oldPrecedenceLayerProperties.userStateBoundingBoxVisibilities.filter(tuple =>
    !userStates.exists(state => getUserId(state) == tuple._1))
  
  val newUserStates = newUserPrecedenceProperties.map {
    case (userId: String, (boundingBoxIds: Seq[Int], boundingBoxVisibilities: Seq[Boolean])) =>
      val defaultState = createDefaultUserState(userId)
      updateBoundingBoxProperties(defaultState, boundingBoxIds, boundingBoxVisibilities)
  }
  
  adaptedExistingUserStates ++ newUserStates
}

Then simplify the specific implementations:

private def adaptSkeletonUserStates(
    userStates: Seq[SkeletonUserStateProto],
    oldPrecedenceLayerProperties: RedundantTracingProperties): Seq[SkeletonUserStateProto] = 
  adaptUserStatesGeneric(
    userStates,
    oldPrecedenceLayerProperties,
    _.userId,
    (state, ids, vis) => state.copy(boundingBoxIds = ids, boundingBoxVisibilities = vis),
    SkeletonTracingDefaults.emptyUserState
  )

private def adaptVolumeUserStates(
    userStates: Seq[VolumeUserStateProto],
    oldPrecedenceLayerProperties: RedundantTracingProperties): Seq[VolumeUserStateProto] = 
  adaptUserStatesGeneric(
    userStates,
    oldPrecedenceLayerProperties,
    _.userId,
    (state, ids, vis) => state.copy(boundingBoxIds = ids, boundingBoxVisibilities = vis),
    VolumeTracingDefaults.emptyUserState
  )
🤖 Prompt for AI Agents
In app/models/annotation/AnnotationLayerPrecedence.scala between lines 77 and
127, the functions adaptSkeletonUserStates and adaptVolumeUserStates are nearly
identical, leading to duplicated code. To improve maintainability, extract the
common logic into a generic helper function that takes parameters for user state
type, user ID extraction, state updating, and default state creation. Then
refactor the specific functions to call this generic helper with appropriate
arguments, reducing duplication and simplifying future updates.

Comment on lines +530 to +533
if (prevVolumeTracing.fallbackLayer != null && volumeTracing.fallbackLayer == null) {
yield removeFallbackLayer(volumeTracing.tracingId);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Only handles fallback-layer removal

You handle the transition “had layer → now none”, but not the inverse (“none → newly added”). If a fallback layer gets (re-)added, the scene will not update until a full reload. Add the complementary branch.

🤖 Prompt for AI Agents
In frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx around lines
530 to 533, the code only handles removing the fallback layer when transitioning
from having a fallback layer to none, but does not handle the case where a
fallback layer is added. Add an else-if branch to handle the scenario where
fallbackLayer is now non-null while previously it was null, and call a function
to add or update the fallback layer accordingly to ensure the scene updates
correctly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
app/controllers/AnnotationController.scala (1)

420-428: LGTM! Parameter order issue has been resolved.

The parameter order for duplicateAnnotation has been corrected to match the expected method signature. The call now properly passes:

  • annotation._id (annotationId)
  • newAnnotationId
  • annotation._user (ownerId - original annotation's user)
  • user._id (requestingUserId - user making the duplication request)

This addresses the issue flagged in the previous review about incorrect argument ordering.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala (2)

546-548: Previous syntax error has been fixed.

The trailing comma issue mentioned in past review comments has been resolved. The code now compiles correctly.


590-592: Previous logic error has been fixed.

The code now correctly updates treeIds and treeVisibilities instead of the incorrect treeGroupIds that was flagged in previous reviews.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/TreeUtils.scala (1)

31-36: Breaking API change requires caller update.

The mergeTrees method signature has changed from 4 to 6 parameters. The caller in SkeletonTracingService.scala needs to be updated to pass the additional nodeMappingA and groupMappingA parameters.

🧹 Nitpick comments (3)
webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/TSAnnotationController.scala (1)

192-281: Consider refactoring for better maintainability.

The mergedFromIds method correctly implements the merging logic and addresses validation concerns from past reviews. However, the method is quite complex (90 lines) and handles multiple responsibilities.

Consider extracting some of the logic into smaller, focused methods:

  • Volume merging logic (lines 212-246)
  • Skeleton merging logic (lines 247-257)
  • Final annotation assembly (lines 258-277)

This would improve readability and testability while maintaining the same functionality.

private def mergeVolumeTracings(annotations: Seq[AnnotationProto], 
                               ownerIds: Seq[String], 
                               requestingUserId: String,
                               newVolumeId: TracingId,
                               newAnnotationId: String,
                               toTemporaryStore: Boolean)
                              (implicit tc: TokenContext): Fox[(Option[VolumeTracing], Long)] = {
  // Extract volume merging logic here
}

private def mergeSkeletonTracings(annotations: Seq[AnnotationProto],
                                 ownerIds: Seq[String], 
                                 requestingUserId: String,
                                 newSkeletonId: TracingId,
                                 targetVersion: Long,
                                 toTemporaryStore: Boolean)
                                (implicit tc: TokenContext): Fox[Option[SkeletonTracing]] = {
  // Extract skeleton merging logic here  
}
webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/AnnotationUserStateUtils.scala (2)

32-137: Consider refactoring this large method for better maintainability.

The renderUserStateForTracing method is over 100 lines long with significant duplication between skeleton and volume cases. Consider extracting common logic into helper methods.

For example, you could extract common patterns:

private def buildVisibilityMap[T](userState: Option[_], 
                                  ids: Seq[T], 
                                  visibilities: Seq[Boolean]): Map[T, Boolean] = 
  userState.map(_ => ids.zip(visibilities).toMap).getOrElse(Map.empty)

private def mergeWithFallback[T](requestingMap: Map[Int, T], 
                                ownerMap: Map[Int, T], 
                                id: Int, 
                                default: Option[T]): Option[T] =
  requestingMap.get(id).orElse(ownerMap.get(id)).orElse(default)

This would reduce duplication and make the code more modular.


12-23: Consider adding ScalaDoc comments for public methods.

The trait and its public methods would benefit from documentation explaining the user state management behavior, especially the precedence rules when multiple user states exist.

Add documentation like:

/**
 * Utilities for managing user-specific annotation states.
 * Handles rendering, merging, and transforming user states with fallback logic.
 */
trait AnnotationUserStateUtils extends BoundingBoxMerger {

  /**
   * Renders user-specific state onto annotation layers.
   * Uses requesting user's state with fallback to owner's state.
   * 
   * @param annotationProto The annotation containing user states
   * @param annotationLayers The layers to render state onto
   * @param requestingUserId The user requesting the annotation (if any)
   * @param ownerId The owner of the annotation
   * @return Annotation layers with user-specific state applied
   */
  protected def renderUserState(...)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9f72f5b and cd504ba.

📒 Files selected for processing (6)
  • app/controllers/AnnotationController.scala (1 hunks)
  • test/backend/AnnotationUserStateTestSuite.scala (1 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/TSAnnotationController.scala (7 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/AnnotationUserStateUtils.scala (1 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/TreeUtils.scala (2 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala (7 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: frontend-tests
  • GitHub Check: backend-tests
  • GitHub Check: build-smoketest-push
🔇 Additional comments (23)
webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala (6)

5-11: LGTM! Proper imports for user state functionality.

The imports correctly include the necessary user state protocol buffer classes and skeleton tracing defaults.


26-45: Well-designed user state action trait with proper conditional logic.

The UserStateSkeletonUpdateAction trait provides a clean abstraction for user-specific skeleton updates. The applyOn method correctly handles both creating new user states and updating existing ones based on the presence of actionAuthorId.


428-465: Efficient implementation of tree group expansion state management.

The UpdateTreeGroupsExpandedStateSkeletonAction correctly uses mutable maps for efficient updates of expansion states. The logic properly handles both existing and new user states with appropriate fallbacks.


500-520: Simple and correct active node state management.

The UpdateActiveNodeSkeletonAction follows the established pattern and correctly updates the active node in user state with proper fallback to empty state when needed.


662-693: Consistent user state pattern for bounding box visibility.

The UpdateUserBoundingBoxVisibilitySkeletonAction correctly follows the established user state pattern with proper handling of single vs. all bounding boxes and mutable map operations for efficient updates.


731-756: Standard JSON format implementations.

The companion objects correctly provide JSON serialization formats for the new action classes following the established pattern.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/TSAnnotationController.scala (3)

37-41: LGTM! Request validation implemented correctly.

The MergedFromIdsRequest case class is well-defined with proper JSON serialization support. The validation for equal length sequences (mentioned in past reviews) has been properly implemented in the mergedFromIds method at line 199.


118-119: LGTM! Method signature updated appropriately for user state support.

The addition of ownerId and requestingUserId parameters to the duplicate method aligns with the PR objectives for user-specific state management. The parameters are correctly passed through to the annotation service.

Also applies to: 131-132


158-190: LGTM! Helper methods are well-structured and consistent.

Both helper methods follow a consistent pattern:

  • Proper filtering by annotation layer type
  • Correct variable naming (the previous volumeLayersOfAnnotation typo has been fixed)
  • Appropriate user state rendering logic
  • Good error handling with Fox monads

The methods effectively encapsulate the logic for retrieving and adapting tracings with user-specific states.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/TreeUtils.scala (6)

11-11: Good addition of type alias for clarity.

The TreeIdMap type alias improves code readability and makes the API more self-documenting.


37-47: Implementation correctly applies ID mappings.

The logic properly:

  • Extracts node IDs for efficient lookups
  • Applies tree, node, and group mappings to treesA
  • Applies only tree ID mapping to treesB
  • Preserves treesB order by concatenating it first

49-61: Comprehensive node mapping application.

The method correctly applies node mapping to all node references including nodes, edges, comments, and branch points. The addition of nodeIdsA parameter enables selective remapping in comment content.


62-69: Excellent improvement for selective node reference remapping.

The conditional remapping based on nodeIdsA.contains(oldId) ensures that only references to nodes from treesA are remapped, preserving references to external nodes. This prevents incorrect remapping of node IDs in comments.


77-89: Well-designed tree ID densification strategy.

The approach of creating dense sequential tree IDs prevents ID space explosion over multiple merges. The implementation correctly:

  • Creates non-overlapping ID ranges for both tree sets
  • Starts treesB IDs from 1 and treesA IDs from (treesB.length + 1)
  • Maintains a sorted, deterministic mapping

91-98: Parameter naming consistency maintained.

The parameter names are updated to match the new naming convention (treesA/treesB) while preserving the correct offset calculation logic.

test/backend/AnnotationUserStateTestSuite.scala (4)

1-7: LGTM: Clean imports and package structure.

The imports are well-organized and include all necessary dependencies for testing annotation user state functionality.


8-30: Well-structured test setup with comprehensive dummy data.

The dummy skeleton data provides good test coverage with two different user states having varying levels of completeness, which effectively tests the fallback behavior.


31-62: Test structure issues resolved - comprehensive skeleton user state testing.

The test structure issues mentioned in past review comments have been properly addressed. The tests now follow correct ScalaTest conventions with proper nesting and closing braces. The three test scenarios effectively cover:

  1. userA: Tests fallback behavior when user has sparse state data
  2. userB (owner): Tests owner's complete state usage
  3. userC: Tests fallback to owner's state when user has no state

The assertions correctly verify the expected merge/fallback logic for each scenario.


64-103:

✅ Verification successful

Verify the ID mapping logic in the volume user state merging test.

The test logic appears correct, but let me verify the expected behavior:

  • segmentIdMapB maps segment ID 1 → 2 for tracingB
  • groupMappingA maps group ID 1 → 6 for tracingA
  • Expected result: segmentIds = Seq(1L, 2L) and segmentGroupIds = Seq(6, 1)

This suggests tracingA keeps original segment ID (1) but gets mapped group ID (6), while tracingB gets mapped segment ID (2) but keeps original group ID (1).

Please confirm this is the intended merging behavior by running the following verification:


🏁 Script executed:

#!/bin/bash
# Search for the mergeVolumeUserStates function implementation to understand the expected behavior
ast-grep --pattern 'def mergeVolumeUserStates($$$) = {
  $$$
}'

Length of output: 65


Let’s locate the actual implementation to confirm its behavior:


🏁 Script executed:

#!/bin/bash
# Find where mergeVolumeUserStates is defined
rg -n 'def\s\+mergeVolumeUserStates' 

Length of output: 38


Let’s inspect the test imports and search the src tree for the real implementation:


🏁 Script executed:

#!/bin/bash
set -e

echo "=== Test file imports and package ==="
sed -n '1,50p' test/backend/AnnotationUserStateTestSuite.scala

echo
echo "=== Searching for mergeVolumeUserStates in src ==="
rg -n 'mergeVolumeUserStates' src

Length of output: 2437


Let’s search the entire repository for the mergeVolumeUserStates definition and, if not found, locate and inspect the AnnotationUserStateUtils file where it should live:


🏁 Script executed:

#!/bin/bash
set -e

echo "=== Searching for mergeVolumeUserStates ==="
rg -n mergeVolumeUserStates || echo "No matches for mergeVolumeUserStates"

echo
echo "=== Locating AnnotationUserStateUtils source ==="
utils_file=$(rg -l 'trait AnnotationUserStateUtils|object AnnotationUserStateUtils')
if [ -n "$utils_file" ]; then
  echo "Found file: $utils_file"
  echo
  echo "=== Inspecting first 200 lines of $utils_file ==="
  sed -n '1,200p' "$utils_file"
else
  echo "AnnotationUserStateUtils not found in repository"
fi

Length of output: 13410


No action needed: mergeVolumeUserStates mapping logic is correct.

The test correctly applies groupMappingA to tracing A’s segment group IDs and segmentIdMapB to tracing B’s segment IDs, producing
• segmentIds = Seq(1L, 2L)
• segmentVisibilities = Seq(true, false)
• segmentGroupIds = Seq(6, 1)
• segmentGroupExpandedStates = Seq(true, false)

All mappings behave as intended; the assertion stands.

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/AnnotationUserStateUtils.scala (4)

156-156: The merge order inconsistency has been fixed.

Good job fixing the merge order - both skeleton (line 156) and volume (line 202) implementations now consistently give precedence to the requesting user's visibility settings.

Also applies to: 202-202


274-281: The method name typo has been fixed.

The method names are now correctly spelled as applyIdMappingsOnSkeletonUserState and applyIdMappingsOnVolumeUserState.

Also applies to: 342-347


283-296: Good implementation of ID mapping with proper filtering.

The bounding box ID mapping methods correctly filter out unmapped IDs using flatMap, which prevents issues with missing mappings. The implementation is clean and handles edge cases well.

Also applies to: 349-362


261-272:

✅ Verification successful

Verify that duplicate IDs are handled correctly when merging user states.

The merge methods concatenate lists without handling potential duplicates. This could lead to duplicate tree/segment/bbox IDs if both user states contain settings for the same elements.

Run the following script to check if the codebase handles duplicate IDs when converting these lists to maps:

Consider whether you need to deduplicate or use the last-one-wins strategy when merging these lists.

Also applies to: 329-340


🏁 Script executed:

#!/bin/bash
# Description: Check how the concatenated lists are used and if duplicates are handled

# Search for usage of the merged lists to understand if duplicate handling is needed
ast-grep --pattern 'userState.$_Ids.zip(userState.$_)'

# Look for any deduplication logic
rg -A 5 "treeIds.*zip.*treeVisibilities|segmentIds.*zip.*segmentVisibilities|boundingBoxIds.*zip.*boundingBoxVisibilities"

Length of output: 19200


Duplicate ID handling confirmed; no action required
The merged lists are immediately converted into maps via .zip(...).toMap, which automatically deduplicates keys by keeping the last occurrence. Subsequent merges using ownerMap ++ requestingMap also apply a last-one-wins strategy.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx (2)

508-516: Reference equality check is appropriate in this context.

The reference equality check prevVolumeTracing.segments !== volumeTracing.segments is actually correct here because the Redux state management ensures immutability. When segments are updated, a new SegmentMap instance is created, so reference inequality properly indicates changes.

Note: This addresses the previous review comment about stale diffs. The memoization with cachedDiffSegmentLists combined with reference equality is the intended pattern for immutable state.


540-557: Fallback layer handling could be more comprehensive.

The current implementation only handles the removal of fallback layers but doesn't handle the case where a fallback layer is added.

Consider adding the complementary case:

  if (prevVolumeTracing.fallbackLayer != null && volumeTracing.fallbackLayer == null) {
    yield removeFallbackLayer(volumeTracing.tracingId);
  }
+ if (prevVolumeTracing.fallbackLayer == null && volumeTracing.fallbackLayer != null) {
+   // Handle fallback layer addition - implementation depends on requirements
+   // This might require a new update action or scene controller refresh
+ }
🧹 Nitpick comments (2)
frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts (2)

701-720: Remove TODO comment and clarify implementation status.

The bounding box diffing logic looks correct, but there's a "todop" comment that should be addressed.

Apply this diff to clean up the comment:

-    if (boxDiff.didContentChange) {
-      // todop
-      yield LEGACY_updateUserBoundingBoxesInSkeletonTracing(
+    if (boxDiff.didContentChange) {
+      // Use legacy action for content changes until full migration is complete
+      yield LEGACY_updateUserBoundingBoxesInSkeletonTracing(

723-729: Clarify the status of commented legacy code.

The commented-out diffBoundingBoxes call should either be removed if it's no longer needed or have a clear comment explaining why it's temporarily disabled.

Either remove the commented code or add a clear explanation:

-  // todop: integrate this?
-  // yield* diffBoundingBoxes(
-  //   prevSkeletonTracing.userBoundingBoxes,
-  //   skeletonTracing.userBoundingBoxes,
-  //   skeletonTracing.tracingId,
-  //   AnnotationLayerEnum.Skeleton,
-  // );
+  // Legacy diffBoundingBoxes approach is replaced by the granular diffing above
+  // Remove this once the new approach is fully validated
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd504ba and 59a9800.

📒 Files selected for processing (13)
  • CHANGELOG.unreleased.md (1 hunks)
  • frontend/javascripts/libs/utils.ts (2 hunks)
  • frontend/javascripts/viewer/model/helpers/compaction/compact_save_queue.ts (1 hunks)
  • frontend/javascripts/viewer/model/reducers/reducer_helpers.ts (8 hunks)
  • frontend/javascripts/viewer/model/reducers/skeletontracing_reducer_helpers.ts (1 hunks)
  • frontend/javascripts/viewer/model/sagas/save_saga.ts (7 hunks)
  • frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts (2 hunks)
  • frontend/javascripts/viewer/model/sagas/update_actions.ts (12 hunks)
  • frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx (3 hunks)
  • frontend/javascripts/viewer/view/version_entry.tsx (3 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/UpdateActions.scala (9 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala (7 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala (8 hunks)
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.unreleased.md
🚧 Files skipped from review as they are similar to previous changes (9)
  • frontend/javascripts/libs/utils.ts
  • frontend/javascripts/viewer/view/version_entry.tsx
  • frontend/javascripts/viewer/model/reducers/skeletontracing_reducer_helpers.ts
  • frontend/javascripts/viewer/model/reducers/reducer_helpers.ts
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/UpdateActions.scala
  • frontend/javascripts/viewer/model/helpers/compaction/compact_save_queue.ts
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala
  • frontend/javascripts/viewer/model/sagas/save_saga.ts
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala
🧰 Additional context used
🧬 Code Graph Analysis (1)
frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx (3)
frontend/javascripts/viewer/store.ts (1)
  • VolumeTracing (191-212)
frontend/javascripts/viewer/model/sagas/update_actions.ts (8)
  • updateActiveSegmentId (455-463)
  • updateLargestSegmentId (451-453)
  • LEGACY_updateUserBoundingBoxesInVolumeTracing (480-493)
  • updateUserBoundingBoxVisibilityInVolumeTracing (520-529)
  • updateSegmentGroups (719-727)
  • updateSegmentGroupsExpandedState (729-742)
  • removeFallbackLayer (791-798)
  • updateMappingName (830-845)
frontend/javascripts/viewer/model/helpers/diff_helpers.ts (2)
  • diffUserBoundingBoxes (74-94)
  • diffGroups (32-52)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: build-smoketest-push
  • GitHub Check: backend-tests
🔇 Additional comments (12)
frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts (3)

69-69: LGTM: Import changes align with the refactoring approach.

The imports correctly introduce the new granular update actions and mark legacy functions appropriately. The addition of specific update actions like updateActiveNode and updateTreeGroupsExpandedState supports the fine-grained update approach.

Also applies to: 76-76, 78-78, 82-82


675-695: LGTM: Well-structured group diffing logic.

The new logic properly separates content changes from expansion state changes, which is essential for per-user state management. The approach of checking didContentChange for server updates while handling expanded state separately is correct.


697-699: LGTM: Clean active node update logic.

The direct comparison and specific update action is much cleaner than the previous broad update approach.

frontend/javascripts/viewer/model/sagas/update_actions.ts (6)

28-32: LGTM: Consistent type definitions for new granular actions.

The new export types properly reflect the granular update approach and maintain consistency across skeleton and volume tracing actions.

Also applies to: 75-82, 88-88


117-123: LGTM: Union type updates maintain backward compatibility.

The union types correctly include the new granular actions while preserving legacy action types for backward compatibility.

Also applies to: 142-142, 145-145, 147-147


371-408: LGTM: Proper legacy marking and clean implementation.

The legacy function is correctly marked and not exported, while the new updateActiveNode function has a clean, focused implementation that only handles the active node state.


427-463: LGTM: Consistent granular action pattern.

The new updateActiveSegmentId and updateLargestSegmentId functions follow the same pattern as the skeleton tracing actions, providing a consistent API across tracing types.


729-757: LGTM: Well-designed expanded state actions.

The updateSegmentGroupsExpandedState and updateTreeGroupsExpandedState functions properly support batch updates of expansion states, which is efficient for user state management.


799-814: LGTM: Clean camera annotation action.

The updateCameraAnnotation function properly separates camera state from tracing data, which aligns with the goal of separating user-specific UI state from annotation content.

frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx (3)

467-479: LGTM: Clean granular property updates.

The new approach of checking individual properties and dispatching specific update actions is much cleaner and supports per-user state management effectively.


481-506: LGTM: Consistent bounding box diffing pattern.

The bounding box diffing logic follows the same pattern as the skeleton tracing saga, providing consistency across tracing types. The use of the diffUserBoundingBoxes helper function is appropriate.


518-538: LGTM: Well-structured group diffing implementation.

The group diffing logic properly separates content changes from expansion state changes, using the helper function consistently with the skeleton tracing approach.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
frontend/javascripts/viewer/model/helpers/diff_helpers.ts (1)

18-24: Consider adding return type annotation for consistency.

While the implementation is correct, adding an explicit return type would improve consistency with other functions in the codebase.

-function stripIsExpanded(groups: TreeGroup[]): TreeGroup[] {
+function stripIsExpanded(groups: TreeGroup[]): Omit<TreeGroup, "isExpanded">[] {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59a9800 and 0bb0ced.

📒 Files selected for processing (7)
  • frontend/javascripts/test/sagas/bounding_box_saving.spec.ts (4 hunks)
  • frontend/javascripts/viewer/model/helpers/diff_helpers.ts (1 hunks)
  • frontend/javascripts/viewer/model/reducers/reducer_helpers.ts (7 hunks)
  • frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts (2 hunks)
  • frontend/javascripts/viewer/model/sagas/update_actions.ts (15 hunks)
  • frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx (3 hunks)
  • frontend/javascripts/viewer/store.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/javascripts/viewer/store.ts
  • frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx
  • frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts
🧰 Additional context used
🧬 Code Graph Analysis (2)
frontend/javascripts/test/sagas/bounding_box_saving.spec.ts (1)
frontend/javascripts/viewer/model/helpers/diff_helpers.ts (1)
  • diffBoundingBoxes (186-201)
frontend/javascripts/viewer/model/reducers/reducer_helpers.ts (4)
frontend/javascripts/types/api_types.ts (4)
  • ServerBoundingBox (838-843)
  • UserBoundingBoxFromServer (844-850)
  • SkeletonUserState (916-931)
  • VolumeUserState (946-961)
frontend/javascripts/viewer/constants.ts (1)
  • BoundingBoxType (41-44)
frontend/javascripts/viewer/store.ts (3)
  • UserBoundingBox (110-112)
  • UserBoundingBoxWithOptIsVisible (113-115)
  • UserBoundingBoxToServer (91-97)
frontend/javascripts/viewer/model/types/tree_types.ts (2)
  • TreeGroup (85-87)
  • Tree (61-75)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: backend-tests
  • GitHub Check: build-smoketest-push
  • GitHub Check: frontend-tests
🔇 Additional comments (7)
frontend/javascripts/test/sagas/bounding_box_saving.spec.ts (1)

3-3: LGTM! Test correctly updated for new diffing behavior.

The test has been properly updated to:

  • Use the new diffBoundingBoxes helper from the diff_helpers module
  • Handle the increased number of diff actions (from 4 to 5) due to separate visibility updates
  • Assert the correct structure for the new visibility update actions

Also applies to: 67-119

frontend/javascripts/viewer/model/reducers/reducer_helpers.ts (2)

46-65: Well-implemented user state integration for bounding boxes.

The function correctly:

  • Creates an id-to-visibility mapping from the user state arrays using lodash zip
  • Falls back through idToVisible[id] ?? isVisible ?? true to ensure a sensible default
  • Handles undefined user state gracefully

223-248: Robust implementation of group expansion state handling.

Good use of type guards to distinguish between skeleton and volume user states. The recursive mapping with Utils.mapGroupsDeep ensures all nested groups receive their user-specific expansion states.

frontend/javascripts/viewer/model/helpers/diff_helpers.ts (1)

44-201: Excellent implementation of diffing utilities.

The diffing logic is well-structured with:

  • Early return optimizations for reference equality
  • Proper separation between content and visibility changes
  • Clear error messages for debugging
  • Efficient use of generator functions
frontend/javascripts/viewer/model/sagas/update_actions.ts (3)

35-36: Good practice: Legacy actions clearly marked.

Renaming legacy actions with the LEGACY_ prefix and adding deprecation comments helps prevent their use in new code while maintaining backward compatibility.

Also applies to: 123-124, 378-401, 433-455


403-469: Well-designed granular update actions.

The new update actions for active nodes, segments, group expansion states, and camera position provide better granularity and align well with the per-user state feature. This will improve performance by reducing unnecessary updates.

Also applies to: 735-820


500-524:

❓ Verification inconclusive

Verify callers are updated for the parameter type change.

The parameter type for bounding box functions changed from UserBoundingBox to UserBoundingBoxWithOptIsVisible. This is a good change as it makes the isVisible property optional, but ensure all callers have been updated.


🏁 Script executed:

#!/bin/bash
# Description: Check for calls to addUserBoundingBox functions to ensure they handle the optional isVisible property

# Search for calls to these functions
echo "Searching for addUserBoundingBoxInSkeletonTracing calls:"
rg -A 3 "addUserBoundingBoxInSkeletonTracing\(" --type ts --type tsx

echo -e "\nSearching for addUserBoundingBoxInVolumeTracing calls:"
rg -A 3 "addUserBoundingBoxInVolumeTracing\(" --type ts --type tsx

Length of output: 443


I’ll broaden the search to include all .ts and .tsx files without relying on predefined types:


🏁 Script executed:

#!/bin/bash
# Re-run search across .ts and .tsx globs

echo "Searching for addUserBoundingBoxInSkeletonTracing calls:"
rg -A 3 "addUserBoundingBoxInSkeletonTracing\(" -g '*.ts' -g '*.tsx'

echo -e "\nSearching for addUserBoundingBoxInVolumeTracing calls:"
rg -A 3 "addUserBoundingBoxInVolumeTracing\(" -g '*.ts' -g '*.tsx'

Length of output: 1113


Verify callers updated for optional isVisible

I didn’t find any calls to addUserBoundingBoxInSkeletonTracing or addUserBoundingBoxInVolumeTracing outside their own definitions. Please review all usages—especially where these actions are dispatched or handled—to ensure they correctly pass or guard the now‐optional isVisible property:

• Check every yield put(addUserBoundingBoxInSkeletonTracing(...)) and yield put(addUserBoundingBoxInVolumeTracing(...)) in your sagas.
• Confirm any reducers or middleware that read action.value.boundingBox.isVisible handle undefined.
• If you match these actions by name or string in other parts of the code, make sure those references are updated.

Copy link
Member

@daniel-wer daniel-wer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice work! I'll test after the PR feedback was addressed :)

@@ -1107,6 +1107,32 @@ export function diffObjects<K extends string | number | symbol, V, Dict extends
return _.fromPairs(_.differenceWith(_.toPairs(b), _.toPairs(a), _.isEqual)) as Partial<Dict>;
}

export function diffSets<T>(setA: Set<T>, setB: Set<T>) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be premature optimization, but as far as I can see only bWithoutA is ever used from the result of this function. That could be more easily (and probably faster) computed using the set's difference method: setB.difference(setA).

@@ -123,8 +112,8 @@ describe("Saga Integration Tests", () => {
const skeletonSaveQueue = Store.getState().save.queue;

// There should only be one chunk
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is no longer accurate. Please mention what the first save queue entry is

Comment on lines 11 to +15
const updateSkeletonTracingOnlyBatches = updateActionsBatches.filter(
(batch) => batch.actions.length === 1 && batch.actions[0].name === "updateSkeletonTracing",
(batch) => batch.actions.length === 1 && batch.actions[0].name === "updateActiveNode",
);
const updateVolumeTracingOnlyBatches = updateActionsBatches.filter(
(batch) => batch.actions.length === 1 && batch.actions[0].name === "updateVolumeTracing",
(batch) => batch.actions.length === 1 && batch.actions[0].name === "updateActiveSegmentId",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The names for the two variables should be adapted

export type APITracingStoreAnnotation = {
readonly description: string;
readonly version: number;
readonly earliestAccessibleVersion: number;
readonly annotationLayers: APITracingStoreAnnotationLayer[];
readonly userStates: APIAnnotationUserState[];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit unexpected to me that all user states are accessible to each viewer of the annotation. I would have expected the server to either select the state of the active user and only return that or the data model to be different so that that happens "automatically" (because the user state is saved at the user instead of the annotation).
At a minimum the current solution exposes who has viewed an annotation (together with the user-specific properties). Whether this is problematic at the moment is debatable but I fear that more "sensitive" data might slip in there in the future. I might be overly cautious though, so I'm interested in your opinion :)

? userState.segmentGroupExpandedStates
: userState.treeGroupExpandedStates;

const segmentGroupToExpanded: Record<number, boolean> = Object.fromEntries(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rename to groupIdToExpanded since both segments and tree groups are supported

) {
if (prevBoundingBoxes === currentBoundingBoxes) return;

yield* diffBoundingBoxesContents(prevBoundingBoxes, currentBoundingBoxes, tracingId, tracingType);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
yield* diffBoundingBoxesContents(prevBoundingBoxes, currentBoundingBoxes, tracingId, tracingType);
yield* diffBoundingBoxContents(prevBoundingBoxes, currentBoundingBoxes, tracingId, tracingType);

return { visible, invisible };
}

function* diffBoundingBoxesContents(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function* diffBoundingBoxesContents(
function* diffBoundingBoxContents(

},
updateCamera: (_action: AsServerAction<UpdateCameraAnnotationAction>): Description => {
return {
description: "Updated the camera position",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could actually be one of editPosition, editRotation, zoomLevel, editPositionAdditionalCoordinates. Maybe simply "Adjusted the camera"?

action: AsServerAction<UpdateSegmentGroupsExpandedStateUpdateAction>,
): Description => {
return {
description: `${action.value.areExpanded ? "Expanded" : "Collapsed"} some tree groups..`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description: `${action.value.areExpanded ? "Expanded" : "Collapsed"} some tree groups..`,
description: `${action.value.areExpanded ? "Expanded" : "Collapsed"} some tree groups.`,

};
},
updateTreeGroupsExpandedState: (
action: AsServerAction<UpdateSegmentGroupsExpandedStateUpdateAction>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
action: AsServerAction<UpdateSegmentGroupsExpandedStateUpdateAction>,
action: AsServerAction<UpdateTreeGroupsExpandedStateSkeletonAction>,

Needs to be imported as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Milestone 1: Store User-dependent information separately
3 participants