Move SSD review selection matching to typed Kotlin

ober

b6b48691380cd02dcf8ad0f7671e98ff51265596

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index bc24666..2a394c9 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -904,9 +904,30 @@
 
     (typed-kotlin-file "com/sfb/ssdreview/SsdReviewGeometry.kt"
       (typed-library (com sfb ssdreview)
-        (export hitGroupId)
+        (export hitGroupId selectionMatchingCells cellIdsForSelection)
         (type Float32)
         (type FloatArray)
+        (def (selectionCellMatches? (rect : FloatArray) (cell : SsdCell)) : Bool
+          (let ((overlap (rectOverlapArea rect (ssdCellRect cell)))
+                (area (* (SsdCell-w cell) (SsdCell-h cell))))
+            (>= (/ overlap (if (> area (float32 1.0)) area (float32 1.0)))
+                (float32 0.08))))
+        (def (selectionMatchingCells (cells : (MutableList SsdCell))
+                                     (rect : FloatArray)) : (MutableList SsdCell)
+          (for/fold ((out (mutable-list-empty SsdCell)))
+                    ((i (in-range (int32 0) (list-size cells))))
+            (let ((cell (list-ref cells i)))
+              (if (selectionCellMatches? rect cell)
+                  (begin
+                    (mutable-list-add! out cell)
+                    out)
+                  out))))
+        (def (cellIdsForSelection (cells : (MutableList SsdCell))) : (MutableList String)
+          (for/fold ((ids (mutable-list-empty String)))
+                    ((i (in-range (int32 0) (list-size cells))))
+            (begin
+              (mutable-list-add! ids (SsdCell-id (list-ref cells i)))
+              ids)))
         (def (pointInPaddedGroup? (group : SsdGroup)
                                   (x : Float32)
                                   (y : Float32)
@@ -5354,14 +5375,7 @@
 	       "            onStatus?.invoke(\"Selection ignored: drag a larger area\")"
 	       "            return"
 	       "        }"
-	       "        val matching = if (snapSelectionToCells) {"
-	       "            sess.cells.filter { cell ->"
-	       "                val overlap = rectOverlapArea(rect, ssdCellRect(cell))"
-	       "                overlap / max(1f, cell.w * cell.h) >= 0.08f"
-	       "            }"
-	       "        } else {"
-	       "            emptyList()"
-	       "        }"
+	       "        val matching = if (snapSelectionToCells) selectionMatchingCells(sess.cells, rect) else mutableListOf()"
 	       "        val snapped = if (!snapSelectionToCells) {"
 	       "            rect"
 	       "        } else if (matching.isNotEmpty()) {"
@@ -5379,7 +5393,7 @@
 	       "                else -> \"Shrank rough selection to SSD boxes\""
 	       "            }"
 	       "        )"
-       "        onAreaSelected?.invoke(snapped, matching.map { it.id })"
+       "        onAreaSelected?.invoke(snapped, cellIdsForSelection(matching))"
        "        invalidate()"
        "    }"
        ""