Move SSD side coverage helper to typed Kotlin

ober

b2841cb68652cb8a50eca24b7d585995002bf09c

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index e99763b..024db02 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -650,20 +650,20 @@
             (int32 255)))
         (def (pixelBlue (pixel : Int32)) : Int32
           (bitwise-and pixel (int32 255)))
-        (def (maxInt32 (a : Int32) (b : Int32)) : Int32
+        (def (pixel-max-int32 (a : Int32) (b : Int32)) : Int32
           (if (> a b) a b))
-        (def (minInt32 (a : Int32) (b : Int32)) : Int32
+        (def (pixel-min-int32 (a : Int32) (b : Int32)) : Int32
           (if (< a b) a b))
         (def (pixelMaxChannel (pixel : Int32)) : Int32
           (let ((r (pixelRed pixel))
                 (g (pixelGreen pixel))
                 (b (pixelBlue pixel)))
-            (maxInt32 r (maxInt32 g b))))
+            (pixel-max-int32 r (pixel-max-int32 g b))))
         (def (pixelMinChannel (pixel : Int32)) : Int32
           (let ((r (pixelRed pixel))
                 (g (pixelGreen pixel))
                 (b (pixelBlue pixel)))
-            (minInt32 r (minInt32 g b))))
+            (pixel-min-int32 r (pixel-min-int32 g b))))
         (def (pixelLuminance (pixel : Int32)) : Float
           (+ (+ (* 0.2126 (exact->inexact (pixelRed pixel)))
                 (* 0.7152 (exact->inexact (pixelGreen pixel))))
@@ -705,10 +705,16 @@
 
     (typed-kotlin-file "com/sfb/ssdreview/VisionArrays.kt"
       (typed-library (com sfb ssdreview)
-        (export grayAt edgeAt)
+        (export grayAt edgeAt sideCoverage)
         (type Int32)
         (type IntArray)
         (type BooleanArray)
+        (type Float32)
+        (type FloatArray)
+        (def (array-max-int32 (a : Int32) (b : Int32)) : Int32
+          (if (> a b) a b))
+        (def (array-min-int32 (a : Int32) (b : Int32)) : Int32
+          (if (< a b) a b))
         (def (grayAt (gray : IntArray)
                      (width : Int32)
                      (height : Int32)
@@ -730,7 +736,87 @@
                       (or (>= x width)
                           (>= y height))))
             #f
-            (boolean-array-ref edge (+ (* y width) x))))))
+            (boolean-array-ref edge (+ (* y width) x))))
+        (def (edgeBandTop? (edge : BooleanArray)
+                           (width : Int32)
+                           (height : Int32)
+                           (px : Int32)
+                           (y : Int32)
+                           (band : Int32)) : Bool
+          (for/fold ((hit #f))
+                    ((offset (in-range (int32 0) band)))
+            (or hit (edgeAt edge width height px (+ y offset)))))
+        (def (edgeBandBottom? (edge : BooleanArray)
+                              (width : Int32)
+                              (height : Int32)
+                              (px : Int32)
+                              (y : Int32)
+                              (h : Int32)
+                              (band : Int32)) : Bool
+          (for/fold ((hit #f))
+                    ((offset (in-range (int32 0) band)))
+            (or hit (edgeAt edge width height px (- (+ y h) (+ (int32 1) offset))))))
+        (def (edgeBandLeft? (edge : BooleanArray)
+                            (width : Int32)
+                            (height : Int32)
+                            (x : Int32)
+                            (py : Int32)
+                            (band : Int32)) : Bool
+          (for/fold ((hit #f))
+                    ((offset (in-range (int32 0) band)))
+            (or hit (edgeAt edge width height (+ x offset) py))))
+        (def (edgeBandRight? (edge : BooleanArray)
+                             (width : Int32)
+                             (height : Int32)
+                             (x : Int32)
+                             (py : Int32)
+                             (w : Int32)
+                             (band : Int32)) : Bool
+          (for/fold ((hit #f))
+                    ((offset (in-range (int32 0) band)))
+            (or hit (edgeAt edge width height (- (+ x w) (+ (int32 1) offset)) py))))
+        (def (sideCoverage (edge : BooleanArray)
+                           (width : Int32)
+                           (height : Int32)
+                           (x : Int32)
+                           (y : Int32)
+                           (w : Int32)
+                           (h : Int32)) : FloatArray
+          (let ((band (array-max-int32
+                        (int32 1)
+                        (array-min-int32
+                          (int32 2)
+                          (array-min-int32 (/ w (int32 2)) (/ h (int32 2)))))))
+            (let ((top
+                    (for/fold ((count (int32 0)))
+                              ((px (in-range x (+ x w))))
+                      (if (edgeBandTop? edge width height px y band)
+                        (+ count (int32 1))
+                        count)))
+                  (bottom
+                    (for/fold ((count (int32 0)))
+                              ((px (in-range x (+ x w))))
+                      (if (edgeBandBottom? edge width height px y h band)
+                        (+ count (int32 1))
+                        count)))
+                  (left
+                    (for/fold ((count (int32 0)))
+                              ((py (in-range y (+ y h))))
+                      (if (edgeBandLeft? edge width height x py band)
+                        (+ count (int32 1))
+                        count)))
+                  (right
+                    (for/fold ((count (int32 0)))
+                              ((py (in-range y (+ y h))))
+                      (if (edgeBandRight? edge width height x py w band)
+                        (+ count (int32 1))
+                        count))))
+              (float-array
+                (/ (float32 top) (float32 (array-max-int32 (int32 1) w)))
+                (/ (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)))))))))
+
     (typed-kotlin-file "com/sfb/ssdreview/UiTextHelpers.kt"
       (typed-library (com sfb ssdreview)
         (export normalizeFiringArc normalizeLabel isGenericGuessLabel isSsdPdfName
@@ -3235,37 +3321,6 @@
        "        )"
        "    }"
        ""
-       "    private fun sideCoverage(edge: BooleanArray, width: Int, height: Int, x: Int, y: Int, w: Int, h: Int): FloatArray {"
-       "        val band = max(1, min(2, min(w / 2, h / 2)))"
-       "        val counts = IntArray(4)"
-       "        for (px in x until x + w) {"
-       "            var top = false"
-       "            var bottom = false"
-       "            for (offset in 0 until band) {"
-       "                top = top || edgeAt(edge, width, height, px, y + offset)"
-       "                bottom = bottom || edgeAt(edge, width, height, px, y + h - 1 - offset)"
-       "            }"
-       "            if (top) counts[0] += 1"
-       "            if (bottom) counts[1] += 1"
-       "        }"
-       "        for (py in y until y + h) {"
-       "            var left = false"
-       "            var right = false"
-       "            for (offset in 0 until band) {"
-       "                left = left || edgeAt(edge, width, height, x + offset, py)"
-       "                right = right || edgeAt(edge, width, height, x + w - 1 - offset, py)"
-       "            }"
-       "            if (left) counts[2] += 1"
-       "            if (right) counts[3] += 1"
-       "        }"
-       "        return floatArrayOf("
-       "            counts[0].toFloat() / max(1, w),"
-       "            counts[1].toFloat() / max(1, w),"
-       "            counts[2].toFloat() / max(1, h),"
-       "            counts[3].toFloat() / max(1, h)"
-       "        )"
-       "    }"
-       ""
        "    private fun edgeMask(gray: IntArray, width: Int, height: Int): BooleanArray {"
        "        val gradients = IntArray(width * height)"
        "        val samples = mutableListOf<Int>()"