Move SSD tone cell helper to typed Kotlin

ober

abc199c67369897d9d4d762dc7f32df417362dce

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 332e833..c2e02c1 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -705,7 +705,7 @@
 
     (typed-kotlin-file "com/sfb/ssdreview/VisionArrays.kt"
       (typed-library (com sfb ssdreview)
-        (export grayAt edgeAt sideCoverage darkSideCoverage contourCell)
+        (export grayAt edgeAt sideCoverage darkSideCoverage contourCell toneCell)
         (type Int32)
         (type IntArray)
         (type BooleanArray)
@@ -905,6 +905,29 @@
                 (/ (float32 bottom) (float32 (array-max-int32 (int32 1) w)))
                 (/ (float32 left) (float32 (array-max-int32 (int32 1) h)))
                 (/ (float32 right) (float32 (array-max-int32 (int32 1) h)))))))
+        (def (toneCell (component : Component)
+                       (gray : IntArray)
+                       (width : Int32)
+                       (height : Int32)) : (Nullable SsdCell)
+          (let ((cell (componentCell component "android-tone")))
+            (if (nullable-null? cell)
+              (nullable-none SsdCell)
+              (let ((pad (int32 2)))
+                (let ((x (array-max-int32 (int32 0) (- (Component-x1 component) pad)))
+                      (y (array-max-int32 (int32 0) (- (Component-y1 component) pad)))
+                      (x2 (array-min-int32 (- width (int32 1)) (+ (Component-x2 component) pad)))
+                      (y2 (array-min-int32 (- height (int32 1)) (+ (Component-y2 component) pad))))
+                  (let ((coverage
+                          (darkSideCoverage gray width height
+                                            x
+                                            y
+                                            (+ (- x2 x) (int32 1))
+                                            (+ (- y2 y) (int32 1)))))
+                    (if (or (< (coverage-count-at-least coverage (float32 0.20))
+                               (int32 3))
+                            (< (coverage-sum coverage) (float32 1.0)))
+                      (nullable-none SsdCell)
+                      cell)))))))
         (def (contourCell (component : Component)
                           (edge : BooleanArray)
                           (width : Int32)
@@ -3375,20 +3398,6 @@
        "        return components"
        "    }"
        ""
-       "    private fun toneCell(component: Component, gray: IntArray, width: Int, height: Int): SsdCell? {"
-       "        val cell = componentCell(component, \"android-tone\") ?: return null"
-       "        val pad = 2"
-       "        val x = max(0, component.x1 - pad)"
-       "        val y = max(0, component.y1 - pad)"
-       "        val x2 = min(width - 1, component.x2 + pad)"
-       "        val y2 = min(height - 1, component.y2 + pad)"
-       "        val coverage = darkSideCoverage(gray, width, height, x, y, x2 - x + 1, y2 - y + 1)"
-       "        val coveredSides = coverage.count { it >= 0.20f }"
-       "        val total = coverage.sum()"
-       "        if (coveredSides < 3 || total < 1.0f) return null"
-       "        return cell"
-       "    }"
-       ""
        "    private fun contourCells(width: Int, height: Int, gray: IntArray): List<SsdCell> {"
        "        val edge = edgeMask(gray, width, height)"
        "        return detectMask(width, height, IntArray(width * height), \"android-contour\") { edge[it] }"