Move SSD group cell updates to typed Kotlin

ober

d8b8408d515c5a7ae56a15bfc44c5dff82e464fc

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index b3a52a0..e1ab6d6 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -460,7 +460,9 @@
                 SsdSession-ssdArea SsdSession-ssdArea-set!
                 ssdSessionCellInsideSsdArea ssdSessionGroupInsideSsdArea
                 ssdSessionSuppressedGroupPresent?
-                ssdSessionFindCellById ssdSessionRecomputeGroup
+                ssdSessionFindCellById ssdSessionCellsByIds
+                ssdSessionBBoxForCellIds ssdSessionRecomputeGroup
+                appendUniqueCellIdsToGroup
                 ssdSessionSetSsdArea ssdSessionPruneGroupsOutsideSsdArea
                 createManualCells
                 ssdSessionNextCellId ssdSessionNextGroupId
@@ -633,6 +635,16 @@
                   (nullable-some cell)
                   found))
               found)))
+        (def (ssdSessionCellsByIds (session : SsdSession)
+                                   (ids : (List String))) : (MutableList SsdCell)
+          (for/fold ((out (mutable-list-empty SsdCell)))
+                    ((i (in-range (int32 0) (list-size ids))))
+            (let ((cell (ssdSessionFindCellById session (list-ref ids i))))
+              (if (nullable-null? cell)
+                out
+                (begin
+                  (mutable-list-add! out (nullable-get cell))
+                  out)))))
         (def (ssdSessionGroupCells (session : SsdSession)
                                    (group : SsdGroup)) : (MutableList SsdCell)
           (for/fold ((out (mutable-list-empty SsdCell)))
@@ -645,6 +657,20 @@
                 (begin
                   (mutable-list-add! out (nullable-get cell))
                   out)))))
+        (def (ssdGroupCellIdPresent? (group : SsdGroup) (id : String)) : Bool
+          (for/fold ((found #f))
+                    ((i (in-range (int32 0) (list-size (SsdGroup-cellIds group)))))
+            (or found (equal? (list-ref (SsdGroup-cellIds group) i) id))))
+        (def (appendUniqueCellIdsToGroup (group : SsdGroup)
+                                         (ids : (List String))) : Int32
+          (for/fold ((added (int32 0)))
+                    ((i (in-range (int32 0) (list-size ids))))
+            (let ((id (list-ref ids i)))
+              (if (ssdGroupCellIdPresent? group id)
+                added
+                (begin
+                  (mutable-list-add! (SsdGroup-cellIds group) id)
+                  (+ added (int32 1)))))))
         (def (bboxForMutableCells (cells : (MutableList SsdCell))) : FloatArray
           (if (= (list-size cells) (int32 0))
             (float-array (float32 0.0) (float32 0.0) (float32 0.0) (float32 0.0))
@@ -668,6 +694,9 @@
                   (let ((cell (list-ref cells i)))
                     (let ((bottom (+ (SsdCell-y cell) (SsdCell-h cell))))
                       (if (> bottom y2) bottom y2))))))))
+        (def (ssdSessionBBoxForCellIds (session : SsdSession)
+                                       (ids : (List String))) : FloatArray
+          (bboxForMutableCells (ssdSessionCellsByIds session ids)))
         (def (ssdSessionRecomputeGroup (session : SsdSession) (group : SsdGroup)) : Unit
           (let ((groupCells (ssdSessionGroupCells session group)))
             (begin
@@ -4222,7 +4251,7 @@
        "            boxTypeId = resolvedType?.id ?: boxType,"
        "            status = \"reviewed\","
        "            count = ids.size,"
-       "            bbox = bboxForCells(ids.mapNotNull { id -> session.cells.firstOrNull { it.id == id } }),"
+       "            bbox = ssdSessionBBoxForCellIds(session, ids),"
        "            cellIds = ids,"
        "            firingArc = normalizeFiringArc(firingArc),"
        "            notes = \"Approved on Android\""
@@ -4249,12 +4278,7 @@
        "        } else {"
        "            createManualCells(session, rect, count.coerceIn(1, 400))"
        "        }"
-       "        val before = group.cellIds.size"
-       "        ids.forEach { id ->"
-       "            if (!group.cellIds.contains(id)) {"
-       "                group.cellIds.add(id)"
-       "            }"
-       "        }"
+       "        val added = appendUniqueCellIdsToGroup(group, ids)"
        "        ssdSessionRecomputeGroup(session, group)"
        "        group.status = \"reviewed\""
        "        group.notes = \"Appended on Android\""
@@ -4264,7 +4288,6 @@
        "        truthStore.appendEvent(\"group_appended\", session)"
        "        reviewView.performHapticFeedback(HapticFeedbackConstants.CONFIRM)"
        "        reviewView.invalidate()"
-       "        val added = group.cellIds.size - before"
        "        saveSessionAsync("
        "            session,"
        "            \"Appended $added boxes to ${group.label.ifBlank { group.id }}; saving...\","